@plenary/react 0.5.1 → 0.6.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/dist/bridge.js CHANGED
@@ -105,6 +105,7 @@ function PlenaryBridge({
105
105
  const [active, setActive] = useState(false);
106
106
  const [selectedId, setSelectedId] = useState(null);
107
107
  const [hoverId, setHoverId] = useState(null);
108
+ const [overlay, setOverlay] = useState(false);
108
109
  const [dropHint, setDropHint] = useState(null);
109
110
  const consoleOrigin = useRef(null);
110
111
  const [remountEpochs, setRemountEpochs] = useState(/* @__PURE__ */ new Map());
@@ -227,6 +228,7 @@ function PlenaryBridge({
227
228
  case "init":
228
229
  consoleOrigin.current = e.origin;
229
230
  setActive(true);
231
+ setOverlay(msg.payload?.overlay === true);
230
232
  send(
231
233
  "manifest",
232
234
  { components: getManifest(), theme: getTheme() },
@@ -295,6 +297,22 @@ function PlenaryBridge({
295
297
  if (sizeTimer) clearTimeout(sizeTimer);
296
298
  };
297
299
  }, [editorOrigins, sdkVersion, region, send, endEdit]);
300
+ useEffect(() => {
301
+ if (!overlay) return;
302
+ let frame = 0;
303
+ let last = "";
304
+ const measure = () => {
305
+ const payload = { selected: measureNode(selectedId), hover: measureNode(hoverId) };
306
+ const key = JSON.stringify(payload);
307
+ if (key !== last) {
308
+ last = key;
309
+ send("rects", payload);
310
+ }
311
+ frame = requestAnimationFrame(measure);
312
+ };
313
+ measure();
314
+ return () => cancelAnimationFrame(frame);
315
+ }, [overlay, selectedId, hoverId, send]);
298
316
  const beginEdit = (nodeId, el) => {
299
317
  endEdit(true);
300
318
  const onBlur = () => endEdit(true);
@@ -345,15 +363,34 @@ function PlenaryBridge({
345
363
  onDrop,
346
364
  onDragLeave,
347
365
  children: [
366
+ /* @__PURE__ */ jsx("style", { "data-plenary-editor": "", dangerouslySetInnerHTML: { __html: EDITOR_CSS } }),
348
367
  regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null,
349
368
  regionDoc ? videoControlsBadgeIds(regionDoc).map((id) => /* @__PURE__ */ jsx(VideoControlsBadge, { nodeId: id }, id)) : null,
350
- selectedId ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: selectedId, variant: "selected" }) : null,
351
- hoverId && hoverId !== selectedId ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: hoverId, variant: "hover" }) : null,
369
+ selectedId && !overlay ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: selectedId, variant: "selected" }) : null,
370
+ hoverId && hoverId !== selectedId && !overlay ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: hoverId, variant: "hover" }) : null,
352
371
  dropHint ? /* @__PURE__ */ jsx(DropIndicator, { hint: dropHint }) : null
353
372
  ]
354
373
  }
355
374
  );
356
375
  }
376
+ const EDITOR_ACCENT = "9, 142, 246";
377
+ const EDITOR_CSS = `
378
+ [data-plenary-empty] > * { position: relative; min-height: 18px; }
379
+ [data-plenary-empty] > *::before {
380
+ content: ""; position: absolute; inset: 0; pointer-events: none;
381
+ background: repeating-linear-gradient(-45deg, rgba(${EDITOR_ACCENT}, 0.28) 0 1px, transparent 1px 8px);
382
+ outline: 1px dashed rgba(${EDITOR_ACCENT}, 0.6); outline-offset: -1px;
383
+ }
384
+ [data-plenary-empty] > *::after {
385
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
386
+ padding: 1px 8px; border: 1px solid rgba(${EDITOR_ACCENT}, 0.4); border-radius: 9999px;
387
+ background: #fff; color: rgb(${EDITOR_ACCENT});
388
+ font: 600 10px/14px ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap;
389
+ pointer-events: none;
390
+ }
391
+ [data-plenary-empty="box"] > *::after { content: "empty box"; }
392
+ [data-plenary-empty="section"] > *::after { content: "empty section"; }
393
+ `;
357
394
  function DropIndicator({ hint }) {
358
395
  if (!hint.rect) return null;
359
396
  const { rect, mode } = hint;
@@ -386,13 +423,22 @@ const OUTLINE_STYLE = {
386
423
  selected: { outline: "2px solid #4f46e5" },
387
424
  hover: { outline: "1px dashed #818cf8" }
388
425
  };
426
+ function nodeBox(nodeId) {
427
+ const marker = document.querySelector(`[data-plenary-id="${CSS.escape(nodeId)}"]`);
428
+ return marker?.firstElementChild ?? marker;
429
+ }
430
+ function measureNode(nodeId) {
431
+ const el = nodeId ? nodeBox(nodeId) : null;
432
+ if (!el || el.getClientRects().length === 0) return null;
433
+ const r = el.getBoundingClientRect();
434
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
435
+ }
389
436
  function useNodeRect(nodeId) {
390
437
  const [rect, setRect] = useState(null);
391
438
  useEffect(() => {
392
439
  let frame = 0;
393
440
  const measure = () => {
394
- const marker = document.querySelector(`[data-plenary-id="${CSS.escape(nodeId)}"]`);
395
- const target = marker?.firstElementChild ?? marker;
441
+ const target = nodeBox(nodeId);
396
442
  const next = target ? target.getBoundingClientRect() : null;
397
443
  setRect((prev) => sameBox(prev, next) ? prev : next);
398
444
  frame = requestAnimationFrame(measure);
@@ -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 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\";\nimport { isRichContent } from \"./components/Text.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 plain `content` — its DOM\n // is predictably marker > element > text node. Rich (HTML) content is edited\n // in the Properties panel: the plaintext session here would flatten it.\n const node = findNodeById(regionDoc.root, id);\n if (node?.component !== \"text\" || isRichContent(String(node.props.content ?? \"\"))) 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":[]}
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 InitPayload,\n type NodeRect,\n type RectsPayload,\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\";\nimport { isRichContent } from \"./components/Text.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 // The console draws those outlines itself (`init.overlay`): report where the\n // nodes are instead of drawing them here.\n const [overlay, setOverlay] = useState(false);\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 setOverlay((msg.payload as InitPayload | undefined)?.overlay === 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 // For a console drawing its own selection chrome, report where the highlighted\n // nodes are. Measured every frame for the same reason the in-frame outline is\n // (scrolls, reflows, late-loading images), but sent only when a box changes.\n useEffect(() => {\n if (!overlay) return;\n let frame = 0;\n let last = \"\";\n const measure = () => {\n const payload: RectsPayload = { selected: measureNode(selectedId), hover: measureNode(hoverId) };\n const key = JSON.stringify(payload);\n if (key !== last) {\n last = key;\n send(\"rects\", payload);\n }\n frame = requestAnimationFrame(measure);\n };\n measure();\n return () => cancelAnimationFrame(frame);\n }, [overlay, selectedId, hoverId, send]);\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 plain `content` — its DOM\n // is predictably marker > element > text node. Rich (HTML) content is edited\n // in the Properties panel: the plaintext session here would flatten it.\n const node = findNodeById(regionDoc.root, id);\n if (node?.component !== \"text\" || isRichContent(String(node.props.content ?? \"\"))) 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 <style data-plenary-editor=\"\" dangerouslySetInnerHTML={{ __html: EDITOR_CSS }} />\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 && !overlay ? <HighlightOverlay nodeId={selectedId} variant=\"selected\" /> : null}\n {hoverId && hoverId !== selectedId && !overlay ? (\n <HighlightOverlay nodeId={hoverId} variant=\"hover\" />\n ) : null}\n {dropHint ? <DropIndicator hint={dropHint} /> : null}\n </div>\n );\n}\n\n/**\n * Editor-only styling for empty built-in layout blocks (the renderer marks them\n * `data-plenary-empty`): a diagonal hatch, a dashed bound and an \"empty box\"\n * pill, so whitespace reads as a block. They are also held open to an 18px\n * minimum so a block with no content and no padding can still be clicked —\n * the one place the preview's layout departs from the published page.\n *\n * Unimportant on purpose: the compiled editor styles are `!important`, so an\n * author's own min-height or position still wins. The hatch is drawn on a\n * pseudo-element, not the background, so an author's background never hides it.\n */\nconst EDITOR_ACCENT = \"9, 142, 246\"; // the console's primary\nconst EDITOR_CSS = `\n[data-plenary-empty] > * { position: relative; min-height: 18px; }\n[data-plenary-empty] > *::before {\n content: \"\"; position: absolute; inset: 0; pointer-events: none;\n background: repeating-linear-gradient(-45deg, rgba(${EDITOR_ACCENT}, 0.28) 0 1px, transparent 1px 8px);\n outline: 1px dashed rgba(${EDITOR_ACCENT}, 0.6); outline-offset: -1px;\n}\n[data-plenary-empty] > *::after {\n position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);\n padding: 1px 8px; border: 1px solid rgba(${EDITOR_ACCENT}, 0.4); border-radius: 9999px;\n background: #fff; color: rgb(${EDITOR_ACCENT});\n font: 600 10px/14px ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap;\n pointer-events: none;\n}\n[data-plenary-empty=\"box\"] > *::after { content: \"empty box\"; }\n[data-plenary-empty=\"section\"] > *::after { content: \"empty section\"; }\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 nodeBox(nodeId: string): HTMLElement | null {\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 return (marker?.firstElementChild as HTMLElement | null) ?? (marker as HTMLElement | null);\n}\n\n/** A node's box for the `rects` report; null when it is absent or renders no box (display: none). */\nfunction measureNode(nodeId: string | null): NodeRect | null {\n const el = nodeId ? nodeBox(nodeId) : null;\n if (!el || el.getClientRects().length === 0) return null;\n const r = el.getBoundingClientRect();\n return { x: r.left, y: r.top, width: r.width, height: r.height };\n}\n\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 target = nodeBox(nodeId);\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":";AA6hBsB,wBAGlB,YAHkB;AA3hBtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAmBK;AACP,SAAS,0BAA0B;AACnC,SAAS,aAAa,gBAAgB;AACtC,SAAS,qBAAqB;AA4B9B,SAAS,SACP,KACA,QACA,YACiB;AACjB,QAAM,QAAQ,CAAC,UACb,MAAM,IAAI,CAAC,SAAS;AAClB,QAAI,OAAO;AACX,QAAI,KAAK,OAAO,OAAQ,QAAO,EAAE,GAAG,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,GAAG,WAAW,EAAE;AAClF,QAAI,KAAK,OAAO;AACd,YAAM,QAAuC,CAAC;AAC9C,iBAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,KAAK,KAAK,EAAG,OAAM,IAAI,IAAI,MAAM,QAAQ;AACvF,aAAO,EAAE,GAAG,MAAM,MAAM;AAAA,IAC1B;AACA,WAAO;AAAA,EACT,CAAC;AACH,SAAO,EAAE,GAAG,KAAK,MAAM,MAAM,IAAI,IAAI,EAAE;AACzC;AAEA,SAAS,gBAAgB,GAAmC;AAC1D,QAAM,KAAM,EAAE,QAA+B,UAAU,mBAAmB;AAC1E,SAAO,IAAI,aAAa,iBAAiB,KAAK;AAChD;AAEA,SAAS,aAAa,OAAsB,IAAgC;AAC1E,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,OAAO,GAAI,QAAO;AAC3B,QAAI,KAAK,OAAO;AACd,iBAAW,YAAY,OAAO,OAAO,KAAK,KAAK,GAAG;AAChD,cAAM,MAAM,aAAa,UAAU,EAAE;AACrC,YAAI,IAAK,QAAO;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOA,IAAI,eAA8B;AAClC,SAAS,kBAA0B;AACjC,MAAI,aAAc,QAAO;AACzB,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,MAAI;AACF,UAAM,kBAAkB;AACxB,mBAAe;AAAA,EACjB,QAAQ;AACN,mBAAe;AAAA,EACjB;AACA,SAAO;AACT;AAuBA,SAAS,kBAAkB,QAAiB;AAC1C,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,OAAO,aAAa,YAAa;AAChD,UAAM,UAAU,CAAC,MAAkB;AACjC,UAAK,EAAE,QAA2B,UAAU,SAAS,EAAG,GAAE,eAAe;AAAA,IAC3E;AACA,UAAM,WAAW,CAAC,MAAa,EAAE,eAAe;AAChD,aAAS,iBAAiB,SAAS,SAAS,IAAI;AAChD,aAAS,iBAAiB,UAAU,UAAU,IAAI;AAClD,WAAO,MAAM;AACX,eAAS,oBAAoB,SAAS,SAAS,IAAI;AACnD,eAAS,oBAAoB,UAAU,UAAU,IAAI;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AACb;AAeA,SAAS,YAAY,GAAW,GAAqB;AACnD,QAAM,OAAO,SAAS,cAAc,wBAAwB;AAC5D,QAAM,KAAK,SAAS,iBAAiB,GAAG,CAAC;AACzC,MAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,SAAS,EAAE,GAAG;AACtC,WAAO,EAAE,QAAQ,EAAE,IAAI,OAAO,GAAG,MAAM,MAAM,sBAAsB,KAAK,MAAM,MAAM,QAAQ;AAAA,EAC9F;AACA,QAAM,SAAS,GAAG,QAAQ,mBAAmB;AAC7C,QAAM,SAAS,GAAG,QAAQ,uBAAuB;AAIjD,MAAI,WAAW,CAAC,UAAU,CAAC,OAAO,SAAS,MAAM,IAAI;AACnD,UAAM,YAAY,OAAO,QAAQ,mBAAmB,GAAG,aAAa,iBAAiB;AACrF,UAAM,OAAO,OAAO,aAAa,qBAAqB;AACtD,QAAI,aAAa,QAAQ,MAAM;AAC7B,aAAO,EAAE,QAAQ,EAAE,IAAI,QAAQ,QAAQ,WAAW,KAAK,GAAG,MAAM,OAAO,sBAAsB,GAAG,MAAM,OAAO;AAAA,IAC/G;AAAA,EACF;AAEA,MAAI,QAAQ;AACV,UAAM,KAAK,OAAO,aAAa,iBAAiB;AAEhD,UAAM,MAAO,OAAO,qBAA4C;AAChE,UAAM,OAAO,IAAI,sBAAsB;AACvC,UAAM,QAAQ,IAAI,KAAK,MAAM,KAAK,SAAS;AAC3C,WAAO,EAAE,QAAQ,EAAE,IAAI,QAAQ,UAAU,UAAU,QAAQ,GAAG,GAAG,MAAM,MAAM,QAAQ,UAAU,SAAS;AAAA,EAC1G;AAEA,SAAO,EAAE,QAAQ,EAAE,IAAI,OAAO,GAAG,MAAM,KAAK,sBAAsB,GAAG,MAAM,QAAQ;AACrF;AAWO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AACF,GAAuB;AACrB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAG1C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAwB,IAAI;AAChE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAG1D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,UAAU,WAAW,IAAI,SAA0B,IAAI;AAC9D,QAAM,gBAAgB,OAAsB,IAAI;AAGhD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAsC,oBAAI,IAAI,CAAC;AACzF,QAAM,cAAc,OAA2B,IAAI;AAInD,oBAAkB,MAAM;AAGxB,QAAM,YAAY,QAAS,MAAM,KAAK,MAAM,IAAoC;AAEhF,QAAM,OAAO,YAAY,CAAC,MAAc,SAAkB,WAAoB;AAC5E,UAAM,SAAS,UAAU,cAAc;AACvC,QAAI,OAAO,WAAW,eAAe,CAAC,OAAQ;AAC9C,WAAO,OAAO,YAAY,EAAE,SAAS,MAAM,GAAG,kBAAkB,MAAM,QAAQ,GAAG,MAAM;AAAA,EACzF,GAAG,CAAC,CAAC;AAGL,QAAM,UAAU;AAAA,IACd,CAAC,WAAoB;AACnB,YAAM,UAAU,YAAY;AAC5B,UAAI,CAAC,QAAS;AACd,kBAAY,UAAU;AACtB,YAAM,EAAE,QAAQ,IAAI,UAAU,OAAO,IAAI;AACzC,SAAG,oBAAoB,QAAQ,MAAM;AACrC,SAAG,gBAAgB,iBAAiB;AACpC,YAAM,QAAQ,GAAG,aAAa,GAAG,eAAe,IAAI,SAAS;AAE7D,uBAAiB,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,IAAI,SAAS,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC,CAAC;AACjF,UAAI,CAAC,UAAU,SAAS,SAAU;AAGlC,eAAS,CAAC,SAAS;AACjB,YAAI,CAAC,KAAM,QAAO;AAClB,cAAM,MAAM,KAAK,KAAK,MAAM;AAC5B,YAAI,CAAC,IAAK,QAAO;AACjB,eAAO,EAAE,GAAG,MAAM,MAAM,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,GAAG,SAAS,KAAK,QAAQ,EAAE,SAAS,KAAK,CAAC,EAAE,EAAE;AAAA,MAC/F,CAAC;AACD,WAAK,QAAQ,EAAE,QAAQ,OAAO,EAAE,SAAS,KAAK,EAAE,CAAuB;AAAA,IACzE;AAAA,IACA,CAAC,QAAQ,IAAI;AAAA,EACf;AAEA,YAAU,MAAM;AACd,QAAI,OAAO,WAAW,eAAe,OAAO,WAAW,OAAQ;AAM/D,UAAM,aAAa,MAAM;AACvB,WAAK,QAAQ,EAAE,QAAQ,KAAK,KAAK,SAAS,gBAAgB,YAAY,EAAE,CAAuB;AAAA,IACjG;AACA,QAAI,YAAkD;AACtD,QAAI,WAAkC;AACtC,QAAI,OAAO,mBAAmB,aAAa;AACzC,iBAAW,IAAI,eAAe,MAAM;AAClC,YAAI,UAAW;AACf,oBAAY,WAAW,MAAM;AAC3B,sBAAY;AACZ,qBAAW;AAAA,QACb,GAAG,GAAG;AAAA,MACR,CAAC;AACD,eAAS,QAAQ,SAAS,IAAI;AAAA,IAChC;AAQA,UAAM,iBAAiB,CAAC,MAAkB;AACxC,UAAI,CAAC,cAAc,WAAW,EAAE,EAAE,WAAW,EAAE,SAAU;AACzD,QAAE,eAAe;AACjB,WAAK,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE,SAAS,GAAG,EAAE,QAAQ,CAAwB;AAAA,IACvF;AACA,QAAI,UAA2C;AAC/C,UAAM,aAAa,CAAC,MAAoB;AACtC,UAAI,CAAC,cAAc,WAAW,EAAE,WAAW,EAAG;AAC9C,gBAAU,EAAE,GAAG,EAAE,SAAS,GAAG,EAAE,QAAQ;AAEvC,UAAI;AACF,QAAC,EAAE,QAA2B,oBAAoB,EAAE,SAAS;AAAA,MAC/D,QAAQ;AAAA,MAER;AAAA,IACF;AACA,UAAM,YAAY,CAAC,MAAoB;AACrC,UAAI,CAAC,QAAS;AAEd,WAAK,EAAE,UAAU,OAAO,GAAG;AACzB,kBAAU;AACV;AAAA,MACF;AACA,WAAK,OAAO,EAAE,IAAI,EAAE,UAAU,QAAQ,GAAG,IAAI,EAAE,UAAU,QAAQ,EAAE,CAAsB;AACzF,gBAAU,EAAE,GAAG,EAAE,SAAS,GAAG,EAAE,QAAQ;AAAA,IACzC;AACA,UAAM,WAAW,CAAC,MAAoB;AACpC,UAAI,EAAE,WAAW,EAAG,WAAU;AAAA,IAChC;AAEA,UAAM,oBAAoB,CAAC,MAAkB;AAC3C,UAAI,cAAc,WAAW,EAAE,WAAW,EAAG,GAAE,eAAe;AAAA,IAChE;AAKA,UAAM,YAAY,CAAC,MAAqB;AAItC,YAAM,UAAU,YAAY;AAC5B,UAAI,WAAW,QAAQ,GAAG,SAAS,EAAE,MAAc,GAAG;AACpD,YAAI,EAAE,QAAQ,UAAU;AACtB,YAAE,eAAe;AACjB,kBAAQ,KAAK;AAAA,QACf,WAAW,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AAC3C,YAAE,eAAe;AACjB,kBAAQ,IAAI;AAAA,QACd;AACA;AAAA,MACF;AACA,UAAI,CAAC,cAAc,QAAS;AAC5B,YAAM,IAAI,EAAE;AACZ,UACE,MACC,EAAE,qBACD,aAAa,oBACb,aAAa,uBACb,aAAa;AAEf;AACF,YAAM,MAAM,EAAE,IAAI,YAAY;AAC9B,YAAM,MAAM,EAAE,WAAW,EAAE;AAE3B,UAAI,QAAQ,QAAQ,OAAO,QAAQ,QAAQ,SAAS,aAAa,GAAG,gBAAgB;AAClF;AACF,YAAM,UAAU,MACZ,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG,EAAE,SAAS,GAAG,IAChD,QAAQ,YAAY,QAAQ;AAChC,UAAI,CAAC,QAAS;AACd,QAAE,eAAe;AACjB,WAAK,OAAO;AAAA,QACV,KAAK,EAAE;AAAA,QACP,SAAS,EAAE;AAAA,QACX,SAAS,EAAE;AAAA,QACX,UAAU,EAAE;AAAA,MACd,CAAsB;AAAA,IACxB;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,aAAS,iBAAiB,SAAS,gBAAgB,EAAE,SAAS,MAAM,CAAC;AACrE,aAAS,iBAAiB,eAAe,UAAU;AACnD,aAAS,iBAAiB,eAAe,SAAS;AAClD,aAAS,iBAAiB,aAAa,QAAQ;AAC/C,aAAS,iBAAiB,aAAa,iBAAiB;AAExD,UAAM,YAAY,CAAC,MAAoB;AACrC,UAAI,CAAC,sBAAsB,eAAe,EAAE,MAAM,EAAG;AACrD,YAAM,MAAM,EAAE;AACd,UAAI,CAAC,OAAO,IAAI,YAAY,KAAM;AAElC,cAAQ,IAAI,MAAM;AAAA,QAChB,KAAK;AACH,wBAAc,UAAU,EAAE;AAC1B,oBAAU,IAAI;AACd,qBAAY,IAAI,SAAqC,YAAY,IAAI;AACrE;AAAA,YACE;AAAA,YACA,EAAE,YAAY,YAAY,GAAG,OAAO,SAAS,EAAE;AAAA,YAC/C,EAAE;AAAA,UACJ;AACA,qBAAW;AACX;AAAA,QACF,KAAK;AACH,wBAAc,UAAU,EAAE;AAC1B,oBAAU,IAAI;AAGd,kBAAQ,KAAK;AACb,mBAAS,IAAI,OAAwB;AACrC;AAAA,QACF,KAAK,SAAS;AACZ,kBAAQ,KAAK;AACb,gBAAM,IAAI,IAAI;AACd,mBAAS,CAAC,SAAS;AACjB,gBAAI,CAAC,KAAM,QAAO;AAClB,kBAAM,MAAM,KAAK,KAAK,MAAM;AAC5B,gBAAI,CAAC,IAAK,QAAO;AACjB,mBAAO,EAAE,GAAG,MAAM,MAAM,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,GAAG,SAAS,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;AAAA,UACvF,CAAC;AACD;AAAA,QACF;AAAA,QACA,KAAK,aAAa;AAChB,gBAAM,IAAI,IAAI;AACd,wBAAc,EAAE,UAAU;AAC1B,qBAAW,EAAE,OAAO;AACpB;AAAA,QACF;AAAA;AAAA;AAAA;AAAA,QAIA,KAAK,kBAAkB;AACrB,gBAAM,IAAI,IAAI;AACd,sBAAY,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;AACjC;AAAA,QACF;AAAA,QACA,KAAK;AACH,sBAAY,IAAI;AAChB;AAAA,QACF,KAAK,kBAAkB;AACrB,gBAAM,IAAI,IAAI;AACd,sBAAY,IAAI;AAChB,gBAAM,EAAE,OAAO,IAAI,YAAY,EAAE,GAAG,EAAE,CAAC;AACvC,eAAK,cAAc,EAAE,WAAW,EAAE,WAAW,OAAO,GAA+B,EAAE,MAAM;AAC3F;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,SAAS;AAM5C,UAAM,QAAQ,EAAE,iBAAiB,kBAAkB,WAAW;AAC9D,eAAW,UAAU,eAAe;AAClC,UAAI,CAAC,OAAO,SAAS,GAAG,EAAG,MAAK,SAAS,OAAO,MAAM;AAAA,IACxD;AACA,QAAI,mBAAmB,aAAa,EAAG,MAAK,SAAS,OAAO,GAAG;AAC/D,WAAO,MAAM;AACX,aAAO,oBAAoB,WAAW,SAAS;AAC/C,eAAS,oBAAoB,WAAW,SAAS;AACjD,eAAS,oBAAoB,SAAS,cAAc;AACpD,eAAS,oBAAoB,eAAe,UAAU;AACtD,eAAS,oBAAoB,eAAe,SAAS;AACrD,eAAS,oBAAoB,aAAa,QAAQ;AAClD,eAAS,oBAAoB,aAAa,iBAAiB;AAC3D,gBAAU,WAAW;AACrB,UAAI,UAAW,cAAa,SAAS;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,eAAe,YAAY,QAAQ,MAAM,OAAO,CAAC;AAKrD,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ;AACZ,QAAI,OAAO;AACX,UAAM,UAAU,MAAM;AACpB,YAAM,UAAwB,EAAE,UAAU,YAAY,UAAU,GAAG,OAAO,YAAY,OAAO,EAAE;AAC/F,YAAM,MAAM,KAAK,UAAU,OAAO;AAClC,UAAI,QAAQ,MAAM;AAChB,eAAO;AACP,aAAK,SAAS,OAAO;AAAA,MACvB;AACA,cAAQ,sBAAsB,OAAO;AAAA,IACvC;AACA,YAAQ;AACR,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACzC,GAAG,CAAC,SAAS,YAAY,SAAS,IAAI,CAAC;AAEvC,QAAM,YAAY,CAAC,QAAgB,OAAoB;AACrD,YAAQ,IAAI;AACZ,UAAM,SAAS,MAAM,QAAQ,IAAI;AACjC,OAAG,aAAa,mBAAmB,gBAAgB,CAAC;AACpD,OAAG,iBAAiB,QAAQ,MAAM;AAClC,gBAAY,UAAU,EAAE,QAAQ,IAAI,UAAU,GAAG,eAAe,IAAI,OAAO;AAC3E,OAAG,MAAM;AAAA,EACX;AAEA,QAAM,uBAAuB,CAAC,MAAuB;AACnD,UAAM,KAAK,gBAAgB,CAAC;AAC5B,QAAI,CAAC,MAAM,CAAC,UAAW;AAIvB,UAAM,OAAO,aAAa,UAAU,MAAM,EAAE;AAC5C,QAAI,MAAM,cAAc,UAAU,cAAc,OAAO,KAAK,MAAM,WAAW,EAAE,CAAC,EAAG;AACnF,UAAM,KAAM,EAAE,OAAuB,QAAQ,mBAAmB,GAC5D;AACJ,QAAI,GAAI,WAAU,IAAI,EAAE;AAAA,EAC1B;AAMA,QAAM,gBAAgB,CAAC,MAAsB,EAAE,aAAa,MAAM,SAAS,OAAO;AAElF,QAAM,aAAa,CAAC,MAAsB;AACxC,QAAI,CAAC,cAAc,CAAC,EAAG;AACvB,MAAE,eAAe;AACjB,MAAE,aAAa,aAAa;AAC5B,gBAAY,YAAY,EAAE,SAAS,EAAE,OAAO,CAAC;AAAA,EAC/C;AACA,QAAM,SAAS,CAAC,MAAsB;AACpC,QAAI,CAAC,cAAc,CAAC,EAAG;AACvB,MAAE,eAAe;AACjB,UAAM,YAAY,EAAE,aAAa,QAAQ,OAAO;AAChD,UAAM,EAAE,OAAO,IAAI,YAAY,EAAE,SAAS,EAAE,OAAO;AACnD,QAAI,UAAW,MAAK,cAAc,EAAE,WAAW,OAAO,CAA6B;AACnF,gBAAY,IAAI;AAAA,EAClB;AACA,QAAM,cAAc,CAAC,MAAsB;AAEzC,QAAI,CAAC,EAAE,cAAc,SAAS,EAAE,aAA4B,EAAG,aAAY,IAAI;AAAA,EACjF;AAEA,MAAI,CAAC,OAAQ,QAAO,gCAAG,UAAS;AAEhC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,wBAAqB;AAAA,MACrB,SAAS,CAAC,MAAM;AACd,cAAM,KAAK,gBAAgB,CAAC;AAC5B,YAAI,GAAI,MAAK,UAAU,EAAE,QAAQ,GAAG,CAAC;AAAA,MACvC;AAAA,MACA,eAAe;AAAA,MACf,aAAa,CAAC,MAAM,KAAK,SAAS,EAAE,QAAQ,gBAAgB,CAAC,EAAE,CAAC;AAAA,MAChE,YAAY,MAAM,KAAK,SAAS,EAAE,QAAQ,KAAK,CAAC;AAAA,MAChD;AAAA,MACA;AAAA,MACA;AAAA,MAEA;AAAA,4BAAC,WAAM,uBAAoB,IAAG,yBAAyB,EAAE,QAAQ,WAAW,GAAG;AAAA,QAC9E,YAAY,mBAAmB,WAAW,EAAE,UAAU,MAAM,aAAa,cAAc,CAAC,IAAI;AAAA,QAC5F,YACG,sBAAsB,SAAS,EAAE,IAAI,CAAC,OAAO,oBAAC,sBAA4B,QAAQ,MAAZ,EAAgB,CAAE,IACxF;AAAA,QACH,cAAc,CAAC,UAAU,oBAAC,oBAAiB,QAAQ,YAAY,SAAQ,YAAW,IAAK;AAAA,QACvF,WAAW,YAAY,cAAc,CAAC,UACrC,oBAAC,oBAAiB,QAAQ,SAAS,SAAQ,SAAQ,IACjD;AAAA,QACH,WAAW,oBAAC,iBAAc,MAAM,UAAU,IAAK;AAAA;AAAA;AAAA,EAClD;AAEJ;AAaA,MAAM,gBAAgB;AACtB,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA,uDAIoC,aAAa;AAAA,6BACvC,aAAa;AAAA;AAAA;AAAA;AAAA,6CAIG,aAAa;AAAA,iCACzB,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS9C,SAAS,cAAc,EAAE,KAAK,GAAuB;AACnD,MAAI,CAAC,KAAK,KAAM,QAAO;AACvB,QAAM,EAAE,MAAM,KAAK,IAAI;AACvB,QAAM,OAAsB,EAAE,UAAU,SAAS,eAAe,QAAQ,QAAQ,WAAW;AAC3F,MAAI,SAAS,QAAQ;AACnB,WACE;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,GAAG;AAAA,UACH,MAAM,KAAK;AAAA,UACX,KAAK,KAAK;AAAA,UACV,OAAO,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,SAAS;AAAA,UACT,eAAe;AAAA,UACf,YAAY;AAAA,QACd;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,QAAM,IAAI,SAAS,WAAW,KAAK,MAAM,KAAK;AAC9C,SACE,oBAAC,SAAI,OAAO,EAAE,GAAG,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,GAAG,OAAO,KAAK,OAAO,QAAQ,GAAG,YAAY,UAAU,GAAG;AAE/G;AAEA,SAAS,QAAQ,GAAmB,GAA4B;AAC9D,MAAI,CAAC,KAAK,CAAC,EAAG,QAAO,MAAM;AAC3B,SAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE;AACvF;AAQA,MAAM,gBAA6D;AAAA,EACjE,UAAU,EAAE,SAAS,oBAAoB;AAAA,EACzC,OAAO,EAAE,SAAS,qBAAqB;AACzC;AAOA,SAAS,QAAQ,QAAoC;AACnD,QAAM,SAAS,SAAS,cAAc,qBAAqB,IAAI,OAAO,MAAM,CAAC,IAAI;AAEjF,SAAQ,QAAQ,qBAA6C;AAC/D;AAGA,SAAS,YAAY,QAAwC;AAC3D,QAAM,KAAK,SAAS,QAAQ,MAAM,IAAI;AACtC,MAAI,CAAC,MAAM,GAAG,eAAe,EAAE,WAAW,EAAG,QAAO;AACpD,QAAM,IAAI,GAAG,sBAAsB;AACnC,SAAO,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,OAAO;AACjE;AAEA,SAAS,YAAY,QAAgC;AACnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAyB,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,QAAQ;AACZ,UAAM,UAAU,MAAM;AACpB,YAAM,SAAS,QAAQ,MAAM;AAC7B,YAAM,OAAO,SAAS,OAAO,sBAAsB,IAAI;AACvD,cAAQ,CAAC,SAAU,QAAQ,MAAM,IAAI,IAAI,OAAO,IAAK;AACrD,cAAQ,sBAAsB,OAAO;AAAA,IACvC;AACA,YAAQ;AACR,WAAO,MAAM,qBAAqB,KAAK;AAAA,EACzC,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AACT;AAEA,SAAS,iBAAiB,EAAE,QAAQ,QAAQ,GAAsD;AAChG,QAAM,OAAO,YAAY,MAAM;AAC/B,MAAI,CAAC,KAAM,QAAO;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,UAAU;AAAA,QACV,MAAM,KAAK;AAAA,QACX,KAAK,KAAK;AAAA,QACV,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA,QACb,GAAG,cAAc,OAAO;AAAA,QACxB,eAAe;AAAA,QACf,QAAQ;AAAA,MACV;AAAA;AAAA,EACF;AAEJ;AAYA,SAAS,sBAAsB,KAAgC;AAC7D,QAAM,MAAgB,CAAC;AACvB,QAAM,OAAO,CAAC,UAA+B;AAC3C,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,KAAK,OAAO;AAC1B,UAAI,KAAK,cAAc,WAAW,KAAK,OAAO,YAAY,SAAS,CAAC,MAAM,SAAS;AACjF,YAAI,KAAK,KAAK,EAAE;AAAA,MAClB;AACA,iBAAW,YAAY,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,EAAG,MAAK,QAAQ;AAAA,IACvE;AAAA,EACF;AACA,OAAK,IAAI,IAAI;AACb,SAAO;AACT;AAOA,SAAS,mBAAmB,EAAE,OAAO,GAAuB;AAC1D,QAAM,OAAO,YAAY,MAAM;AAE/B,MAAI,CAAC,QAAQ,KAAK,QAAQ,MAAM,KAAK,SAAS,GAAI,QAAO;AAEzD,QAAM,QAAQ,CAAC,MACb,oBAAC,SAAY,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,gBAAe,eAAY,QACtF,8BAAC,UAAK,GAAM,KADJ,CAEV;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,qCAAkC;AAAA,MAClC,OAAO;AAAA,QACL,UAAU;AAAA,QACV,MAAM,KAAK,OAAO;AAAA,QAClB,KAAK,KAAK,SAAS;AAAA,QACnB,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,KAAK;AAAA,QACL,SAAS;AAAA,QACT,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,OAAO;AAAA,QACP,eAAe;AAAA,QACf,QAAQ;AAAA;AAAA,MACV;AAAA,MAEC;AAAA,cAAM,eAAe;AAAA,QACrB,MAAM,yEAAyE;AAAA,QAC/E,MAAM,gFAAgF;AAAA;AAAA;AAAA,EACzF;AAEJ;","names":[]}
package/dist/render.js CHANGED
@@ -80,12 +80,13 @@ function nodeKey(id, opts) {
80
80
  const epoch = opts.remountKeys?.get(id);
81
81
  return epoch ? `${id}#${epoch}` : id;
82
82
  }
83
- function annotate(id, element, opts) {
83
+ function annotate(id, element, opts, empty) {
84
84
  if (!opts.annotate && !opts.styledIds.has(id)) return element;
85
85
  return /* @__PURE__ */ jsx(
86
86
  "div",
87
87
  {
88
88
  "data-plenary-id": id,
89
+ "data-plenary-empty": empty,
89
90
  style: { display: "contents" },
90
91
  children: element
91
92
  },
@@ -123,9 +124,14 @@ function renderNode(node, opts) {
123
124
  return annotate(
124
125
  node.id,
125
126
  createElement(Component, { ...props, key }, children),
126
- opts
127
+ opts,
128
+ opts.annotate && isEmptyContainer(node) ? node.component : void 0
127
129
  );
128
130
  }
131
+ function isEmptyContainer(node) {
132
+ if (node.component !== "box" && node.component !== "section") return false;
133
+ return !Object.values(node.slots ?? {}).some((children) => children.length > 0);
134
+ }
129
135
  function renderDocumentSync(document, opts = {}) {
130
136
  const { css, styledIds } = compileStyles(
131
137
  document,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/render.tsx"],"sourcesContent":["import { createElement, Fragment, Suspense, type ReactNode } from \"react\";\nimport {\n PlenaryNotFoundError,\n type PlenaryClient,\n type MediaDetail,\n} from \"@plenary/client\";\nimport {\n STYLE_PROP_FOR_BREAKPOINT,\n compileNodeCss,\n readNodeStyles,\n resolveCutoffs,\n type BreakpointCutoffs,\n type ComponentRegistration,\n type PlenaryDocument,\n type PlenaryNode,\n} from \"@plenary/core\";\nimport { registry } from \"./registry.js\";\nimport { Placeholder } from \"./components/Placeholder.js\";\nimport { toResolvedMedia, type MediaValue } from \"./media.js\";\nimport type { RenderContext } from \"./types.js\";\n\nconst MEDIA_PROP_TYPES = new Set([\"image\", \"file\"]);\n\n/** Prop keys of a registration whose values are media pointers. */\nfunction mediaPropKeys(reg: ComponentRegistration): string[] {\n return (reg.props ?? [])\n .filter((p) => MEDIA_PROP_TYPES.has(p.type))\n .map((p) => p.key);\n}\n\nfunction walkNodes(\n nodes: PlenaryNode[],\n visit: (n: PlenaryNode) => void,\n): void {\n for (const node of nodes) {\n visit(node);\n if (node.slots) {\n for (const children of Object.values(node.slots))\n walkNodes(children, visit);\n }\n }\n}\n\n/**\n * Batch-fetch the detail for every bare `media_id` referenced by a media-typed\n * prop, so nodes can then be rendered synchronously. Values already carrying a\n * URL, and all fetches, are skipped when there is no client.\n *\n * A 404 is reported separately from a failed fetch: it means the asset was\n * deleted while entries still point at it, and such a value must not fall back\n * to the URLs it carries (see `toResolvedMedia`).\n */\nasync function resolveMediaDetails(\n document: PlenaryDocument,\n client: PlenaryClient | undefined,\n): Promise<{ details: Map<string, MediaDetail>; missing: Set<string> }> {\n const details = new Map<string, MediaDetail>();\n const missing = new Set<string>();\n if (!client) return { details, missing };\n\n const ids = new Set<string>();\n walkNodes(document.root, (node) => {\n const reg = registry.get(node.component)?.registration;\n if (!reg) return;\n for (const key of mediaPropKeys(reg)) {\n const val = node.props[key] as MediaValue | undefined;\n if (val && val.media_id && !val.url) ids.add(val.media_id);\n }\n });\n\n await Promise.all(\n [...ids].map(async (id) => {\n try {\n details.set(id, await client.getMedia(id));\n } catch (err) {\n // Anything else degrades to an unresolved value (e.g. a network blip).\n if (err instanceof PlenaryNotFoundError) missing.add(id);\n }\n }),\n );\n return { details, missing };\n}\n\n/** Node props with media-typed values replaced by their resolved shape. */\nfunction effectiveProps(\n node: PlenaryNode,\n reg: ComponentRegistration,\n mediaDetails: Map<string, MediaDetail>,\n missingMedia: Set<string>,\n): Record<string, unknown> {\n const props: Record<string, unknown> = { ...node.props };\n // Editor styles render through the compiled per-node stylesheet, never as\n // props: an inline base style would beat the sheet's media overrides, and\n // components no longer need to do anything for styling to work.\n for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];\n for (const key of mediaPropKeys(reg)) {\n const val = props[key] as MediaValue | undefined;\n if (val) {\n props[key] = toResolvedMedia(\n val,\n val.media_id ? mediaDetails.get(val.media_id) : undefined,\n val.media_id ? missingMedia.has(val.media_id) : false,\n );\n }\n }\n return props;\n}\n\n/**\n * Compile every node's style layers into one stylesheet, and remember which\n * nodes contributed — those are the nodes the published render must wrap so\n * the sheet's `[data-plenary-id] > *` selectors have something to target.\n */\nfunction compileStyles(\n document: PlenaryDocument,\n cutoffs: BreakpointCutoffs,\n): { css: string; styledIds: Set<string> } {\n const parts: string[] = [];\n const styledIds = new Set<string>();\n walkNodes(document.root, (node) => {\n const css = compileNodeCss(node.id, readNodeStyles(node.props), cutoffs);\n if (css) {\n parts.push(css);\n styledIds.add(node.id);\n }\n });\n return { css: parts.join(\"\\n\"), styledIds };\n}\n\ninterface RenderOptions {\n mediaDetails: Map<string, MediaDetail>;\n /** Media ids whose asset was deleted — their values render as missing. */\n missingMedia: Set<string>;\n /** Wrap each node with a `data-plenary-id` marker (preview-bridge mode). */\n annotate?: boolean;\n /** Nodes with compiled styles — wrapped even outside annotate mode. */\n styledIds: Set<string>;\n /**\n * Per-node remount epochs (preview-bridge mode). A bumped epoch changes the\n * node's React key, forcing its subtree to be rebuilt from the document —\n * used after in-place editing, which mutates DOM React never tracked.\n */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/** A node's React key: its id, salted by its remount epoch when one exists. */\nfunction nodeKey(id: string, opts: RenderOptions): string {\n const epoch = opts.remountKeys?.get(id);\n return epoch ? `${id}#${epoch}` : id;\n}\n\n/**\n * Wrap a node's element in a layout-neutral marker carrying its id. Every node\n * gets one in annotate mode (the preview bridge maps clicked DOM elements back\n * to nodes through it); on published renders only styled nodes do, since the\n * marker is what the compiled stylesheet's selectors target.\n */\nfunction annotate(\n id: string,\n element: ReactNode,\n opts: RenderOptions,\n): ReactNode {\n if (!opts.annotate && !opts.styledIds.has(id)) return element;\n return (\n <div\n key={nodeKey(id, opts)}\n data-plenary-id={id}\n style={{ display: \"contents\" }}\n >\n {element}\n </div>\n );\n}\n\nfunction renderNode(node: PlenaryNode, opts: RenderOptions): ReactNode {\n const key = nodeKey(node.id, opts);\n const found = registry.get(node.component);\n if (!found) {\n return annotate(\n node.id,\n <Placeholder key={key} component={node.component} />,\n opts,\n );\n }\n\n const { component: Component, registration } = found;\n const props = effectiveProps(\n node,\n registration,\n opts.mediaDetails,\n opts.missingMedia,\n );\n // Annotated renders are editor previews; the flag lets components show\n // editor-only affordances (e.g. the Image empty-state placeholder) that a\n // published render must not.\n if (opts.annotate) props.editor = true;\n\n // The `children` slot becomes React children; every other slot is delivered\n // via a `slots` prop record of already-rendered nodes.\n let children: ReactNode;\n let slots: Record<string, ReactNode> | undefined;\n if (node.slots) {\n for (const [name, childNodes] of Object.entries(node.slots)) {\n const rendered = (\n <Fragment>\n {childNodes.map((child) => renderNode(child, opts))}\n </Fragment>\n );\n if (name === \"children\") children = rendered;\n else (slots ??= {})[name] = rendered;\n }\n }\n if (slots) props.slots = slots;\n\n return annotate(\n node.id,\n createElement(Component, { ...props, key }, children),\n opts,\n );\n}\n\nexport interface SyncRenderOptions {\n /** Pre-fetched media detail, keyed by media_id (empty resolves nothing). */\n mediaDetails?: Map<string, MediaDetail>;\n /** Media ids known to be deleted. The console marks preview values instead. */\n missingMedia?: Set<string>;\n /** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */\n /** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */\n annotate?: boolean;\n /** Per-node remount epochs — see RenderOptions.remountKeys. */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/**\n * Render a document synchronously from already-resolved inputs. Used by the\n * preview bridge (client-side, no async media pass). Media props that carry a\n * URL still render; bare `media_id`s without a supplied detail stay unresolved.\n *\n * Editor styles ride along as one compiled `<style data-plenary-styles>`\n * element ahead of the tree — real media queries against the registry theme's\n * cutoffs on every render path, preview and published alike (the artboard's\n * locked device widths make them evaluate correctly in the editor). The\n * compiler's sanitizer guarantees the CSS text cannot escape the style element,\n * which is what makes the innerHTML write safe.\n */\nexport function renderDocumentSync(\n document: PlenaryDocument,\n opts: SyncRenderOptions = {},\n): ReactNode {\n const { css, styledIds } = compileStyles(\n document,\n resolveCutoffs(registry.getTheme()),\n );\n const renderOpts: RenderOptions = {\n mediaDetails: opts.mediaDetails ?? new Map(),\n missingMedia: opts.missingMedia ?? new Set(),\n annotate: opts.annotate,\n styledIds,\n remountKeys: opts.remountKeys,\n };\n return (\n <Suspense>\n {css ? (\n <style\n data-plenary-styles=\"\"\n dangerouslySetInnerHTML={{ __html: css }}\n />\n ) : null}\n {document.root.map((node) => renderNode(node, renderOpts))}\n </Suspense>\n );\n}\n\n/**\n * Render a whole document to a React tree. Resolves media first (one batched\n * async pass), then walks the tree synchronously. Wrapped in Suspense so lazy\n * (`React.lazy` / `next/dynamic`) custom components are SSR-safe.\n */\nexport async function renderDocument(\n document: PlenaryDocument,\n ctx: RenderContext = {},\n): Promise<ReactNode> {\n const media = await resolveMediaDetails(document, ctx.client);\n return renderDocumentSync(document, {\n mediaDetails: media.details,\n missingMedia: media.missing,\n });\n}\n\nexport interface PlenaryPageProps {\n document: PlenaryDocument;\n /** Client used to resolve media props (optional). */\n client?: PlenaryClient;\n}\n\n/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */\nexport async function PlenaryPage({\n document,\n client,\n}: PlenaryPageProps): Promise<ReactNode> {\n return renderDocument(document, { client });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/render.tsx"],"sourcesContent":["import { createElement, Fragment, Suspense, type ReactNode } from \"react\";\nimport {\n PlenaryNotFoundError,\n type PlenaryClient,\n type MediaDetail,\n} from \"@plenary/client\";\nimport {\n STYLE_PROP_FOR_BREAKPOINT,\n compileNodeCss,\n readNodeStyles,\n resolveCutoffs,\n type BreakpointCutoffs,\n type ComponentRegistration,\n type PlenaryDocument,\n type PlenaryNode,\n} from \"@plenary/core\";\nimport { registry } from \"./registry.js\";\nimport { Placeholder } from \"./components/Placeholder.js\";\nimport { toResolvedMedia, type MediaValue } from \"./media.js\";\nimport type { RenderContext } from \"./types.js\";\n\nconst MEDIA_PROP_TYPES = new Set([\"image\", \"file\"]);\n\n/** Prop keys of a registration whose values are media pointers. */\nfunction mediaPropKeys(reg: ComponentRegistration): string[] {\n return (reg.props ?? [])\n .filter((p) => MEDIA_PROP_TYPES.has(p.type))\n .map((p) => p.key);\n}\n\nfunction walkNodes(\n nodes: PlenaryNode[],\n visit: (n: PlenaryNode) => void,\n): void {\n for (const node of nodes) {\n visit(node);\n if (node.slots) {\n for (const children of Object.values(node.slots))\n walkNodes(children, visit);\n }\n }\n}\n\n/**\n * Batch-fetch the detail for every bare `media_id` referenced by a media-typed\n * prop, so nodes can then be rendered synchronously. Values already carrying a\n * URL, and all fetches, are skipped when there is no client.\n *\n * A 404 is reported separately from a failed fetch: it means the asset was\n * deleted while entries still point at it, and such a value must not fall back\n * to the URLs it carries (see `toResolvedMedia`).\n */\nasync function resolveMediaDetails(\n document: PlenaryDocument,\n client: PlenaryClient | undefined,\n): Promise<{ details: Map<string, MediaDetail>; missing: Set<string> }> {\n const details = new Map<string, MediaDetail>();\n const missing = new Set<string>();\n if (!client) return { details, missing };\n\n const ids = new Set<string>();\n walkNodes(document.root, (node) => {\n const reg = registry.get(node.component)?.registration;\n if (!reg) return;\n for (const key of mediaPropKeys(reg)) {\n const val = node.props[key] as MediaValue | undefined;\n if (val && val.media_id && !val.url) ids.add(val.media_id);\n }\n });\n\n await Promise.all(\n [...ids].map(async (id) => {\n try {\n details.set(id, await client.getMedia(id));\n } catch (err) {\n // Anything else degrades to an unresolved value (e.g. a network blip).\n if (err instanceof PlenaryNotFoundError) missing.add(id);\n }\n }),\n );\n return { details, missing };\n}\n\n/** Node props with media-typed values replaced by their resolved shape. */\nfunction effectiveProps(\n node: PlenaryNode,\n reg: ComponentRegistration,\n mediaDetails: Map<string, MediaDetail>,\n missingMedia: Set<string>,\n): Record<string, unknown> {\n const props: Record<string, unknown> = { ...node.props };\n // Editor styles render through the compiled per-node stylesheet, never as\n // props: an inline base style would beat the sheet's media overrides, and\n // components no longer need to do anything for styling to work.\n for (const key of Object.values(STYLE_PROP_FOR_BREAKPOINT)) delete props[key];\n for (const key of mediaPropKeys(reg)) {\n const val = props[key] as MediaValue | undefined;\n if (val) {\n props[key] = toResolvedMedia(\n val,\n val.media_id ? mediaDetails.get(val.media_id) : undefined,\n val.media_id ? missingMedia.has(val.media_id) : false,\n );\n }\n }\n return props;\n}\n\n/**\n * Compile every node's style layers into one stylesheet, and remember which\n * nodes contributed — those are the nodes the published render must wrap so\n * the sheet's `[data-plenary-id] > *` selectors have something to target.\n */\nfunction compileStyles(\n document: PlenaryDocument,\n cutoffs: BreakpointCutoffs,\n): { css: string; styledIds: Set<string> } {\n const parts: string[] = [];\n const styledIds = new Set<string>();\n walkNodes(document.root, (node) => {\n const css = compileNodeCss(node.id, readNodeStyles(node.props), cutoffs);\n if (css) {\n parts.push(css);\n styledIds.add(node.id);\n }\n });\n return { css: parts.join(\"\\n\"), styledIds };\n}\n\ninterface RenderOptions {\n mediaDetails: Map<string, MediaDetail>;\n /** Media ids whose asset was deleted — their values render as missing. */\n missingMedia: Set<string>;\n /** Wrap each node with a `data-plenary-id` marker (preview-bridge mode). */\n annotate?: boolean;\n /** Nodes with compiled styles — wrapped even outside annotate mode. */\n styledIds: Set<string>;\n /**\n * Per-node remount epochs (preview-bridge mode). A bumped epoch changes the\n * node's React key, forcing its subtree to be rebuilt from the document —\n * used after in-place editing, which mutates DOM React never tracked.\n */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/** A node's React key: its id, salted by its remount epoch when one exists. */\nfunction nodeKey(id: string, opts: RenderOptions): string {\n const epoch = opts.remountKeys?.get(id);\n return epoch ? `${id}#${epoch}` : id;\n}\n\n/**\n * Wrap a node's element in a layout-neutral marker carrying its id. Every node\n * gets one in annotate mode (the preview bridge maps clicked DOM elements back\n * to nodes through it); on published renders only styled nodes do, since the\n * marker is what the compiled stylesheet's selectors target.\n */\nfunction annotate(\n id: string,\n element: ReactNode,\n opts: RenderOptions,\n empty?: string,\n): ReactNode {\n if (!opts.annotate && !opts.styledIds.has(id)) return element;\n return (\n <div\n key={nodeKey(id, opts)}\n data-plenary-id={id}\n data-plenary-empty={empty}\n style={{ display: \"contents\" }}\n >\n {element}\n </div>\n );\n}\n\nfunction renderNode(node: PlenaryNode, opts: RenderOptions): ReactNode {\n const key = nodeKey(node.id, opts);\n const found = registry.get(node.component);\n if (!found) {\n return annotate(\n node.id,\n <Placeholder key={key} component={node.component} />,\n opts,\n );\n }\n\n const { component: Component, registration } = found;\n const props = effectiveProps(\n node,\n registration,\n opts.mediaDetails,\n opts.missingMedia,\n );\n // Annotated renders are editor previews; the flag lets components show\n // editor-only affordances (e.g. the Image empty-state placeholder) that a\n // published render must not.\n if (opts.annotate) props.editor = true;\n\n // The `children` slot becomes React children; every other slot is delivered\n // via a `slots` prop record of already-rendered nodes.\n let children: ReactNode;\n let slots: Record<string, ReactNode> | undefined;\n if (node.slots) {\n for (const [name, childNodes] of Object.entries(node.slots)) {\n const rendered = (\n <Fragment>\n {childNodes.map((child) => renderNode(child, opts))}\n </Fragment>\n );\n if (name === \"children\") children = rendered;\n else (slots ??= {})[name] = rendered;\n }\n }\n if (slots) props.slots = slots;\n\n return annotate(\n node.id,\n createElement(Component, { ...props, key }, children),\n opts,\n opts.annotate && isEmptyContainer(node) ? node.component : undefined,\n );\n}\n\n/**\n * A built-in layout block with nothing in it. Editor previews mark these\n * (`data-plenary-empty`) so the bridge can hatch them and hold them open to a\n * clickable size; custom components are left alone.\n */\nfunction isEmptyContainer(node: PlenaryNode): boolean {\n if (node.component !== \"box\" && node.component !== \"section\") return false;\n return !Object.values(node.slots ?? {}).some((children) => children.length > 0);\n}\n\nexport interface SyncRenderOptions {\n /** Pre-fetched media detail, keyed by media_id (empty resolves nothing). */\n mediaDetails?: Map<string, MediaDetail>;\n /** Media ids known to be deleted. The console marks preview values instead. */\n missingMedia?: Set<string>;\n /** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */\n /** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */\n annotate?: boolean;\n /** Per-node remount epochs — see RenderOptions.remountKeys. */\n remountKeys?: ReadonlyMap<string, number>;\n}\n\n/**\n * Render a document synchronously from already-resolved inputs. Used by the\n * preview bridge (client-side, no async media pass). Media props that carry a\n * URL still render; bare `media_id`s without a supplied detail stay unresolved.\n *\n * Editor styles ride along as one compiled `<style data-plenary-styles>`\n * element ahead of the tree — real media queries against the registry theme's\n * cutoffs on every render path, preview and published alike (the artboard's\n * locked device widths make them evaluate correctly in the editor). The\n * compiler's sanitizer guarantees the CSS text cannot escape the style element,\n * which is what makes the innerHTML write safe.\n */\nexport function renderDocumentSync(\n document: PlenaryDocument,\n opts: SyncRenderOptions = {},\n): ReactNode {\n const { css, styledIds } = compileStyles(\n document,\n resolveCutoffs(registry.getTheme()),\n );\n const renderOpts: RenderOptions = {\n mediaDetails: opts.mediaDetails ?? new Map(),\n missingMedia: opts.missingMedia ?? new Set(),\n annotate: opts.annotate,\n styledIds,\n remountKeys: opts.remountKeys,\n };\n return (\n <Suspense>\n {css ? (\n <style\n data-plenary-styles=\"\"\n dangerouslySetInnerHTML={{ __html: css }}\n />\n ) : null}\n {document.root.map((node) => renderNode(node, renderOpts))}\n </Suspense>\n );\n}\n\n/**\n * Render a whole document to a React tree. Resolves media first (one batched\n * async pass), then walks the tree synchronously. Wrapped in Suspense so lazy\n * (`React.lazy` / `next/dynamic`) custom components are SSR-safe.\n */\nexport async function renderDocument(\n document: PlenaryDocument,\n ctx: RenderContext = {},\n): Promise<ReactNode> {\n const media = await resolveMediaDetails(document, ctx.client);\n return renderDocumentSync(document, {\n mediaDetails: media.details,\n missingMedia: media.missing,\n });\n}\n\nexport interface PlenaryPageProps {\n document: PlenaryDocument;\n /** Client used to resolve media props (optional). */\n client?: PlenaryClient;\n}\n\n/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */\nexport async function PlenaryPage({\n document,\n client,\n}: PlenaryPageProps): Promise<ReactNode> {\n return renderDocument(document, { client });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/react",
3
- "version": "0.5.1",
3
+ "version": "0.6.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.5.1",
24
- "@plenary/core": "0.5.1"
23
+ "@plenary/core": "0.6.0",
24
+ "@plenary/client": "0.6.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^18 || ^19",