@plenary/react 0.2.0 → 0.2.2
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 +3 -1
- package/dist/bridge.js.map +1 -1
- package/dist/components/Text.d.ts +14 -4
- package/dist/components/Text.js +10 -1
- package/dist/components/Text.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +4 -12
- package/dist/index.js.map +1 -1
- package/dist/registry.js +22 -48
- package/dist/registry.js.map +1 -1
- package/dist/render.d.ts +6 -8
- package/dist/render.js +40 -49
- package/dist/render.js.map +1 -1
- package/package.json +3 -3
- package/dist/components/Form.d.ts +0 -36
- package/dist/components/Form.js +0 -280
- package/dist/components/Form.js.map +0 -1
- package/dist/context.d.ts +0 -26
- package/dist/context.js +0 -20
- package/dist/context.js.map +0 -1
- package/dist/form.d.ts +0 -25
- package/dist/form.js +0 -11
- package/dist/form.js.map +0 -1
package/dist/bridge.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "@plenary/core";
|
|
15
15
|
import { renderDocumentSync } from "./render.js";
|
|
16
16
|
import { getManifest, getTheme } from "./registry.js";
|
|
17
|
+
import { isRichContent } from "./components/Text.js";
|
|
17
18
|
function patchDom(doc, nodeId, propsPatch) {
|
|
18
19
|
const patch = (nodes) => nodes.map((node) => {
|
|
19
20
|
let next = node;
|
|
@@ -305,7 +306,8 @@ function PlenaryBridge({
|
|
|
305
306
|
const onPreviewDoubleClick = (e) => {
|
|
306
307
|
const id = nodeIdFromEvent(e);
|
|
307
308
|
if (!id || !regionDoc) return;
|
|
308
|
-
|
|
309
|
+
const node = findNodeById(regionDoc.root, id);
|
|
310
|
+
if (node?.component !== "text" || isRichContent(String(node.props.content ?? ""))) return;
|
|
309
311
|
const el = e.target.closest("[data-plenary-id]")?.firstElementChild;
|
|
310
312
|
if (el) beginEdit(id, el);
|
|
311
313
|
};
|
package/dist/bridge.js.map
CHANGED
|
@@ -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\";\n\nexport interface PlenaryBridgeProps {\n /**\n * The `blocks` field on the entry this bridge previews — the key in the draft's\n * `data` whose component tree it renders. One `<PlenaryBridge>` drives one region.\n */\n region: string;\n /**\n * Console origins allowed to drive this preview; others are ignored. Each\n * entry is an exact origin (`https://cms.example.com`, `http://localhost:5173`)\n * or a pattern: `https://*.example.com` allows any subdomain (never the bare\n * apex), `http://localhost:*` allows any port — so a locally-running console\n * can drive a production site. See `isAllowedEditorOrigin` in `@plenary/core`.\n */\n editorOrigins: string[];\n /** Reported to the console in the `hello` handshake. */\n sdkVersion?: string;\n /** Normal page content, rendered until a draft render takes over. */\n children?: ReactNode;\n}\n\ninterface Draft {\n data: Record<string, unknown>;\n locale: string;\n}\n\n/** Return a new document with `propsPatch` merged into the node with `nodeId`. */\nfunction patchDom(\n doc: PlenaryDocument,\n nodeId: string,\n propsPatch: Record<string, unknown>,\n): PlenaryDocument {\n const patch = (nodes: PlenaryNode[]): PlenaryNode[] =>\n nodes.map((node) => {\n let next = node;\n if (node.id === nodeId) next = { ...next, props: { ...next.props, ...propsPatch } };\n if (next.slots) {\n const slots: Record<string, PlenaryNode[]> = {};\n for (const [name, children] of Object.entries(next.slots)) slots[name] = patch(children);\n next = { ...next, slots };\n }\n return next;\n });\n return { ...doc, root: patch(doc.root) };\n}\n\nfunction nodeIdFromEvent(e: ReactMouseEvent): string | null {\n const el = (e.target as HTMLElement | null)?.closest?.(\"[data-plenary-id]\");\n return el?.getAttribute(\"data-plenary-id\") ?? null;\n}\n\nfunction findNodeById(nodes: PlenaryNode[], id: string): PlenaryNode | null {\n for (const node of nodes) {\n if (node.id === id) return node;\n if (node.slots) {\n for (const children of Object.values(node.slots)) {\n const hit = findNodeById(children, id);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/**\n * The contenteditable mode for inline editing: `plaintext-only` where supported\n * (assigning an unsupported value to the property throws per spec), else `true`\n * — pasted markup is flattened on read either way.\n */\nlet editableMode: string | null = null;\nfunction getEditableMode(): string {\n if (editableMode) return editableMode;\n const probe = document.createElement(\"div\");\n try {\n probe.contentEditable = \"plaintext-only\";\n editableMode = \"plaintext-only\";\n } catch {\n editableMode = \"true\";\n }\n return editableMode;\n}\n\n/** A live inline-editing session on a text block's rendered element. */\ninterface EditSession {\n nodeId: string;\n el: HTMLElement;\n /** The committed content when editing began, to skip no-op edits. */\n original: string;\n onBlur: () => void;\n}\n\n/**\n * Keep the framed page from navigating away from the entry being edited. While\n * `locked`, link clicks and form submits inside the frame are cancelled — but\n * nothing else is: the listeners run in the capture phase and only call\n * `preventDefault()`, never `stopPropagation()`, so the bridge's own click/hover\n * selection (and any other event the SDK relies on) still fire normally.\n *\n * Toggleable by design: pass `false` and the lock lifts. It is tied to the\n * active preview today; when the console later lets the user browse their own\n * site and resolves the entry from the preview URL, that mode would drive this\n * flag (over the protocol) instead of blocking outright.\n */\nfunction useNavigationLock(locked: boolean) {\n useEffect(() => {\n if (!locked || typeof document === \"undefined\") return;\n const onClick = (e: MouseEvent) => {\n if ((e.target as Element | null)?.closest?.(\"a[href]\")) e.preventDefault();\n };\n const onSubmit = (e: Event) => e.preventDefault();\n document.addEventListener(\"click\", onClick, true);\n document.addEventListener(\"submit\", onSubmit, true);\n return () => {\n document.removeEventListener(\"click\", onClick, true);\n document.removeEventListener(\"submit\", onSubmit, true);\n };\n }, [locked]);\n}\n\ninterface DropHint {\n target: CanvasDropTarget;\n /** Viewport rect of the reference element, for drawing the indicator. */\n rect: DOMRect | null;\n mode: \"before\" | \"after\" | \"into\";\n}\n\n/**\n * Resolve a pointer position over the rendered preview to a tree drop target:\n * before/after the block under the cursor, into an empty Section column, or the\n * region root. Uses the `data-plenary-id` / `data-plenary-column` annotations the\n * renderer emits.\n */\nfunction resolveDrop(x: number, y: number): DropHint {\n const root = document.querySelector(\"[data-plenary-preview]\");\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n if (!root || !el || !root.contains(el)) {\n return { target: { at: \"root\" }, rect: root?.getBoundingClientRect() ?? null, mode: \"after\" };\n }\n const marker = el.closest(\"[data-plenary-id]\") as HTMLElement | null;\n const column = el.closest(\"[data-plenary-column]\") as HTMLElement | null;\n\n // Empty column space: the nearest block marker is the Section (an ancestor of\n // the column), not a child inside it → drop into that column's slot.\n if (column && (!marker || !column.contains(marker))) {\n const sectionId = column.closest(\"[data-plenary-id]\")?.getAttribute(\"data-plenary-id\");\n const slot = column.getAttribute(\"data-plenary-column\");\n if (sectionId && slot != null) {\n return { target: { at: \"slot\", nodeId: sectionId, slot }, rect: column.getBoundingClientRect(), mode: \"into\" };\n }\n }\n\n if (marker) {\n const id = marker.getAttribute(\"data-plenary-id\")!;\n // Markers use display:contents (no box); measure the rendered child.\n const box = (marker.firstElementChild as HTMLElement | null) ?? marker;\n const rect = box.getBoundingClientRect();\n const after = y > rect.top + rect.height / 2;\n return { target: { at: after ? \"after\" : \"before\", nodeId: id }, rect, mode: after ? \"after\" : \"before\" };\n }\n\n return { target: { at: \"root\" }, rect: root.getBoundingClientRect(), mode: \"after\" };\n}\n\n/**\n * The iframe side of live preview. Mounted by the site; inert until a valid\n * handshake from an allowed origin arrives. Once active it renders the draft the\n * console pushes over postMessage instead of the normal page, and reports\n * clicks/hovers back for block selection.\n *\n * Origin discipline is the security model: every inbound message must come from\n * an `editorOrigins` origin and carry `{ plenary: true }`.\n */\nexport function PlenaryBridge({\n region,\n editorOrigins,\n sdkVersion = \"0.0.0\",\n children,\n}: PlenaryBridgeProps) {\n const [draft, setDraft] = useState<Draft | null>(null);\n const [active, setActive] = useState(false);\n // Two outline channels the console drives independently: the selected block and\n // whatever is hovered (a tree row or a preview element). Drawn with distinct styles.\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [hoverId, setHoverId] = useState<string | null>(null);\n const [dropHint, setDropHint] = useState<DropHint | null>(null);\n const consoleOrigin = useRef<string | null>(null);\n // Nodes whose DOM must be rebuilt from the document on the next render (their\n // key changes), because an inline edit mutated DOM React never tracked.\n const [remountEpochs, setRemountEpochs] = useState<ReadonlyMap<string, number>>(new Map());\n const editSession = useRef<EditSession | null>(null);\n\n // While the console is driving the preview, don't let a stray link or form\n // submit navigate the frame off the entry being edited.\n useNavigationLock(active);\n\n // The component tree this bridge renders: the `region` blocks field of the draft.\n const regionDoc = draft ? (draft.data[region] as PlenaryDocument | undefined) : undefined;\n\n const send = useCallback((type: string, payload: unknown, origin?: string) => {\n const target = origin ?? consoleOrigin.current;\n if (typeof window === \"undefined\" || !target) return;\n window.parent.postMessage({ plenary: true, v: PROTOCOL_VERSION, type, payload }, target);\n }, []);\n\n /** End the inline-editing session: tear down, remount the node, send the change up. */\n const endEdit = useCallback(\n (commit: boolean) => {\n const session = editSession.current;\n if (!session) return;\n editSession.current = null;\n const { nodeId, el, original, onBlur } = session;\n el.removeEventListener(\"blur\", onBlur);\n el.removeAttribute(\"contenteditable\");\n const text = (el.innerText ?? el.textContent ?? \"\").toString();\n // Rebuild the node from the document: typing left DOM state React never saw.\n setRemountEpochs((prev) => new Map(prev).set(nodeId, (prev.get(nodeId) ?? 0) + 1));\n if (!commit || text === original) return;\n // Apply locally first so the remounted node renders the new text at once;\n // the console's render echo then arrives already matching.\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, nodeId, { content: text }) } };\n });\n send(\"edit\", { nodeId, props: { content: text } } satisfies EditPayload);\n },\n [region, send],\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\" || window.parent === window) return; // not framed\n\n // Report the page's content height so the console's artboard can size the\n // iframe to the whole page (no inner scrollbar). Sent on init and, trailing-\n // throttled, whenever the body's size changes; send() drops it until the\n // console origin is known.\n const reportSize = () => {\n send(\"size\", { height: Math.ceil(document.documentElement.scrollHeight) } satisfies SizePayload);\n };\n let sizeTimer: ReturnType<typeof setTimeout> | null = null;\n let observer: ResizeObserver | null = null;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(() => {\n if (sizeTimer) return;\n sizeTimer = setTimeout(() => {\n sizeTimer = null;\n reportSize();\n }, 150);\n });\n observer.observe(document.body);\n }\n\n // Artboard gestures performed over the page. The iframe consumes these\n // events, so the console never sees them — forward them once connected:\n // Ctrl/Cmd+wheel becomes a zoom request (page point included so the console\n // can anchor the zoom at the cursor), and a middle-button drag becomes pan\n // steps (the hand tool). Both are prevented locally: the page must neither\n // scroll under a zoom gesture nor start autoscroll under a pan.\n const onWheelGesture = (e: WheelEvent) => {\n if (!consoleOrigin.current || !(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n send(\"wheel\", { deltaY: e.deltaY, x: e.clientX, y: e.clientY } satisfies WheelPayload);\n };\n let panFrom: { x: number; y: number } | null = null;\n const onPanStart = (e: PointerEvent) => {\n if (!consoleOrigin.current || e.button !== 1) return;\n panFrom = { x: e.clientX, y: e.clientY };\n // Capture keeps the drag alive even when the pointer leaves the iframe.\n try {\n (e.target as Element | null)?.setPointerCapture?.(e.pointerId);\n } catch {\n /* capture is best-effort */\n }\n };\n const onPanMove = (e: PointerEvent) => {\n if (!panFrom) return;\n // The middle button was released outside our reach (e.g. over browser chrome).\n if ((e.buttons & 4) === 0) {\n panFrom = null;\n return;\n }\n send(\"pan\", { dx: e.clientX - panFrom.x, dy: e.clientY - panFrom.y } satisfies PanPayload);\n panFrom = { x: e.clientX, y: e.clientY };\n };\n const onPanEnd = (e: PointerEvent) => {\n if (e.button === 1) panFrom = null;\n };\n // Middle-button autoscroll is a `mousedown` default, not a pointerdown one.\n const onMiddleMouseDown = (e: MouseEvent) => {\n if (consoleOrigin.current && e.button === 1) e.preventDefault();\n };\n // Editor shortcuts pressed while the frame has focus (clicking a block\n // focuses the iframe, so the console's own keydown listener never fires).\n // Forward only recognized shortcut combinations, preventing their default\n // here — the frame must not run its own native undo or save dialog.\n const onKeyDown = (e: KeyboardEvent) => {\n // Inside an inline-edit session, Escape cancels and Enter commits\n // (Shift+Enter keeps the native line break); all other keys are native\n // editing — never forwarded (the editable-target check below also holds).\n const session = editSession.current;\n if (session && session.el.contains(e.target as Node)) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n endEdit(false);\n } else if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n endEdit(true);\n }\n return;\n }\n if (!consoleOrigin.current) return;\n const t = e.target as HTMLElement | null;\n if (\n t &&\n (t.isContentEditable ||\n t instanceof HTMLInputElement ||\n t instanceof HTMLTextAreaElement ||\n t instanceof HTMLSelectElement)\n )\n return;\n const key = e.key.toLowerCase();\n const mod = e.ctrlKey || e.metaKey;\n // A live text selection in the page keeps native copy/cut.\n if (mod && (key === \"c\" || key === \"x\") && document.getSelection()?.isCollapsed === false)\n return;\n const forward = mod\n ? [\"z\", \"y\", \"s\", \"d\", \"c\", \"x\", \"v\"].includes(key)\n : key === \"delete\" || key === \"backspace\";\n if (!forward) return;\n e.preventDefault();\n send(\"key\", {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n } satisfies KeyPayload);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n document.addEventListener(\"wheel\", onWheelGesture, { passive: false });\n document.addEventListener(\"pointerdown\", onPanStart);\n document.addEventListener(\"pointermove\", onPanMove);\n document.addEventListener(\"pointerup\", onPanEnd);\n document.addEventListener(\"mousedown\", onMiddleMouseDown);\n\n const onMessage = (e: MessageEvent) => {\n if (!isAllowedEditorOrigin(editorOrigins, e.origin)) return;\n const msg = e.data as { plenary?: boolean; type?: string; payload?: unknown } | null;\n if (!msg || msg.plenary !== true) return;\n\n switch (msg.type) {\n case \"init\":\n consoleOrigin.current = e.origin;\n setActive(true);\n send(\n \"manifest\",\n { components: getManifest(), theme: getTheme() } satisfies ManifestPayload,\n e.origin,\n );\n reportSize();\n break;\n case \"render\":\n consoleOrigin.current = e.origin;\n setActive(true);\n // A draft arriving mid-edit means the console changed the document\n // (e.g. toolbar undo); it is authoritative — cancel the session.\n endEdit(false);\n setDraft(msg.payload as RenderPayload);\n break;\n case \"patch\": {\n endEdit(false);\n const p = msg.payload as PatchPayload;\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, p.nodeId, p.props) } };\n });\n break;\n }\n case \"highlight\": {\n const p = msg.payload as HighlightPayload;\n setSelectedId(p.selectedId);\n setHoverId(p.hoverId);\n break;\n }\n // A palette drag the console is tracking on our behalf. It reports the\n // pointer in our own coordinate space; resolving it against the\n // rendered DOM is something only this side can do.\n case \"canvasDragOver\": {\n const p = msg.payload as CanvasDragPayload;\n setDropHint(resolveDrop(p.x, p.y));\n break;\n }\n case \"canvasDragLeave\":\n setDropHint(null);\n break;\n case \"canvasDragDrop\": {\n const p = msg.payload as CanvasDragDropPayload;\n setDropHint(null);\n const { target } = resolveDrop(p.x, p.y);\n send(\"canvasDrop\", { component: p.component, target } satisfies CanvasDropPayload, e.origin);\n break;\n }\n }\n };\n\n window.addEventListener(\"message\", onMessage);\n // Announce readiness to every allowed console origin. Exact origins are\n // addressed directly; a wildcard pattern cannot be a postMessage target, so\n // when any is configured the announce also goes out with target \"*\". That is\n // safe for `hello` alone — it carries only version numbers — and everything\n // after it is sent strictly to the matched origin of a validated `init`.\n const hello = { protocolVersion: PROTOCOL_VERSION, sdkVersion };\n for (const origin of editorOrigins) {\n if (!origin.includes(\"*\")) send(\"hello\", hello, origin);\n }\n if (hasWildcardPattern(editorOrigins)) send(\"hello\", hello, \"*\");\n return () => {\n window.removeEventListener(\"message\", onMessage);\n document.removeEventListener(\"keydown\", onKeyDown);\n document.removeEventListener(\"wheel\", onWheelGesture);\n document.removeEventListener(\"pointerdown\", onPanStart);\n document.removeEventListener(\"pointermove\", onPanMove);\n document.removeEventListener(\"pointerup\", onPanEnd);\n document.removeEventListener(\"mousedown\", onMiddleMouseDown);\n observer?.disconnect();\n if (sizeTimer) clearTimeout(sizeTimer);\n };\n }, [editorOrigins, sdkVersion, region, send, endEdit]);\n\n const beginEdit = (nodeId: string, el: HTMLElement) => {\n endEdit(true); // switching targets commits the previous session\n const onBlur = () => endEdit(true);\n el.setAttribute(\"contenteditable\", getEditableMode());\n el.addEventListener(\"blur\", onBlur);\n editSession.current = { nodeId, el, original: el.textContent ?? \"\", onBlur };\n el.focus();\n };\n\n const onPreviewDoubleClick = (e: ReactMouseEvent) => {\n const id = nodeIdFromEvent(e);\n if (!id || !regionDoc) return;\n // v1: inline editing is the built-in text block's `content` — its DOM is\n // predictably marker > element > text node.\n if (findNodeById(regionDoc.root, id)?.component !== \"text\") return;\n const el = (e.target as HTMLElement).closest(\"[data-plenary-id]\")\n ?.firstElementChild as HTMLElement | null;\n if (el) beginEdit(id, el);\n };\n\n // Native drag handling, kept for a preview served from the console's own\n // origin — the only case where the browser delivers drag events to the frame.\n // A cross-origin preview (every real deployment) gets nothing here and is\n // driven by the console's `canvasDrag*` messages above instead.\n const isPaletteDrag = (e: ReactDragEvent) => e.dataTransfer.types.includes(DND_NEW);\n\n const onDragOver = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n setDropHint(resolveDrop(e.clientX, e.clientY));\n };\n const onDrop = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n const component = e.dataTransfer.getData(DND_NEW);\n const { target } = resolveDrop(e.clientX, e.clientY);\n if (component) send(\"canvasDrop\", { component, target } satisfies CanvasDropPayload);\n setDropHint(null);\n };\n const onDragLeave = (e: ReactDragEvent) => {\n // Clear only when the pointer leaves the preview root entirely.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDropHint(null);\n };\n\n if (!active) return <>{children}</>;\n\n return (\n <div\n data-plenary-preview=\"\"\n onClick={(e) => {\n const id = nodeIdFromEvent(e);\n if (id) send(\"select\", { nodeId: id });\n }}\n onDoubleClick={onPreviewDoubleClick}\n onMouseOver={(e) => send(\"hover\", { nodeId: nodeIdFromEvent(e) })}\n onMouseOut={() => send(\"hover\", { nodeId: null })}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onDragLeave={onDragLeave}\n >\n {regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null}\n {regionDoc\n ? videoControlsBadgeIds(regionDoc).map((id) => <VideoControlsBadge key={id} nodeId={id} />)\n : null}\n {selectedId ? <HighlightOverlay nodeId={selectedId} variant=\"selected\" /> : null}\n {hoverId && hoverId !== selectedId ? (\n <HighlightOverlay nodeId={hoverId} variant=\"hover\" />\n ) : null}\n {dropHint ? <DropIndicator hint={dropHint} /> : null}\n </div>\n );\n}\n\n/** The live insertion indicator drawn over the canvas while dragging a component. */\nfunction DropIndicator({ hint }: { hint: DropHint }) {\n if (!hint.rect) return null;\n const { rect, mode } = hint;\n const base: CSSProperties = { position: \"fixed\", pointerEvents: \"none\", zIndex: 2147483647 };\n if (mode === \"into\") {\n return (\n <div\n style={{\n ...base,\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n outline: \"2px dashed #4f46e5\",\n outlineOffset: \"-2px\",\n background: \"rgba(79,70,229,0.06)\",\n }}\n />\n );\n }\n const y = mode === \"before\" ? rect.top : rect.bottom;\n return (\n <div style={{ ...base, left: rect.left, top: y - 1, width: rect.width, height: 2, background: \"#4f46e5\" }} />\n );\n}\n\nfunction sameBox(a: DOMRect | null, b: DOMRect | null): boolean {\n if (!a || !b) return a === b;\n return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;\n}\n\n/**\n * A fixed outline over a highlighted node, driven from the console sidebar. The\n * `selected` variant marks the block being edited (thick, solid, saturated); the\n * `hover` variant marks whatever the cursor is over (thinner, dashed, lighter) so\n * the two read as clearly different states.\n */\nconst OUTLINE_STYLE: Record<\"selected\" | \"hover\", CSSProperties> = {\n selected: { outline: \"2px solid #4f46e5\" },\n hover: { outline: \"1px dashed #818cf8\" },\n};\n\n// An overlay is positioned in viewport coordinates, so it goes stale as soon\n// as the block moves — a scroll, a resize, an image finishing loading, or an\n// edit that reflows the page above it. Re-measuring every frame tracks all of\n// those without enumerating them; state changes only when the box actually\n// does, so a still page costs one measurement per frame and no re-render.\nfunction useNodeRect(nodeId: string): DOMRect | null {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n let frame = 0;\n const measure = () => {\n const marker = document.querySelector(`[data-plenary-id=\"${CSS.escape(nodeId)}\"]`);\n // Markers use display:contents (no box of their own), so measure the child.\n const target = (marker?.firstElementChild as HTMLElement | null) ?? (marker as HTMLElement | null);\n const next = target ? target.getBoundingClientRect() : null;\n setRect((prev) => (sameBox(prev, next) ? prev : next));\n frame = requestAnimationFrame(measure);\n };\n measure();\n return () => cancelAnimationFrame(frame);\n }, [nodeId]);\n\n return rect;\n}\n\nfunction HighlightOverlay({ nodeId, variant }: { nodeId: string; variant: \"selected\" | \"hover\" }) {\n const rect = useNodeRect(nodeId);\n if (!rect) return null;\n return (\n <div\n style={{\n position: \"fixed\",\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n ...OUTLINE_STYLE[variant],\n pointerEvents: \"none\",\n zIndex: 2147483647,\n }}\n />\n );\n}\n\n/**\n * Ids of video nodes whose published player will show native controls. The\n * editor render hides them (a controls-less video lets clicks bubble to\n * selection), so the canvas draws a cosmetic badge instead — the hint that\n * the real page will have a control bar. Fidelity note: the renderer does not\n * merge registration defaults into props, so only an explicit truthy\n * `controls` counts, exactly like the published render. Media-less nodes show\n * the placeholder, which carries its own glyph — as do nodes whose asset was\n * deleted, where the published page renders no player to hint at either.\n */\nfunction videoControlsBadgeIds(doc: PlenaryDocument): string[] {\n const ids: string[] = [];\n const walk = (nodes: PlenaryNode[]): void => {\n for (const node of nodes) {\n const media = node.props?.media as { missing?: boolean } | undefined;\n if (node.component === \"video\" && node.props?.controls && media && !media.missing) {\n ids.push(node.id);\n }\n for (const children of Object.values(node.slots ?? {})) walk(children);\n }\n };\n walk(doc.root);\n return ids;\n}\n\n/**\n * A miniature control-bar chip drawn over a video block's bottom-left corner.\n * Purely cosmetic (pointer-events none) and drawn outside the annotated tree,\n * so node styles and selection behavior are untouched.\n */\nfunction VideoControlsBadge({ nodeId }: { nodeId: string }) {\n const rect = useNodeRect(nodeId);\n // On a tiny player the chip would cover the content it hints at.\n if (!rect || rect.width < 96 || rect.height < 48) return null;\n\n const glyph = (d: string) => (\n <svg key={d} width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d={d} />\n </svg>\n );\n return (\n <div\n data-plenary-video-controls-badge=\"\"\n style={{\n position: \"fixed\",\n left: rect.left + 8,\n top: rect.bottom - 26,\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"4px 9px\",\n borderRadius: 9999,\n background: \"rgba(0,0,0,0.65)\",\n color: \"#fff\",\n pointerEvents: \"none\",\n zIndex: 2147483646, // just under the highlight outlines\n }}\n >\n {glyph(\"M8 5v14l11-7z\") /* play */}\n {glyph(\"M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z\") /* volume */}\n {glyph(\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\") /* fullscreen */}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
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":";AAkgBsB,wBAGlB,YAHkB;AAhgBtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAgBK;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;AAC1D,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;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;AAErD,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,MAEC;AAAA,oBAAY,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,aAAa,oBAAC,oBAAiB,QAAQ,YAAY,SAAQ,YAAW,IAAK;AAAA,QAC3E,WAAW,YAAY,aACtB,oBAAC,oBAAiB,QAAQ,SAAS,SAAQ,SAAQ,IACjD;AAAA,QACH,WAAW,oBAAC,iBAAc,MAAM,UAAU,IAAK;AAAA;AAAA;AAAA,EAClD;AAEJ;AAGA,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,YAAY,QAAgC;AACnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAyB,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,QAAQ;AACZ,UAAM,UAAU,MAAM;AACpB,YAAM,SAAS,SAAS,cAAc,qBAAqB,IAAI,OAAO,MAAM,CAAC,IAAI;AAEjF,YAAM,SAAU,QAAQ,qBAA6C;AACrE,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":[]}
|
|
@@ -2,14 +2,24 @@ import * as react from 'react';
|
|
|
2
2
|
import { ElementType, CSSProperties } from 'react';
|
|
3
3
|
|
|
4
4
|
interface TextProps {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Plain text, or an HTML fragment as the console's rich-text editor writes it
|
|
7
|
+
* (`<p>…<strong>…</strong></p><ul>…</ul>`). Markup is detected by the
|
|
8
|
+
* presence of a tag.
|
|
9
|
+
*/
|
|
6
10
|
content?: string;
|
|
7
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Element/format for plain text: `p`, `h1`–`h6`, `span`, `code`, … Defaults
|
|
13
|
+
* to `p`. Rich content carries its own block elements, so it always renders
|
|
14
|
+
* inside a `div` (a heading or list can't legally sit inside a `<p>`).
|
|
15
|
+
*/
|
|
8
16
|
as?: ElementType;
|
|
9
17
|
className?: string;
|
|
10
18
|
style?: CSSProperties;
|
|
11
19
|
}
|
|
20
|
+
/** Whether a Text block's content is an HTML fragment rather than plain text. */
|
|
21
|
+
declare function isRichContent(content: string): boolean;
|
|
12
22
|
/** A text leaf rendered as the element named by `as`. */
|
|
13
|
-
declare function Text({ content, as, className, style }: TextProps): react.
|
|
23
|
+
declare function Text({ content, as, className, style }: TextProps): react.JSX.Element;
|
|
14
24
|
|
|
15
|
-
export { Text, type TextProps };
|
|
25
|
+
export { Text, type TextProps, isRichContent };
|
package/dist/components/Text.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
1
2
|
import { createElement } from "react";
|
|
3
|
+
function isRichContent(content) {
|
|
4
|
+
return /<[a-z][\s\S]*>/i.test(content);
|
|
5
|
+
}
|
|
2
6
|
function Text({ content = "", as = "p", className, style }) {
|
|
7
|
+
if (isRichContent(content)) {
|
|
8
|
+
const cls = className ? `plenary-text--rich ${className}` : "plenary-text--rich";
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { className: cls, style, dangerouslySetInnerHTML: { __html: content } });
|
|
10
|
+
}
|
|
3
11
|
return createElement(as, { className, style }, content);
|
|
4
12
|
}
|
|
5
13
|
export {
|
|
6
|
-
Text
|
|
14
|
+
Text,
|
|
15
|
+
isRichContent
|
|
7
16
|
};
|
|
8
17
|
//# sourceMappingURL=Text.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/Text.tsx"],"sourcesContent":["import { createElement, type CSSProperties, type ElementType } from \"react\";\n\nexport interface TextProps {\n
|
|
1
|
+
{"version":3,"sources":["../../src/components/Text.tsx"],"sourcesContent":["import { createElement, type CSSProperties, type ElementType } from \"react\";\n\nexport interface TextProps {\n /**\n * Plain text, or an HTML fragment as the console's rich-text editor writes it\n * (`<p>…<strong>…</strong></p><ul>…</ul>`). Markup is detected by the\n * presence of a tag.\n */\n content?: string;\n /**\n * Element/format for plain text: `p`, `h1`–`h6`, `span`, `code`, … Defaults\n * to `p`. Rich content carries its own block elements, so it always renders\n * inside a `div` (a heading or list can't legally sit inside a `<p>`).\n */\n as?: ElementType;\n className?: string;\n style?: CSSProperties;\n}\n\n/** Whether a Text block's content is an HTML fragment rather than plain text. */\nexport function isRichContent(content: string): boolean {\n return /<[a-z][\\s\\S]*>/i.test(content);\n}\n\n/** A text leaf rendered as the element named by `as`. */\nexport function Text({ content = \"\", as = \"p\", className, style }: TextProps) {\n if (isRichContent(content)) {\n const cls = className ? `plenary-text--rich ${className}` : \"plenary-text--rich\";\n return <div className={cls} style={style} dangerouslySetInnerHTML={{ __html: content }} />;\n }\n return createElement(as, { className, style }, content);\n}\n"],"mappings":"AA4BW;AA5BX,SAAS,qBAA2D;AAoB7D,SAAS,cAAc,SAA0B;AACtD,SAAO,kBAAkB,KAAK,OAAO;AACvC;AAGO,SAAS,KAAK,EAAE,UAAU,IAAI,KAAK,KAAK,WAAW,MAAM,GAAc;AAC5E,MAAI,cAAc,OAAO,GAAG;AAC1B,UAAM,MAAM,YAAY,sBAAsB,SAAS,KAAK;AAC5D,WAAO,oBAAC,SAAI,WAAW,KAAK,OAAc,yBAAyB,EAAE,QAAQ,QAAQ,GAAG;AAAA,EAC1F;AACA,SAAO,cAAc,IAAI,EAAE,WAAW,MAAM,GAAG,OAAO;AACxD;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,8 @@ export { Section, SectionProps } from './components/Section.js';
|
|
|
5
5
|
export { Text, TextProps } from './components/Text.js';
|
|
6
6
|
export { Image, ImageProps } from './components/Image.js';
|
|
7
7
|
export { Video, VideoProps } from './components/Video.js';
|
|
8
|
-
export { Form, FormProps } from './components/Form.js';
|
|
9
8
|
export { Placeholder, PlaceholderProps } from './components/Placeholder.js';
|
|
10
9
|
export { MediaValue, ResolvedMedia, resolveMedia, toResolvedMedia } from './media.js';
|
|
11
|
-
export { FormValue, ResolvedForm, toResolvedForm } from './form.js';
|
|
12
|
-
export { PlenaryProvider, PlenaryProviderProps, usePlenaryClient } from './context.js';
|
|
13
10
|
export { PlenaryComponent, RegisteredComponent, RenderContext, SlotProps } from './types.js';
|
|
14
11
|
import 'react';
|
|
15
12
|
import '@plenary/client';
|
package/dist/index.js
CHANGED
|
@@ -17,25 +17,19 @@ import { Section } from "./components/Section.js";
|
|
|
17
17
|
import { Text } from "./components/Text.js";
|
|
18
18
|
import { Image } from "./components/Image.js";
|
|
19
19
|
import { Video } from "./components/Video.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
20
|
+
import {
|
|
21
|
+
Placeholder
|
|
22
|
+
} from "./components/Placeholder.js";
|
|
22
23
|
import {
|
|
23
24
|
resolveMedia,
|
|
24
25
|
toResolvedMedia
|
|
25
26
|
} from "./media.js";
|
|
26
|
-
import { toResolvedForm } from "./form.js";
|
|
27
|
-
import {
|
|
28
|
-
PlenaryProvider,
|
|
29
|
-
usePlenaryClient
|
|
30
|
-
} from "./context.js";
|
|
31
27
|
export {
|
|
32
28
|
Box,
|
|
33
|
-
Form,
|
|
34
29
|
Image,
|
|
35
30
|
Placeholder,
|
|
36
31
|
Plenary,
|
|
37
32
|
PlenaryPage,
|
|
38
|
-
PlenaryProvider,
|
|
39
33
|
Registry,
|
|
40
34
|
Section,
|
|
41
35
|
Text,
|
|
@@ -48,8 +42,6 @@ export {
|
|
|
48
42
|
renderDocument,
|
|
49
43
|
renderDocumentSync,
|
|
50
44
|
resolveMedia,
|
|
51
|
-
|
|
52
|
-
toResolvedMedia,
|
|
53
|
-
usePlenaryClient
|
|
45
|
+
toResolvedMedia
|
|
54
46
|
};
|
|
55
47
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @plenary/react — the DOM renderer, component registry, and built-in\n * components (Box/Section/Text/Image/Video
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @plenary/react — the DOM renderer, component registry, and built-in\n * components (Box/Section/Text/Image/Video) for rendering a Page's `dom`\n * tree. Custom components register on top of the built-ins; the preview bridge\n * lives in the `./bridge` export.\n */\n\nexport {\n PlenaryPage,\n renderDocument,\n renderDocumentSync,\n type PlenaryPageProps,\n type SyncRenderOptions,\n} from \"./render.js\";\nexport {\n Registry,\n registry,\n register,\n registerTheme,\n Plenary,\n getManifest,\n getTheme,\n} from \"./registry.js\";\nexport { Box, type BoxProps } from \"./components/Box.js\";\nexport { Section, type SectionProps } from \"./components/Section.js\";\nexport { Text, type TextProps } from \"./components/Text.js\";\nexport { Image, type ImageProps } from \"./components/Image.js\";\nexport { Video, type VideoProps } from \"./components/Video.js\";\nexport {\n Placeholder,\n type PlaceholderProps,\n} from \"./components/Placeholder.js\";\nexport {\n resolveMedia,\n toResolvedMedia,\n type MediaValue,\n type ResolvedMedia,\n} from \"./media.js\";\nexport type {\n PlenaryComponent,\n RegisteredComponent,\n RenderContext,\n SlotProps,\n} from \"./types.js\";\n"],"mappings":"AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAA0B;AACnC,SAAS,eAAkC;AAC3C,SAAS,YAA4B;AACrC,SAAS,aAA8B;AACvC,SAAS,aAA8B;AACvC;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAGK;","names":[]}
|
package/dist/registry.js
CHANGED
|
@@ -3,7 +3,6 @@ import { Section } from "./components/Section.js";
|
|
|
3
3
|
import { Text } from "./components/Text.js";
|
|
4
4
|
import { Image } from "./components/Image.js";
|
|
5
5
|
import { Video } from "./components/Video.js";
|
|
6
|
-
import { Form } from "./components/Form.js";
|
|
7
6
|
const BUILTINS = [
|
|
8
7
|
{
|
|
9
8
|
component: Box,
|
|
@@ -39,7 +38,7 @@ const BUILTINS = [
|
|
|
39
38
|
label: "Text",
|
|
40
39
|
category: "Content",
|
|
41
40
|
props: [
|
|
42
|
-
{ key: "content", type: "
|
|
41
|
+
{ key: "content", type: "rich_text" },
|
|
43
42
|
{
|
|
44
43
|
key: "as",
|
|
45
44
|
type: "select",
|
|
@@ -97,13 +96,30 @@ const BUILTINS = [
|
|
|
97
96
|
// The source file is served as uploaded — no transcoding — so the
|
|
98
97
|
// picker pins to the library's video type. Alt/caption sub-fields are
|
|
99
98
|
// hidden like the Image block's: the player has no use for them.
|
|
100
|
-
{
|
|
101
|
-
|
|
99
|
+
{
|
|
100
|
+
key: "media",
|
|
101
|
+
type: "file",
|
|
102
|
+
ui: { label: "Video", media_type: "video", show_alt_caption: false }
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "poster",
|
|
106
|
+
type: "image",
|
|
107
|
+
ui: { label: "Poster Image", show_alt_caption: false }
|
|
108
|
+
},
|
|
102
109
|
{ key: "controls", type: "boolean", defaultValue: true },
|
|
103
|
-
{
|
|
110
|
+
{
|
|
111
|
+
key: "autoplay",
|
|
112
|
+
type: "boolean",
|
|
113
|
+
ui: { label: "Autoplay (browsers require Muted)" }
|
|
114
|
+
},
|
|
104
115
|
{ key: "loop", type: "boolean" },
|
|
105
116
|
{ key: "muted", type: "boolean" },
|
|
106
|
-
{
|
|
117
|
+
{
|
|
118
|
+
key: "playsinline",
|
|
119
|
+
type: "boolean",
|
|
120
|
+
defaultValue: true,
|
|
121
|
+
ui: { label: "Plays Inline" }
|
|
122
|
+
},
|
|
107
123
|
{
|
|
108
124
|
key: "preload",
|
|
109
125
|
type: "select",
|
|
@@ -119,48 +135,6 @@ const BUILTINS = [
|
|
|
119
135
|
],
|
|
120
136
|
styles: ["size", "css"]
|
|
121
137
|
}
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
component: Form,
|
|
125
|
-
registration: {
|
|
126
|
-
key: "form",
|
|
127
|
-
label: "Form",
|
|
128
|
-
category: "Content",
|
|
129
|
-
props: [
|
|
130
|
-
{ key: "form", type: "form", ui: { label: "Form" } },
|
|
131
|
-
{ key: "showTitle", type: "boolean", ui: { label: "Show Form Title" } },
|
|
132
|
-
{ key: "submitLabel", type: "text", defaultValue: "Submit", ui: { label: "Submit Button Label" } },
|
|
133
|
-
{
|
|
134
|
-
key: "onSuccess",
|
|
135
|
-
type: "select",
|
|
136
|
-
// Required: a non-required select offers an empty "None" choice,
|
|
137
|
-
// which is meaningless here — the block always does one of the two.
|
|
138
|
-
required: true,
|
|
139
|
-
defaultValue: "message",
|
|
140
|
-
ui: { label: "After Submit" },
|
|
141
|
-
settings: {
|
|
142
|
-
options: [
|
|
143
|
-
{ value: "message", label: "Show a success message" },
|
|
144
|
-
{ value: "redirect", label: "Redirect to a page" }
|
|
145
|
-
]
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
key: "successMessage",
|
|
150
|
-
type: "text",
|
|
151
|
-
defaultValue: "Thanks! Your submission has been received.",
|
|
152
|
-
ui: { label: "Success Message", control: "textarea" },
|
|
153
|
-
visibleWhen: { prop: "onSuccess", equals: "message" }
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
key: "redirectUrl",
|
|
157
|
-
type: "url",
|
|
158
|
-
ui: { label: "Redirect URL" },
|
|
159
|
-
visibleWhen: { prop: "onSuccess", equals: "redirect" }
|
|
160
|
-
}
|
|
161
|
-
],
|
|
162
|
-
styles: ["layout", "size", "typography", "css"]
|
|
163
|
-
}
|
|
164
138
|
}
|
|
165
139
|
];
|
|
166
140
|
class Registry {
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type {\n ComponentRegistration,\n ManifestComponent,\n ThemeRegistration,\n} from \"@plenary/core\";\nimport type { PlenaryComponent, RegisteredComponent } from \"./types.js\";\nimport { Box } from \"./components/Box.js\";\nimport { Section } from \"./components/Section.js\";\nimport { Text } from \"./components/Text.js\";\nimport { Image } from \"./components/Image.js\";\nimport { Video } from \"./components/Video.js\";\nimport { Form } from \"./components/Form.js\";\n\n/** The built-in primitives, pre-registered so they behave like any custom component. */\nconst BUILTINS: { component: PlenaryComponent; registration: ComponentRegistration }[] = [\n {\n component: Box,\n registration: {\n key: \"box\",\n label: \"Box\",\n category: \"Layout\",\n props: [{ key: \"className\", type: \"text\" }],\n slots: [{ name: \"children\", label: \"Content\" }],\n styles: [\"layout\", \"size\", \"background\", \"css\"],\n },\n },\n {\n component: Section,\n registration: {\n key: \"section\",\n label: \"Section\",\n category: \"Layout\",\n // Column slots (\"0\"…\"n-1\") are dynamic, driven by `columns`, and managed\n // by the built-in — so they are not declared statically here.\n props: [\n { key: \"columns\", type: \"number\", defaultValue: 1 },\n { key: \"gap\", type: \"number\", defaultValue: 16 },\n { key: \"className\", type: \"text\" },\n ],\n styles: [\"size\", \"background\", \"css\"],\n },\n },\n {\n component: Text,\n registration: {\n key: \"text\",\n label: \"Text\",\n category: \"Content\",\n props: [\n { key: \"content\", type: \"text\" },\n {\n key: \"as\",\n type: \"select\",\n defaultValue: \"p\",\n // Options use the server's select field-settings shape ({value,label})\n // so the console's select input can drive them directly.\n settings: {\n options: [\n { value: \"p\", label: \"Paragraph\" },\n { value: \"h1\", label: \"Heading 1\" },\n { value: \"h2\", label: \"Heading 2\" },\n { value: \"h3\", label: \"Heading 3\" },\n { value: \"h4\", label: \"Heading 4\" },\n { value: \"h5\", label: \"Heading 5\" },\n { value: \"h6\", label: \"Heading 6\" },\n { value: \"span\", label: \"Span\" },\n { value: \"code\", label: \"Code\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"typography\", \"css\"],\n },\n },\n {\n component: Image,\n registration: {\n key: \"image\",\n label: \"Image\",\n category: \"Content\",\n props: [\n // Alt is a block prop (below), so the media picker shows only the\n // asset itself — not its own alt/caption sub-fields.\n { key: \"media\", type: \"image\", ui: { show_alt_caption: false } },\n { key: \"alt\", type: \"text\", ui: { label: \"Alt Text\" } },\n // A one-off variant spec for this placement. The server enqueues the\n // matching variant on save and hydrates `media.variant_url` on read;\n // with no dimensions set, the original renders. Labels say \"output\"\n // (pixels served) to keep these apart from the Styles tab's layout Size.\n { key: \"width\", type: \"number\", ui: { label: \"Max Width (px)\" } },\n { key: \"height\", type: \"number\", ui: { label: \"Max Height (px)\" } },\n { key: \"quality\", type: \"number\", ui: { label: \"Quality (%)\" } },\n { key: \"crop\", type: \"boolean\", ui: { label: \"Crop to fill\" } },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n {\n component: Video,\n registration: {\n key: \"video\",\n label: \"Video\",\n category: \"Content\",\n props: [\n // The source file is served as uploaded — no transcoding — so the\n // picker pins to the library's video type. Alt/caption sub-fields are\n // hidden like the Image block's: the player has no use for them.\n { key: \"media\", type: \"file\", ui: { label: \"Video\", media_type: \"video\", show_alt_caption: false } },\n { key: \"poster\", type: \"image\", ui: { label: \"Poster Image\", show_alt_caption: false } },\n { key: \"controls\", type: \"boolean\", defaultValue: true },\n { key: \"autoplay\", type: \"boolean\", ui: { label: \"Autoplay (browsers require Muted)\" } },\n { key: \"loop\", type: \"boolean\" },\n { key: \"muted\", type: \"boolean\" },\n { key: \"playsinline\", type: \"boolean\", defaultValue: true, ui: { label: \"Plays Inline\" } },\n {\n key: \"preload\",\n type: \"select\",\n defaultValue: \"metadata\",\n settings: {\n options: [\n { value: \"none\", label: \"None\" },\n { value: \"metadata\", label: \"Metadata\" },\n { value: \"auto\", label: \"Auto\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n {\n component: Form,\n registration: {\n key: \"form\",\n label: \"Form\",\n category: \"Content\",\n props: [\n { key: \"form\", type: \"form\", ui: { label: \"Form\" } },\n { key: \"showTitle\", type: \"boolean\", ui: { label: \"Show Form Title\" } },\n { key: \"submitLabel\", type: \"text\", defaultValue: \"Submit\", ui: { label: \"Submit Button Label\" } },\n {\n key: \"onSuccess\",\n type: \"select\",\n // Required: a non-required select offers an empty \"None\" choice,\n // which is meaningless here — the block always does one of the two.\n required: true,\n defaultValue: \"message\",\n ui: { label: \"After Submit\" },\n settings: {\n options: [\n { value: \"message\", label: \"Show a success message\" },\n { value: \"redirect\", label: \"Redirect to a page\" },\n ],\n },\n },\n {\n key: \"successMessage\",\n type: \"text\",\n defaultValue: \"Thanks! Your submission has been received.\",\n ui: { label: \"Success Message\", control: \"textarea\" },\n visibleWhen: { prop: \"onSuccess\", equals: \"message\" },\n },\n {\n key: \"redirectUrl\",\n type: \"url\",\n ui: { label: \"Redirect URL\" },\n visibleWhen: { prop: \"onSuccess\", equals: \"redirect\" },\n },\n ],\n styles: [\"layout\", \"size\", \"typography\", \"css\"],\n },\n },\n];\n\n/**\n * The module-singleton component registry. Because it is a shared module, both\n * the render path (SSR + client) and the preview bridge see the same\n * registrations — the site's registry file is imported wherever either is used.\n */\nexport class Registry {\n private components = new Map<string, RegisteredComponent>();\n private theme: ThemeRegistration | undefined;\n\n constructor() {\n for (const b of BUILTINS) this.register(b.component, b.registration);\n }\n\n /** Register a component (built-in or custom) under its registration key. */\n register(component: PlenaryComponent, registration: ComponentRegistration): this {\n this.components.set(registration.key, { component, registration });\n return this;\n }\n\n /**\n * Declare the site's design tokens. Each token mirrors a CSS custom property\n * the site's stylesheet defines (`--<name>`); the editor stores references\n * (`var(--<name>)`), so the tokens must actually exist in the site's CSS for\n * styled pages to render.\n */\n registerTheme(theme: ThemeRegistration): this {\n this.theme = theme;\n return this;\n }\n\n getTheme(): ThemeRegistration | undefined {\n return this.theme;\n }\n\n get(key: string): RegisteredComponent | undefined {\n return this.components.get(key);\n }\n\n all(): RegisteredComponent[] {\n return [...this.components.values()];\n }\n\n /** The full component manifest announced to the console over the bridge. */\n manifest(): ManifestComponent[] {\n return this.all().map(({ registration: r }) => ({\n name: r.key,\n label: r.label,\n icon: r.icon,\n props: r.props ?? [],\n slots: r.slots ?? [],\n category: r.category,\n styles: r.styles,\n }));\n }\n}\n\nexport const registry = new Registry();\n\n/** Register a component so the dom tree may reference it by key. */\nexport function register(component: PlenaryComponent, registration: ComponentRegistration): Registry {\n return registry.register(component, registration);\n}\n\n/** Declare the site's design tokens (see Registry.registerTheme). */\nexport function registerTheme(theme: ThemeRegistration): Registry {\n return registry.registerTheme(theme);\n}\n\n/** Namespace mirroring the design brief's `Plenary.register(...)` surface. */\nexport const Plenary = { register, registerTheme };\n\n/** The manifest of every registered component (built-ins + custom). */\nexport function getManifest(): ManifestComponent[] {\n return registry.manifest();\n}\n\n/** The site's registered design tokens, if any. */\nexport function getTheme(): ThemeRegistration | undefined {\n return registry.getTheme();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type {\n ComponentRegistration,\n ManifestComponent,\n ThemeRegistration,\n} from \"@plenary/core\";\nimport type { PlenaryComponent, RegisteredComponent } from \"./types.js\";\nimport { Box } from \"./components/Box.js\";\nimport { Section } from \"./components/Section.js\";\nimport { Text } from \"./components/Text.js\";\nimport { Image } from \"./components/Image.js\";\nimport { Video } from \"./components/Video.js\";\n\n/** The built-in primitives, pre-registered so they behave like any custom component. */\nconst BUILTINS: {\n component: PlenaryComponent;\n registration: ComponentRegistration;\n}[] = [\n {\n component: Box,\n registration: {\n key: \"box\",\n label: \"Box\",\n category: \"Layout\",\n props: [{ key: \"className\", type: \"text\" }],\n slots: [{ name: \"children\", label: \"Content\" }],\n styles: [\"layout\", \"size\", \"background\", \"css\"],\n },\n },\n {\n component: Section,\n registration: {\n key: \"section\",\n label: \"Section\",\n category: \"Layout\",\n // Column slots (\"0\"…\"n-1\") are dynamic, driven by `columns`, and managed\n // by the built-in — so they are not declared statically here.\n props: [\n { key: \"columns\", type: \"number\", defaultValue: 1 },\n { key: \"gap\", type: \"number\", defaultValue: 16 },\n { key: \"className\", type: \"text\" },\n ],\n styles: [\"size\", \"background\", \"css\"],\n },\n },\n {\n component: Text,\n registration: {\n key: \"text\",\n label: \"Text\",\n category: \"Content\",\n props: [\n { key: \"content\", type: \"rich_text\" },\n {\n key: \"as\",\n type: \"select\",\n defaultValue: \"p\",\n // Options use the server's select field-settings shape ({value,label})\n // so the console's select input can drive them directly.\n settings: {\n options: [\n { value: \"p\", label: \"Paragraph\" },\n { value: \"h1\", label: \"Heading 1\" },\n { value: \"h2\", label: \"Heading 2\" },\n { value: \"h3\", label: \"Heading 3\" },\n { value: \"h4\", label: \"Heading 4\" },\n { value: \"h5\", label: \"Heading 5\" },\n { value: \"h6\", label: \"Heading 6\" },\n { value: \"span\", label: \"Span\" },\n { value: \"code\", label: \"Code\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"typography\", \"css\"],\n },\n },\n {\n component: Image,\n registration: {\n key: \"image\",\n label: \"Image\",\n category: \"Content\",\n props: [\n // Alt is a block prop (below), so the media picker shows only the\n // asset itself — not its own alt/caption sub-fields.\n { key: \"media\", type: \"image\", ui: { show_alt_caption: false } },\n { key: \"alt\", type: \"text\", ui: { label: \"Alt Text\" } },\n // A one-off variant spec for this placement. The server enqueues the\n // matching variant on save and hydrates `media.variant_url` on read;\n // with no dimensions set, the original renders. Labels say \"output\"\n // (pixels served) to keep these apart from the Styles tab's layout Size.\n { key: \"width\", type: \"number\", ui: { label: \"Max Width (px)\" } },\n { key: \"height\", type: \"number\", ui: { label: \"Max Height (px)\" } },\n { key: \"quality\", type: \"number\", ui: { label: \"Quality (%)\" } },\n { key: \"crop\", type: \"boolean\", ui: { label: \"Crop to fill\" } },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n {\n component: Video,\n registration: {\n key: \"video\",\n label: \"Video\",\n category: \"Content\",\n props: [\n // The source file is served as uploaded — no transcoding — so the\n // picker pins to the library's video type. Alt/caption sub-fields are\n // hidden like the Image block's: the player has no use for them.\n {\n key: \"media\",\n type: \"file\",\n ui: { label: \"Video\", media_type: \"video\", show_alt_caption: false },\n },\n {\n key: \"poster\",\n type: \"image\",\n ui: { label: \"Poster Image\", show_alt_caption: false },\n },\n { key: \"controls\", type: \"boolean\", defaultValue: true },\n {\n key: \"autoplay\",\n type: \"boolean\",\n ui: { label: \"Autoplay (browsers require Muted)\" },\n },\n { key: \"loop\", type: \"boolean\" },\n { key: \"muted\", type: \"boolean\" },\n {\n key: \"playsinline\",\n type: \"boolean\",\n defaultValue: true,\n ui: { label: \"Plays Inline\" },\n },\n {\n key: \"preload\",\n type: \"select\",\n defaultValue: \"metadata\",\n settings: {\n options: [\n { value: \"none\", label: \"None\" },\n { value: \"metadata\", label: \"Metadata\" },\n { value: \"auto\", label: \"Auto\" },\n ],\n },\n },\n ],\n styles: [\"size\", \"css\"],\n },\n },\n];\n\n/**\n * The module-singleton component registry. Because it is a shared module, both\n * the render path (SSR + client) and the preview bridge see the same\n * registrations — the site's registry file is imported wherever either is used.\n */\nexport class Registry {\n private components = new Map<string, RegisteredComponent>();\n private theme: ThemeRegistration | undefined;\n\n constructor() {\n for (const b of BUILTINS) this.register(b.component, b.registration);\n }\n\n /** Register a component (built-in or custom) under its registration key. */\n register(\n component: PlenaryComponent,\n registration: ComponentRegistration,\n ): this {\n this.components.set(registration.key, { component, registration });\n return this;\n }\n\n /**\n * Declare the site's design tokens. Each token mirrors a CSS custom property\n * the site's stylesheet defines (`--<name>`); the editor stores references\n * (`var(--<name>)`), so the tokens must actually exist in the site's CSS for\n * styled pages to render.\n */\n registerTheme(theme: ThemeRegistration): this {\n this.theme = theme;\n return this;\n }\n\n getTheme(): ThemeRegistration | undefined {\n return this.theme;\n }\n\n get(key: string): RegisteredComponent | undefined {\n return this.components.get(key);\n }\n\n all(): RegisteredComponent[] {\n return [...this.components.values()];\n }\n\n /** The full component manifest announced to the console over the bridge. */\n manifest(): ManifestComponent[] {\n return this.all().map(({ registration: r }) => ({\n name: r.key,\n label: r.label,\n icon: r.icon,\n props: r.props ?? [],\n slots: r.slots ?? [],\n category: r.category,\n styles: r.styles,\n }));\n }\n}\n\nexport const registry = new Registry();\n\n/** Register a component so the dom tree may reference it by key. */\nexport function register(\n component: PlenaryComponent,\n registration: ComponentRegistration,\n): Registry {\n return registry.register(component, registration);\n}\n\n/** Declare the site's design tokens (see Registry.registerTheme). */\nexport function registerTheme(theme: ThemeRegistration): Registry {\n return registry.registerTheme(theme);\n}\n\n/** Namespace mirroring the design brief's `Plenary.register(...)` surface. */\nexport const Plenary = { register, registerTheme };\n\n/** The manifest of every registered component (built-ins + custom). */\nexport function getManifest(): ManifestComponent[] {\n return registry.manifest();\n}\n\n/** The site's registered design tokens, if any. */\nexport function getTheme(): ThemeRegistration | undefined {\n return registry.getTheme();\n}\n"],"mappings":"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","names":[]}
|
package/dist/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { PlenaryClient, MediaDetail
|
|
2
|
+
import { PlenaryClient, MediaDetail } from '@plenary/client';
|
|
3
3
|
import { PlenaryDocument } from '@plenary/core';
|
|
4
4
|
import { RenderContext } from './types.js';
|
|
5
5
|
|
|
@@ -9,7 +9,6 @@ interface SyncRenderOptions {
|
|
|
9
9
|
/** Media ids known to be deleted. The console marks preview values instead. */
|
|
10
10
|
missingMedia?: Set<string>;
|
|
11
11
|
/** Pre-fetched form definitions, keyed by form key (empty resolves nothing). */
|
|
12
|
-
formDefinitions?: Map<string, PublicFormDefinition>;
|
|
13
12
|
/** Wrap nodes with `data-plenary-id` markers (for the preview bridge). */
|
|
14
13
|
annotate?: boolean;
|
|
15
14
|
/** Per-node remount epochs — see RenderOptions.remountKeys. */
|
|
@@ -29,18 +28,17 @@ interface SyncRenderOptions {
|
|
|
29
28
|
*/
|
|
30
29
|
declare function renderDocumentSync(document: PlenaryDocument, opts?: SyncRenderOptions): ReactNode;
|
|
31
30
|
/**
|
|
32
|
-
* Render a whole document to a React tree. Resolves media
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* SSR-safe.
|
|
31
|
+
* Render a whole document to a React tree. Resolves media first (one batched
|
|
32
|
+
* async pass), then walks the tree synchronously. Wrapped in Suspense so lazy
|
|
33
|
+
* (`React.lazy` / `next/dynamic`) custom components are SSR-safe.
|
|
36
34
|
*/
|
|
37
35
|
declare function renderDocument(document: PlenaryDocument, ctx?: RenderContext): Promise<ReactNode>;
|
|
38
36
|
interface PlenaryPageProps {
|
|
39
37
|
document: PlenaryDocument;
|
|
40
|
-
/** Client used to resolve media
|
|
38
|
+
/** Client used to resolve media props (optional). */
|
|
41
39
|
client?: PlenaryClient;
|
|
42
40
|
}
|
|
43
41
|
/** Renders a Page's `dom` tree. An async server component (SSR-friendly). */
|
|
44
|
-
declare function PlenaryPage({ document, client }: PlenaryPageProps): Promise<ReactNode>;
|
|
42
|
+
declare function PlenaryPage({ document, client, }: PlenaryPageProps): Promise<ReactNode>;
|
|
45
43
|
|
|
46
44
|
export { PlenaryPage, type PlenaryPageProps, type SyncRenderOptions, renderDocument, renderDocumentSync };
|