@plenary/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge.d.ts +34 -0
  3. package/dist/bridge.js +452 -0
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/components/Box.d.ts +14 -0
  6. package/dist/components/Box.js +8 -0
  7. package/dist/components/Box.js.map +1 -0
  8. package/dist/components/Form.d.ts +36 -0
  9. package/dist/components/Form.js +280 -0
  10. package/dist/components/Form.js.map +1 -0
  11. package/dist/components/Image.d.ts +25 -0
  12. package/dist/components/Image.js +116 -0
  13. package/dist/components/Image.js.map +1 -0
  14. package/dist/components/Placeholder.d.ts +12 -0
  15. package/dist/components/Placeholder.js +24 -0
  16. package/dist/components/Placeholder.js.map +1 -0
  17. package/dist/components/Section.d.ts +20 -0
  18. package/dist/components/Section.js +15 -0
  19. package/dist/components/Section.js.map +1 -0
  20. package/dist/components/Text.d.ts +15 -0
  21. package/dist/components/Text.js +8 -0
  22. package/dist/components/Text.js.map +1 -0
  23. package/dist/components/Video.d.ts +48 -0
  24. package/dist/components/Video.js +131 -0
  25. package/dist/components/Video.js.map +1 -0
  26. package/dist/context.d.ts +26 -0
  27. package/dist/context.js +20 -0
  28. package/dist/context.js.map +1 -0
  29. package/dist/form.d.ts +25 -0
  30. package/dist/form.js +11 -0
  31. package/dist/form.js.map +1 -0
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.js +55 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/media.d.ts +60 -0
  36. package/dist/media.js +74 -0
  37. package/dist/media.js.map +1 -0
  38. package/dist/registry.d.ts +45 -0
  39. package/dist/registry.js +232 -0
  40. package/dist/registry.js.map +1 -0
  41. package/dist/render.d.ts +46 -0
  42. package/dist/render.js +179 -0
  43. package/dist/render.js.map +1 -0
  44. package/dist/types.d.ts +24 -0
  45. package/dist/types.js +1 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick DeLoughry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface PlenaryBridgeProps {
5
+ /**
6
+ * The `blocks` field on the entry this bridge previews — the key in the draft's
7
+ * `data` whose component tree it renders. One `<PlenaryBridge>` drives one region.
8
+ */
9
+ region: string;
10
+ /**
11
+ * Console origins allowed to drive this preview; others are ignored. Each
12
+ * entry is an exact origin (`https://cms.example.com`, `http://localhost:5173`)
13
+ * or a pattern: `https://*.example.com` allows any subdomain (never the bare
14
+ * apex), `http://localhost:*` allows any port — so a locally-running console
15
+ * can drive a production site. See `isAllowedEditorOrigin` in `@plenary/core`.
16
+ */
17
+ editorOrigins: string[];
18
+ /** Reported to the console in the `hello` handshake. */
19
+ sdkVersion?: string;
20
+ /** Normal page content, rendered until a draft render takes over. */
21
+ children?: ReactNode;
22
+ }
23
+ /**
24
+ * The iframe side of live preview. Mounted by the site; inert until a valid
25
+ * handshake from an allowed origin arrives. Once active it renders the draft the
26
+ * console pushes over postMessage instead of the normal page, and reports
27
+ * clicks/hovers back for block selection.
28
+ *
29
+ * Origin discipline is the security model: every inbound message must come from
30
+ * an `editorOrigins` origin and carry `{ plenary: true }`.
31
+ */
32
+ declare function PlenaryBridge({ region, editorOrigins, sdkVersion, children, }: PlenaryBridgeProps): react.JSX.Element;
33
+
34
+ export { PlenaryBridge, type PlenaryBridgeProps };
package/dist/bridge.js ADDED
@@ -0,0 +1,452 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState
8
+ } from "react";
9
+ import {
10
+ PROTOCOL_VERSION,
11
+ DND_NEW,
12
+ hasWildcardPattern,
13
+ isAllowedEditorOrigin
14
+ } from "@plenary/core";
15
+ import { renderDocumentSync } from "./render.js";
16
+ import { getManifest, getTheme } from "./registry.js";
17
+ function patchDom(doc, nodeId, propsPatch) {
18
+ const patch = (nodes) => nodes.map((node) => {
19
+ let next = node;
20
+ if (node.id === nodeId) next = { ...next, props: { ...next.props, ...propsPatch } };
21
+ if (next.slots) {
22
+ const slots = {};
23
+ for (const [name, children] of Object.entries(next.slots)) slots[name] = patch(children);
24
+ next = { ...next, slots };
25
+ }
26
+ return next;
27
+ });
28
+ return { ...doc, root: patch(doc.root) };
29
+ }
30
+ function nodeIdFromEvent(e) {
31
+ const el = e.target?.closest?.("[data-plenary-id]");
32
+ return el?.getAttribute("data-plenary-id") ?? null;
33
+ }
34
+ function findNodeById(nodes, id) {
35
+ for (const node of nodes) {
36
+ if (node.id === id) return node;
37
+ if (node.slots) {
38
+ for (const children of Object.values(node.slots)) {
39
+ const hit = findNodeById(children, id);
40
+ if (hit) return hit;
41
+ }
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ let editableMode = null;
47
+ function getEditableMode() {
48
+ if (editableMode) return editableMode;
49
+ const probe = document.createElement("div");
50
+ try {
51
+ probe.contentEditable = "plaintext-only";
52
+ editableMode = "plaintext-only";
53
+ } catch {
54
+ editableMode = "true";
55
+ }
56
+ return editableMode;
57
+ }
58
+ function useNavigationLock(locked) {
59
+ useEffect(() => {
60
+ if (!locked || typeof document === "undefined") return;
61
+ const onClick = (e) => {
62
+ if (e.target?.closest?.("a[href]")) e.preventDefault();
63
+ };
64
+ const onSubmit = (e) => e.preventDefault();
65
+ document.addEventListener("click", onClick, true);
66
+ document.addEventListener("submit", onSubmit, true);
67
+ return () => {
68
+ document.removeEventListener("click", onClick, true);
69
+ document.removeEventListener("submit", onSubmit, true);
70
+ };
71
+ }, [locked]);
72
+ }
73
+ function resolveDrop(x, y) {
74
+ const root = document.querySelector("[data-plenary-preview]");
75
+ const el = document.elementFromPoint(x, y);
76
+ if (!root || !el || !root.contains(el)) {
77
+ return { target: { at: "root" }, rect: root?.getBoundingClientRect() ?? null, mode: "after" };
78
+ }
79
+ const marker = el.closest("[data-plenary-id]");
80
+ const column = el.closest("[data-plenary-column]");
81
+ if (column && (!marker || !column.contains(marker))) {
82
+ const sectionId = column.closest("[data-plenary-id]")?.getAttribute("data-plenary-id");
83
+ const slot = column.getAttribute("data-plenary-column");
84
+ if (sectionId && slot != null) {
85
+ return { target: { at: "slot", nodeId: sectionId, slot }, rect: column.getBoundingClientRect(), mode: "into" };
86
+ }
87
+ }
88
+ if (marker) {
89
+ const id = marker.getAttribute("data-plenary-id");
90
+ const box = marker.firstElementChild ?? marker;
91
+ const rect = box.getBoundingClientRect();
92
+ const after = y > rect.top + rect.height / 2;
93
+ return { target: { at: after ? "after" : "before", nodeId: id }, rect, mode: after ? "after" : "before" };
94
+ }
95
+ return { target: { at: "root" }, rect: root.getBoundingClientRect(), mode: "after" };
96
+ }
97
+ function PlenaryBridge({
98
+ region,
99
+ editorOrigins,
100
+ sdkVersion = "0.0.0",
101
+ children
102
+ }) {
103
+ const [draft, setDraft] = useState(null);
104
+ const [active, setActive] = useState(false);
105
+ const [selectedId, setSelectedId] = useState(null);
106
+ const [hoverId, setHoverId] = useState(null);
107
+ const [dropHint, setDropHint] = useState(null);
108
+ const consoleOrigin = useRef(null);
109
+ const [remountEpochs, setRemountEpochs] = useState(/* @__PURE__ */ new Map());
110
+ const editSession = useRef(null);
111
+ useNavigationLock(active);
112
+ const regionDoc = draft ? draft.data[region] : void 0;
113
+ const send = useCallback((type, payload, origin) => {
114
+ const target = origin ?? consoleOrigin.current;
115
+ if (typeof window === "undefined" || !target) return;
116
+ window.parent.postMessage({ plenary: true, v: PROTOCOL_VERSION, type, payload }, target);
117
+ }, []);
118
+ const endEdit = useCallback(
119
+ (commit) => {
120
+ const session = editSession.current;
121
+ if (!session) return;
122
+ editSession.current = null;
123
+ const { nodeId, el, original, onBlur } = session;
124
+ el.removeEventListener("blur", onBlur);
125
+ el.removeAttribute("contenteditable");
126
+ const text = (el.innerText ?? el.textContent ?? "").toString();
127
+ setRemountEpochs((prev) => new Map(prev).set(nodeId, (prev.get(nodeId) ?? 0) + 1));
128
+ if (!commit || text === original) return;
129
+ setDraft((prev) => {
130
+ if (!prev) return prev;
131
+ const doc = prev.data[region];
132
+ if (!doc) return prev;
133
+ return { ...prev, data: { ...prev.data, [region]: patchDom(doc, nodeId, { content: text }) } };
134
+ });
135
+ send("edit", { nodeId, props: { content: text } });
136
+ },
137
+ [region, send]
138
+ );
139
+ useEffect(() => {
140
+ if (typeof window === "undefined" || window.parent === window) return;
141
+ const reportSize = () => {
142
+ send("size", { height: Math.ceil(document.documentElement.scrollHeight) });
143
+ };
144
+ let sizeTimer = null;
145
+ let observer = null;
146
+ if (typeof ResizeObserver !== "undefined") {
147
+ observer = new ResizeObserver(() => {
148
+ if (sizeTimer) return;
149
+ sizeTimer = setTimeout(() => {
150
+ sizeTimer = null;
151
+ reportSize();
152
+ }, 150);
153
+ });
154
+ observer.observe(document.body);
155
+ }
156
+ const onWheelGesture = (e) => {
157
+ if (!consoleOrigin.current || !(e.ctrlKey || e.metaKey)) return;
158
+ e.preventDefault();
159
+ send("wheel", { deltaY: e.deltaY, x: e.clientX, y: e.clientY });
160
+ };
161
+ let panFrom = null;
162
+ const onPanStart = (e) => {
163
+ if (!consoleOrigin.current || e.button !== 1) return;
164
+ panFrom = { x: e.clientX, y: e.clientY };
165
+ try {
166
+ e.target?.setPointerCapture?.(e.pointerId);
167
+ } catch {
168
+ }
169
+ };
170
+ const onPanMove = (e) => {
171
+ if (!panFrom) return;
172
+ if ((e.buttons & 4) === 0) {
173
+ panFrom = null;
174
+ return;
175
+ }
176
+ send("pan", { dx: e.clientX - panFrom.x, dy: e.clientY - panFrom.y });
177
+ panFrom = { x: e.clientX, y: e.clientY };
178
+ };
179
+ const onPanEnd = (e) => {
180
+ if (e.button === 1) panFrom = null;
181
+ };
182
+ const onMiddleMouseDown = (e) => {
183
+ if (consoleOrigin.current && e.button === 1) e.preventDefault();
184
+ };
185
+ const onKeyDown = (e) => {
186
+ const session = editSession.current;
187
+ if (session && session.el.contains(e.target)) {
188
+ if (e.key === "Escape") {
189
+ e.preventDefault();
190
+ endEdit(false);
191
+ } else if (e.key === "Enter" && !e.shiftKey) {
192
+ e.preventDefault();
193
+ endEdit(true);
194
+ }
195
+ return;
196
+ }
197
+ if (!consoleOrigin.current) return;
198
+ const t = e.target;
199
+ if (t && (t.isContentEditable || t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement))
200
+ return;
201
+ const key = e.key.toLowerCase();
202
+ const mod = e.ctrlKey || e.metaKey;
203
+ if (mod && (key === "c" || key === "x") && document.getSelection()?.isCollapsed === false)
204
+ return;
205
+ const forward = mod ? ["z", "y", "s", "d", "c", "x", "v"].includes(key) : key === "delete" || key === "backspace";
206
+ if (!forward) return;
207
+ e.preventDefault();
208
+ send("key", {
209
+ key: e.key,
210
+ ctrlKey: e.ctrlKey,
211
+ metaKey: e.metaKey,
212
+ shiftKey: e.shiftKey
213
+ });
214
+ };
215
+ document.addEventListener("keydown", onKeyDown);
216
+ document.addEventListener("wheel", onWheelGesture, { passive: false });
217
+ document.addEventListener("pointerdown", onPanStart);
218
+ document.addEventListener("pointermove", onPanMove);
219
+ document.addEventListener("pointerup", onPanEnd);
220
+ document.addEventListener("mousedown", onMiddleMouseDown);
221
+ const onMessage = (e) => {
222
+ if (!isAllowedEditorOrigin(editorOrigins, e.origin)) return;
223
+ const msg = e.data;
224
+ if (!msg || msg.plenary !== true) return;
225
+ switch (msg.type) {
226
+ case "init":
227
+ consoleOrigin.current = e.origin;
228
+ setActive(true);
229
+ send(
230
+ "manifest",
231
+ { components: getManifest(), theme: getTheme() },
232
+ e.origin
233
+ );
234
+ reportSize();
235
+ break;
236
+ case "render":
237
+ consoleOrigin.current = e.origin;
238
+ setActive(true);
239
+ endEdit(false);
240
+ setDraft(msg.payload);
241
+ break;
242
+ case "patch": {
243
+ endEdit(false);
244
+ const p = msg.payload;
245
+ setDraft((prev) => {
246
+ if (!prev) return prev;
247
+ const doc = prev.data[region];
248
+ if (!doc) return prev;
249
+ return { ...prev, data: { ...prev.data, [region]: patchDom(doc, p.nodeId, p.props) } };
250
+ });
251
+ break;
252
+ }
253
+ case "highlight": {
254
+ const p = msg.payload;
255
+ setSelectedId(p.selectedId);
256
+ setHoverId(p.hoverId);
257
+ break;
258
+ }
259
+ }
260
+ };
261
+ window.addEventListener("message", onMessage);
262
+ const hello = { protocolVersion: PROTOCOL_VERSION, sdkVersion };
263
+ for (const origin of editorOrigins) {
264
+ if (!origin.includes("*")) send("hello", hello, origin);
265
+ }
266
+ if (hasWildcardPattern(editorOrigins)) send("hello", hello, "*");
267
+ return () => {
268
+ window.removeEventListener("message", onMessage);
269
+ document.removeEventListener("keydown", onKeyDown);
270
+ document.removeEventListener("wheel", onWheelGesture);
271
+ document.removeEventListener("pointerdown", onPanStart);
272
+ document.removeEventListener("pointermove", onPanMove);
273
+ document.removeEventListener("pointerup", onPanEnd);
274
+ document.removeEventListener("mousedown", onMiddleMouseDown);
275
+ observer?.disconnect();
276
+ if (sizeTimer) clearTimeout(sizeTimer);
277
+ };
278
+ }, [editorOrigins, sdkVersion, region, send, endEdit]);
279
+ const beginEdit = (nodeId, el) => {
280
+ endEdit(true);
281
+ const onBlur = () => endEdit(true);
282
+ el.setAttribute("contenteditable", getEditableMode());
283
+ el.addEventListener("blur", onBlur);
284
+ editSession.current = { nodeId, el, original: el.textContent ?? "", onBlur };
285
+ el.focus();
286
+ };
287
+ const onPreviewDoubleClick = (e) => {
288
+ const id = nodeIdFromEvent(e);
289
+ if (!id || !regionDoc) return;
290
+ if (findNodeById(regionDoc.root, id)?.component !== "text") return;
291
+ const el = e.target.closest("[data-plenary-id]")?.firstElementChild;
292
+ if (el) beginEdit(id, el);
293
+ };
294
+ const isPaletteDrag = (e) => e.dataTransfer.types.includes(DND_NEW);
295
+ const onDragOver = (e) => {
296
+ if (!isPaletteDrag(e)) return;
297
+ e.preventDefault();
298
+ e.dataTransfer.dropEffect = "copy";
299
+ setDropHint(resolveDrop(e.clientX, e.clientY));
300
+ };
301
+ const onDrop = (e) => {
302
+ if (!isPaletteDrag(e)) return;
303
+ e.preventDefault();
304
+ const component = e.dataTransfer.getData(DND_NEW);
305
+ const { target } = resolveDrop(e.clientX, e.clientY);
306
+ if (component) send("canvasDrop", { component, target });
307
+ setDropHint(null);
308
+ };
309
+ const onDragLeave = (e) => {
310
+ if (!e.currentTarget.contains(e.relatedTarget)) setDropHint(null);
311
+ };
312
+ if (!active) return /* @__PURE__ */ jsx(Fragment, { children });
313
+ return /* @__PURE__ */ jsxs(
314
+ "div",
315
+ {
316
+ "data-plenary-preview": "",
317
+ onClick: (e) => {
318
+ const id = nodeIdFromEvent(e);
319
+ if (id) send("select", { nodeId: id });
320
+ },
321
+ onDoubleClick: onPreviewDoubleClick,
322
+ onMouseOver: (e) => send("hover", { nodeId: nodeIdFromEvent(e) }),
323
+ onMouseOut: () => send("hover", { nodeId: null }),
324
+ onDragOver,
325
+ onDrop,
326
+ onDragLeave,
327
+ children: [
328
+ regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null,
329
+ regionDoc ? videoControlsBadgeIds(regionDoc).map((id) => /* @__PURE__ */ jsx(VideoControlsBadge, { nodeId: id }, id)) : null,
330
+ selectedId ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: selectedId, variant: "selected" }) : null,
331
+ hoverId && hoverId !== selectedId ? /* @__PURE__ */ jsx(HighlightOverlay, { nodeId: hoverId, variant: "hover" }) : null,
332
+ dropHint ? /* @__PURE__ */ jsx(DropIndicator, { hint: dropHint }) : null
333
+ ]
334
+ }
335
+ );
336
+ }
337
+ function DropIndicator({ hint }) {
338
+ if (!hint.rect) return null;
339
+ const { rect, mode } = hint;
340
+ const base = { position: "fixed", pointerEvents: "none", zIndex: 2147483647 };
341
+ if (mode === "into") {
342
+ return /* @__PURE__ */ jsx(
343
+ "div",
344
+ {
345
+ style: {
346
+ ...base,
347
+ left: rect.left,
348
+ top: rect.top,
349
+ width: rect.width,
350
+ height: rect.height,
351
+ outline: "2px dashed #4f46e5",
352
+ outlineOffset: "-2px",
353
+ background: "rgba(79,70,229,0.06)"
354
+ }
355
+ }
356
+ );
357
+ }
358
+ const y = mode === "before" ? rect.top : rect.bottom;
359
+ return /* @__PURE__ */ jsx("div", { style: { ...base, left: rect.left, top: y - 1, width: rect.width, height: 2, background: "#4f46e5" } });
360
+ }
361
+ function sameBox(a, b) {
362
+ if (!a || !b) return a === b;
363
+ return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
364
+ }
365
+ const OUTLINE_STYLE = {
366
+ selected: { outline: "2px solid #4f46e5" },
367
+ hover: { outline: "1px dashed #818cf8" }
368
+ };
369
+ function useNodeRect(nodeId) {
370
+ const [rect, setRect] = useState(null);
371
+ useEffect(() => {
372
+ let frame = 0;
373
+ const measure = () => {
374
+ const marker = document.querySelector(`[data-plenary-id="${CSS.escape(nodeId)}"]`);
375
+ const target = marker?.firstElementChild ?? marker;
376
+ const next = target ? target.getBoundingClientRect() : null;
377
+ setRect((prev) => sameBox(prev, next) ? prev : next);
378
+ frame = requestAnimationFrame(measure);
379
+ };
380
+ measure();
381
+ return () => cancelAnimationFrame(frame);
382
+ }, [nodeId]);
383
+ return rect;
384
+ }
385
+ function HighlightOverlay({ nodeId, variant }) {
386
+ const rect = useNodeRect(nodeId);
387
+ if (!rect) return null;
388
+ return /* @__PURE__ */ jsx(
389
+ "div",
390
+ {
391
+ style: {
392
+ position: "fixed",
393
+ left: rect.left,
394
+ top: rect.top,
395
+ width: rect.width,
396
+ height: rect.height,
397
+ ...OUTLINE_STYLE[variant],
398
+ pointerEvents: "none",
399
+ zIndex: 2147483647
400
+ }
401
+ }
402
+ );
403
+ }
404
+ function videoControlsBadgeIds(doc) {
405
+ const ids = [];
406
+ const walk = (nodes) => {
407
+ for (const node of nodes) {
408
+ const media = node.props?.media;
409
+ if (node.component === "video" && node.props?.controls && media && !media.missing) {
410
+ ids.push(node.id);
411
+ }
412
+ for (const children of Object.values(node.slots ?? {})) walk(children);
413
+ }
414
+ };
415
+ walk(doc.root);
416
+ return ids;
417
+ }
418
+ function VideoControlsBadge({ nodeId }) {
419
+ const rect = useNodeRect(nodeId);
420
+ if (!rect || rect.width < 96 || rect.height < 48) return null;
421
+ const glyph = (d) => /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d }) }, d);
422
+ return /* @__PURE__ */ jsxs(
423
+ "div",
424
+ {
425
+ "data-plenary-video-controls-badge": "",
426
+ style: {
427
+ position: "fixed",
428
+ left: rect.left + 8,
429
+ top: rect.bottom - 26,
430
+ display: "flex",
431
+ alignItems: "center",
432
+ gap: 6,
433
+ padding: "4px 9px",
434
+ borderRadius: 9999,
435
+ background: "rgba(0,0,0,0.65)",
436
+ color: "#fff",
437
+ pointerEvents: "none",
438
+ zIndex: 2147483646
439
+ // just under the highlight outlines
440
+ },
441
+ children: [
442
+ glyph("M8 5v14l11-7z"),
443
+ 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"),
444
+ glyph("M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z")
445
+ ]
446
+ }
447
+ );
448
+ }
449
+ export {
450
+ PlenaryBridge
451
+ };
452
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bridge.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type DragEvent as ReactDragEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from \"react\";\nimport {\n PROTOCOL_VERSION,\n DND_NEW,\n hasWildcardPattern,\n isAllowedEditorOrigin,\n type PlenaryDocument,\n type PlenaryNode,\n type ManifestPayload,\n type RenderPayload,\n type PatchPayload,\n type HighlightPayload,\n type CanvasDropPayload,\n type CanvasDropTarget,\n type SizePayload,\n type WheelPayload,\n type PanPayload,\n type KeyPayload,\n type EditPayload,\n} from \"@plenary/core\";\nimport { renderDocumentSync } from \"./render.js\";\nimport { getManifest, getTheme } from \"./registry.js\";\n\nexport interface PlenaryBridgeProps {\n /**\n * The `blocks` field on the entry this bridge previews — the key in the draft's\n * `data` whose component tree it renders. One `<PlenaryBridge>` drives one region.\n */\n region: string;\n /**\n * Console origins allowed to drive this preview; others are ignored. Each\n * entry is an exact origin (`https://cms.example.com`, `http://localhost:5173`)\n * or a pattern: `https://*.example.com` allows any subdomain (never the bare\n * apex), `http://localhost:*` allows any port — so a locally-running console\n * can drive a production site. See `isAllowedEditorOrigin` in `@plenary/core`.\n */\n editorOrigins: string[];\n /** Reported to the console in the `hello` handshake. */\n sdkVersion?: string;\n /** Normal page content, rendered until a draft render takes over. */\n children?: ReactNode;\n}\n\ninterface Draft {\n data: Record<string, unknown>;\n locale: string;\n}\n\n/** Return a new document with `propsPatch` merged into the node with `nodeId`. */\nfunction patchDom(\n doc: PlenaryDocument,\n nodeId: string,\n propsPatch: Record<string, unknown>,\n): PlenaryDocument {\n const patch = (nodes: PlenaryNode[]): PlenaryNode[] =>\n nodes.map((node) => {\n let next = node;\n if (node.id === nodeId) next = { ...next, props: { ...next.props, ...propsPatch } };\n if (next.slots) {\n const slots: Record<string, PlenaryNode[]> = {};\n for (const [name, children] of Object.entries(next.slots)) slots[name] = patch(children);\n next = { ...next, slots };\n }\n return next;\n });\n return { ...doc, root: patch(doc.root) };\n}\n\nfunction nodeIdFromEvent(e: ReactMouseEvent): string | null {\n const el = (e.target as HTMLElement | null)?.closest?.(\"[data-plenary-id]\");\n return el?.getAttribute(\"data-plenary-id\") ?? null;\n}\n\nfunction findNodeById(nodes: PlenaryNode[], id: string): PlenaryNode | null {\n for (const node of nodes) {\n if (node.id === id) return node;\n if (node.slots) {\n for (const children of Object.values(node.slots)) {\n const hit = findNodeById(children, id);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/**\n * The contenteditable mode for inline editing: `plaintext-only` where supported\n * (assigning an unsupported value to the property throws per spec), else `true`\n * — pasted markup is flattened on read either way.\n */\nlet editableMode: string | null = null;\nfunction getEditableMode(): string {\n if (editableMode) return editableMode;\n const probe = document.createElement(\"div\");\n try {\n probe.contentEditable = \"plaintext-only\";\n editableMode = \"plaintext-only\";\n } catch {\n editableMode = \"true\";\n }\n return editableMode;\n}\n\n/** A live inline-editing session on a text block's rendered element. */\ninterface EditSession {\n nodeId: string;\n el: HTMLElement;\n /** The committed content when editing began, to skip no-op edits. */\n original: string;\n onBlur: () => void;\n}\n\n/**\n * Keep the framed page from navigating away from the entry being edited. While\n * `locked`, link clicks and form submits inside the frame are cancelled — but\n * nothing else is: the listeners run in the capture phase and only call\n * `preventDefault()`, never `stopPropagation()`, so the bridge's own click/hover\n * selection (and any other event the SDK relies on) still fire normally.\n *\n * Toggleable by design: pass `false` and the lock lifts. It is tied to the\n * active preview today; when the console later lets the user browse their own\n * site and resolves the entry from the preview URL, that mode would drive this\n * flag (over the protocol) instead of blocking outright.\n */\nfunction useNavigationLock(locked: boolean) {\n useEffect(() => {\n if (!locked || typeof document === \"undefined\") return;\n const onClick = (e: MouseEvent) => {\n if ((e.target as Element | null)?.closest?.(\"a[href]\")) e.preventDefault();\n };\n const onSubmit = (e: Event) => e.preventDefault();\n document.addEventListener(\"click\", onClick, true);\n document.addEventListener(\"submit\", onSubmit, true);\n return () => {\n document.removeEventListener(\"click\", onClick, true);\n document.removeEventListener(\"submit\", onSubmit, true);\n };\n }, [locked]);\n}\n\ninterface DropHint {\n target: CanvasDropTarget;\n /** Viewport rect of the reference element, for drawing the indicator. */\n rect: DOMRect | null;\n mode: \"before\" | \"after\" | \"into\";\n}\n\n/**\n * Resolve a pointer position over the rendered preview to a tree drop target:\n * before/after the block under the cursor, into an empty Section column, or the\n * region root. Uses the `data-plenary-id` / `data-plenary-column` annotations the\n * renderer emits.\n */\nfunction resolveDrop(x: number, y: number): DropHint {\n const root = document.querySelector(\"[data-plenary-preview]\");\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n if (!root || !el || !root.contains(el)) {\n return { target: { at: \"root\" }, rect: root?.getBoundingClientRect() ?? null, mode: \"after\" };\n }\n const marker = el.closest(\"[data-plenary-id]\") as HTMLElement | null;\n const column = el.closest(\"[data-plenary-column]\") as HTMLElement | null;\n\n // Empty column space: the nearest block marker is the Section (an ancestor of\n // the column), not a child inside it → drop into that column's slot.\n if (column && (!marker || !column.contains(marker))) {\n const sectionId = column.closest(\"[data-plenary-id]\")?.getAttribute(\"data-plenary-id\");\n const slot = column.getAttribute(\"data-plenary-column\");\n if (sectionId && slot != null) {\n return { target: { at: \"slot\", nodeId: sectionId, slot }, rect: column.getBoundingClientRect(), mode: \"into\" };\n }\n }\n\n if (marker) {\n const id = marker.getAttribute(\"data-plenary-id\")!;\n // Markers use display:contents (no box); measure the rendered child.\n const box = (marker.firstElementChild as HTMLElement | null) ?? marker;\n const rect = box.getBoundingClientRect();\n const after = y > rect.top + rect.height / 2;\n return { target: { at: after ? \"after\" : \"before\", nodeId: id }, rect, mode: after ? \"after\" : \"before\" };\n }\n\n return { target: { at: \"root\" }, rect: root.getBoundingClientRect(), mode: \"after\" };\n}\n\n/**\n * The iframe side of live preview. Mounted by the site; inert until a valid\n * handshake from an allowed origin arrives. Once active it renders the draft the\n * console pushes over postMessage instead of the normal page, and reports\n * clicks/hovers back for block selection.\n *\n * Origin discipline is the security model: every inbound message must come from\n * an `editorOrigins` origin and carry `{ plenary: true }`.\n */\nexport function PlenaryBridge({\n region,\n editorOrigins,\n sdkVersion = \"0.0.0\",\n children,\n}: PlenaryBridgeProps) {\n const [draft, setDraft] = useState<Draft | null>(null);\n const [active, setActive] = useState(false);\n // Two outline channels the console drives independently: the selected block and\n // whatever is hovered (a tree row or a preview element). Drawn with distinct styles.\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [hoverId, setHoverId] = useState<string | null>(null);\n const [dropHint, setDropHint] = useState<DropHint | null>(null);\n const consoleOrigin = useRef<string | null>(null);\n // Nodes whose DOM must be rebuilt from the document on the next render (their\n // key changes), because an inline edit mutated DOM React never tracked.\n const [remountEpochs, setRemountEpochs] = useState<ReadonlyMap<string, number>>(new Map());\n const editSession = useRef<EditSession | null>(null);\n\n // While the console is driving the preview, don't let a stray link or form\n // submit navigate the frame off the entry being edited.\n useNavigationLock(active);\n\n // The component tree this bridge renders: the `region` blocks field of the draft.\n const regionDoc = draft ? (draft.data[region] as PlenaryDocument | undefined) : undefined;\n\n const send = useCallback((type: string, payload: unknown, origin?: string) => {\n const target = origin ?? consoleOrigin.current;\n if (typeof window === \"undefined\" || !target) return;\n window.parent.postMessage({ plenary: true, v: PROTOCOL_VERSION, type, payload }, target);\n }, []);\n\n /** End the inline-editing session: tear down, remount the node, send the change up. */\n const endEdit = useCallback(\n (commit: boolean) => {\n const session = editSession.current;\n if (!session) return;\n editSession.current = null;\n const { nodeId, el, original, onBlur } = session;\n el.removeEventListener(\"blur\", onBlur);\n el.removeAttribute(\"contenteditable\");\n const text = (el.innerText ?? el.textContent ?? \"\").toString();\n // Rebuild the node from the document: typing left DOM state React never saw.\n setRemountEpochs((prev) => new Map(prev).set(nodeId, (prev.get(nodeId) ?? 0) + 1));\n if (!commit || text === original) return;\n // Apply locally first so the remounted node renders the new text at once;\n // the console's render echo then arrives already matching.\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, nodeId, { content: text }) } };\n });\n send(\"edit\", { nodeId, props: { content: text } } satisfies EditPayload);\n },\n [region, send],\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\" || window.parent === window) return; // not framed\n\n // Report the page's content height so the console's artboard can size the\n // iframe to the whole page (no inner scrollbar). Sent on init and, trailing-\n // throttled, whenever the body's size changes; send() drops it until the\n // console origin is known.\n const reportSize = () => {\n send(\"size\", { height: Math.ceil(document.documentElement.scrollHeight) } satisfies SizePayload);\n };\n let sizeTimer: ReturnType<typeof setTimeout> | null = null;\n let observer: ResizeObserver | null = null;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(() => {\n if (sizeTimer) return;\n sizeTimer = setTimeout(() => {\n sizeTimer = null;\n reportSize();\n }, 150);\n });\n observer.observe(document.body);\n }\n\n // Artboard gestures performed over the page. The iframe consumes these\n // events, so the console never sees them — forward them once connected:\n // Ctrl/Cmd+wheel becomes a zoom request (page point included so the console\n // can anchor the zoom at the cursor), and a middle-button drag becomes pan\n // steps (the hand tool). Both are prevented locally: the page must neither\n // scroll under a zoom gesture nor start autoscroll under a pan.\n const onWheelGesture = (e: WheelEvent) => {\n if (!consoleOrigin.current || !(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n send(\"wheel\", { deltaY: e.deltaY, x: e.clientX, y: e.clientY } satisfies WheelPayload);\n };\n let panFrom: { x: number; y: number } | null = null;\n const onPanStart = (e: PointerEvent) => {\n if (!consoleOrigin.current || e.button !== 1) return;\n panFrom = { x: e.clientX, y: e.clientY };\n // Capture keeps the drag alive even when the pointer leaves the iframe.\n try {\n (e.target as Element | null)?.setPointerCapture?.(e.pointerId);\n } catch {\n /* capture is best-effort */\n }\n };\n const onPanMove = (e: PointerEvent) => {\n if (!panFrom) return;\n // The middle button was released outside our reach (e.g. over browser chrome).\n if ((e.buttons & 4) === 0) {\n panFrom = null;\n return;\n }\n send(\"pan\", { dx: e.clientX - panFrom.x, dy: e.clientY - panFrom.y } satisfies PanPayload);\n panFrom = { x: e.clientX, y: e.clientY };\n };\n const onPanEnd = (e: PointerEvent) => {\n if (e.button === 1) panFrom = null;\n };\n // Middle-button autoscroll is a `mousedown` default, not a pointerdown one.\n const onMiddleMouseDown = (e: MouseEvent) => {\n if (consoleOrigin.current && e.button === 1) e.preventDefault();\n };\n // Editor shortcuts pressed while the frame has focus (clicking a block\n // focuses the iframe, so the console's own keydown listener never fires).\n // Forward only recognized shortcut combinations, preventing their default\n // here — the frame must not run its own native undo or save dialog.\n const onKeyDown = (e: KeyboardEvent) => {\n // Inside an inline-edit session, Escape cancels and Enter commits\n // (Shift+Enter keeps the native line break); all other keys are native\n // editing — never forwarded (the editable-target check below also holds).\n const session = editSession.current;\n if (session && session.el.contains(e.target as Node)) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n endEdit(false);\n } else if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n endEdit(true);\n }\n return;\n }\n if (!consoleOrigin.current) return;\n const t = e.target as HTMLElement | null;\n if (\n t &&\n (t.isContentEditable ||\n t instanceof HTMLInputElement ||\n t instanceof HTMLTextAreaElement ||\n t instanceof HTMLSelectElement)\n )\n return;\n const key = e.key.toLowerCase();\n const mod = e.ctrlKey || e.metaKey;\n // A live text selection in the page keeps native copy/cut.\n if (mod && (key === \"c\" || key === \"x\") && document.getSelection()?.isCollapsed === false)\n return;\n const forward = mod\n ? [\"z\", \"y\", \"s\", \"d\", \"c\", \"x\", \"v\"].includes(key)\n : key === \"delete\" || key === \"backspace\";\n if (!forward) return;\n e.preventDefault();\n send(\"key\", {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n } satisfies KeyPayload);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n document.addEventListener(\"wheel\", onWheelGesture, { passive: false });\n document.addEventListener(\"pointerdown\", onPanStart);\n document.addEventListener(\"pointermove\", onPanMove);\n document.addEventListener(\"pointerup\", onPanEnd);\n document.addEventListener(\"mousedown\", onMiddleMouseDown);\n\n const onMessage = (e: MessageEvent) => {\n if (!isAllowedEditorOrigin(editorOrigins, e.origin)) return;\n const msg = e.data as { plenary?: boolean; type?: string; payload?: unknown } | null;\n if (!msg || msg.plenary !== true) return;\n\n switch (msg.type) {\n case \"init\":\n consoleOrigin.current = e.origin;\n setActive(true);\n send(\n \"manifest\",\n { components: getManifest(), theme: getTheme() } satisfies ManifestPayload,\n e.origin,\n );\n reportSize();\n break;\n case \"render\":\n consoleOrigin.current = e.origin;\n setActive(true);\n // A draft arriving mid-edit means the console changed the document\n // (e.g. toolbar undo); it is authoritative — cancel the session.\n endEdit(false);\n setDraft(msg.payload as RenderPayload);\n break;\n case \"patch\": {\n endEdit(false);\n const p = msg.payload as PatchPayload;\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, p.nodeId, p.props) } };\n });\n break;\n }\n case \"highlight\": {\n const p = msg.payload as HighlightPayload;\n setSelectedId(p.selectedId);\n setHoverId(p.hoverId);\n break;\n }\n }\n };\n\n window.addEventListener(\"message\", onMessage);\n // Announce readiness to every allowed console origin. Exact origins are\n // addressed directly; a wildcard pattern cannot be a postMessage target, so\n // when any is configured the announce also goes out with target \"*\". That is\n // safe for `hello` alone — it carries only version numbers — and everything\n // after it is sent strictly to the matched origin of a validated `init`.\n const hello = { protocolVersion: PROTOCOL_VERSION, sdkVersion };\n for (const origin of editorOrigins) {\n if (!origin.includes(\"*\")) send(\"hello\", hello, origin);\n }\n if (hasWildcardPattern(editorOrigins)) send(\"hello\", hello, \"*\");\n return () => {\n window.removeEventListener(\"message\", onMessage);\n document.removeEventListener(\"keydown\", onKeyDown);\n document.removeEventListener(\"wheel\", onWheelGesture);\n document.removeEventListener(\"pointerdown\", onPanStart);\n document.removeEventListener(\"pointermove\", onPanMove);\n document.removeEventListener(\"pointerup\", onPanEnd);\n document.removeEventListener(\"mousedown\", onMiddleMouseDown);\n observer?.disconnect();\n if (sizeTimer) clearTimeout(sizeTimer);\n };\n }, [editorOrigins, sdkVersion, region, send, endEdit]);\n\n const beginEdit = (nodeId: string, el: HTMLElement) => {\n endEdit(true); // switching targets commits the previous session\n const onBlur = () => endEdit(true);\n el.setAttribute(\"contenteditable\", getEditableMode());\n el.addEventListener(\"blur\", onBlur);\n editSession.current = { nodeId, el, original: el.textContent ?? \"\", onBlur };\n el.focus();\n };\n\n const onPreviewDoubleClick = (e: ReactMouseEvent) => {\n const id = nodeIdFromEvent(e);\n if (!id || !regionDoc) return;\n // v1: inline editing is the built-in text block's `content` — its DOM is\n // predictably marker > element > text node.\n if (findNodeById(regionDoc.root, id)?.component !== \"text\") return;\n const el = (e.target as HTMLElement).closest(\"[data-plenary-id]\")\n ?.firstElementChild as HTMLElement | null;\n if (el) beginEdit(id, el);\n };\n\n const isPaletteDrag = (e: ReactDragEvent) => e.dataTransfer.types.includes(DND_NEW);\n\n const onDragOver = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n setDropHint(resolveDrop(e.clientX, e.clientY));\n };\n const onDrop = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n const component = e.dataTransfer.getData(DND_NEW);\n const { target } = resolveDrop(e.clientX, e.clientY);\n if (component) send(\"canvasDrop\", { component, target } satisfies CanvasDropPayload);\n setDropHint(null);\n };\n const onDragLeave = (e: ReactDragEvent) => {\n // Clear only when the pointer leaves the preview root entirely.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDropHint(null);\n };\n\n if (!active) return <>{children}</>;\n\n return (\n <div\n data-plenary-preview=\"\"\n onClick={(e) => {\n const id = nodeIdFromEvent(e);\n if (id) send(\"select\", { nodeId: id });\n }}\n onDoubleClick={onPreviewDoubleClick}\n onMouseOver={(e) => send(\"hover\", { nodeId: nodeIdFromEvent(e) })}\n onMouseOut={() => send(\"hover\", { nodeId: null })}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onDragLeave={onDragLeave}\n >\n {regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null}\n {regionDoc\n ? videoControlsBadgeIds(regionDoc).map((id) => <VideoControlsBadge key={id} nodeId={id} />)\n : null}\n {selectedId ? <HighlightOverlay nodeId={selectedId} variant=\"selected\" /> : null}\n {hoverId && hoverId !== selectedId ? (\n <HighlightOverlay nodeId={hoverId} variant=\"hover\" />\n ) : null}\n {dropHint ? <DropIndicator hint={dropHint} /> : null}\n </div>\n );\n}\n\n/** The live insertion indicator drawn over the canvas while dragging a component. */\nfunction DropIndicator({ hint }: { hint: DropHint }) {\n if (!hint.rect) return null;\n const { rect, mode } = hint;\n const base: CSSProperties = { position: \"fixed\", pointerEvents: \"none\", zIndex: 2147483647 };\n if (mode === \"into\") {\n return (\n <div\n style={{\n ...base,\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n outline: \"2px dashed #4f46e5\",\n outlineOffset: \"-2px\",\n background: \"rgba(79,70,229,0.06)\",\n }}\n />\n );\n }\n const y = mode === \"before\" ? rect.top : rect.bottom;\n return (\n <div style={{ ...base, left: rect.left, top: y - 1, width: rect.width, height: 2, background: \"#4f46e5\" }} />\n );\n}\n\nfunction sameBox(a: DOMRect | null, b: DOMRect | null): boolean {\n if (!a || !b) return a === b;\n return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;\n}\n\n/**\n * A fixed outline over a highlighted node, driven from the console sidebar. The\n * `selected` variant marks the block being edited (thick, solid, saturated); the\n * `hover` variant marks whatever the cursor is over (thinner, dashed, lighter) so\n * the two read as clearly different states.\n */\nconst OUTLINE_STYLE: Record<\"selected\" | \"hover\", CSSProperties> = {\n selected: { outline: \"2px solid #4f46e5\" },\n hover: { outline: \"1px dashed #818cf8\" },\n};\n\n// An overlay is positioned in viewport coordinates, so it goes stale as soon\n// as the block moves — a scroll, a resize, an image finishing loading, or an\n// edit that reflows the page above it. Re-measuring every frame tracks all of\n// those without enumerating them; state changes only when the box actually\n// does, so a still page costs one measurement per frame and no re-render.\nfunction useNodeRect(nodeId: string): DOMRect | null {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n let frame = 0;\n const measure = () => {\n const marker = document.querySelector(`[data-plenary-id=\"${CSS.escape(nodeId)}\"]`);\n // Markers use display:contents (no box of their own), so measure the child.\n const target = (marker?.firstElementChild as HTMLElement | null) ?? (marker as HTMLElement | null);\n const next = target ? target.getBoundingClientRect() : null;\n setRect((prev) => (sameBox(prev, next) ? prev : next));\n frame = requestAnimationFrame(measure);\n };\n measure();\n return () => cancelAnimationFrame(frame);\n }, [nodeId]);\n\n return rect;\n}\n\nfunction HighlightOverlay({ nodeId, variant }: { nodeId: string; variant: \"selected\" | \"hover\" }) {\n const rect = useNodeRect(nodeId);\n if (!rect) return null;\n return (\n <div\n style={{\n position: \"fixed\",\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n ...OUTLINE_STYLE[variant],\n pointerEvents: \"none\",\n zIndex: 2147483647,\n }}\n />\n );\n}\n\n/**\n * Ids of video nodes whose published player will show native controls. The\n * editor render hides them (a controls-less video lets clicks bubble to\n * selection), so the canvas draws a cosmetic badge instead — the hint that\n * the real page will have a control bar. Fidelity note: the renderer does not\n * merge registration defaults into props, so only an explicit truthy\n * `controls` counts, exactly like the published render. Media-less nodes show\n * the placeholder, which carries its own glyph — as do nodes whose asset was\n * deleted, where the published page renders no player to hint at either.\n */\nfunction videoControlsBadgeIds(doc: PlenaryDocument): string[] {\n const ids: string[] = [];\n const walk = (nodes: PlenaryNode[]): void => {\n for (const node of nodes) {\n const media = node.props?.media as { missing?: boolean } | undefined;\n if (node.component === \"video\" && node.props?.controls && media && !media.missing) {\n ids.push(node.id);\n }\n for (const children of Object.values(node.slots ?? {})) walk(children);\n }\n };\n walk(doc.root);\n return ids;\n}\n\n/**\n * A miniature control-bar chip drawn over a video block's bottom-left corner.\n * Purely cosmetic (pointer-events none) and drawn outside the annotated tree,\n * so node styles and selection behavior are untouched.\n */\nfunction VideoControlsBadge({ nodeId }: { nodeId: string }) {\n const rect = useNodeRect(nodeId);\n // On a tiny player the chip would cover the content it hints at.\n if (!rect || rect.width < 96 || rect.height < 48) return null;\n\n const glyph = (d: string) => (\n <svg key={d} width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d={d} />\n </svg>\n );\n return (\n <div\n data-plenary-video-controls-badge=\"\"\n style={{\n position: \"fixed\",\n left: rect.left + 8,\n top: rect.bottom - 26,\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"4px 9px\",\n borderRadius: 9999,\n background: \"rgba(0,0,0,0.65)\",\n color: \"#fff\",\n pointerEvents: \"none\",\n zIndex: 2147483646, // just under the highlight outlines\n }}\n >\n {glyph(\"M8 5v14l11-7z\") /* play */}\n {glyph(\"M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z\") /* volume */}\n {glyph(\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\") /* fullscreen */}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,14 @@
1
+ import * as react from 'react';
2
+ import { ElementType, CSSProperties, ReactNode } from 'react';
3
+
4
+ interface BoxProps {
5
+ /** Element to render. Defaults to `div`. */
6
+ as?: ElementType;
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ children?: ReactNode;
10
+ }
11
+ /** A generic styleable container with a single default `children` slot. */
12
+ declare function Box({ as, className, style, children }: BoxProps): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
13
+
14
+ export { Box, type BoxProps };
@@ -0,0 +1,8 @@
1
+ import { createElement } from "react";
2
+ function Box({ as = "div", className, style, children }) {
3
+ return createElement(as, { className, style }, children);
4
+ }
5
+ export {
6
+ Box
7
+ };
8
+ //# sourceMappingURL=Box.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Box.tsx"],"sourcesContent":["import { createElement, type CSSProperties, type ElementType, type ReactNode } from \"react\";\n\nexport interface BoxProps {\n /** Element to render. Defaults to `div`. */\n as?: ElementType;\n className?: string;\n style?: CSSProperties;\n children?: ReactNode;\n}\n\n/** A generic styleable container with a single default `children` slot. */\nexport function Box({ as = \"div\", className, style, children }: BoxProps) {\n return createElement(as, { className, style }, children);\n}\n"],"mappings":"AAAA,SAAS,qBAA2E;AAW7E,SAAS,IAAI,EAAE,KAAK,OAAO,WAAW,OAAO,SAAS,GAAa;AACxE,SAAO,cAAc,IAAI,EAAE,WAAW,MAAM,GAAG,QAAQ;AACzD;","names":[]}
@@ -0,0 +1,36 @@
1
+ import * as react from 'react';
2
+ import { CSSProperties } from 'react';
3
+ import { ResolvedForm, FormValue } from '../form.js';
4
+ import '@plenary/client';
5
+
6
+ interface FormProps {
7
+ /** A resolved form value (the renderer resolves it before render). */
8
+ form?: ResolvedForm | FormValue | null;
9
+ showTitle?: boolean;
10
+ submitLabel?: string;
11
+ onSuccess?: "message" | "redirect";
12
+ successMessage?: string;
13
+ redirectUrl?: string;
14
+ className?: string;
15
+ style?: CSSProperties;
16
+ /** True on editor-preview renders; enables the no-form placeholder. */
17
+ editor?: boolean;
18
+ }
19
+ /**
20
+ * Renders a Form Manager form from its resolved definition and submits entries
21
+ * through the provided client (see `PlenaryProvider`). On success it either
22
+ * swaps itself for `successMessage` or navigates to `redirectUrl`.
23
+ *
24
+ * Editor previews render the real fields inert (a disabled fieldset), so the
25
+ * block is selectable on the canvas without being fillable; the bridge also
26
+ * cancels any submit while the console drives the iframe. Without a chosen
27
+ * form it renders a placeholder in the editor and nothing on a published page.
28
+ *
29
+ * The hidden `website` input is the server's reserved honeypot key and the
30
+ * `ts` token (when the definition carries one) is echoed back — both are
31
+ * ignored by servers without anti-spam enabled. The honeypot is skipped in the
32
+ * rare case the form itself defines a `website` field.
33
+ */
34
+ declare function Form({ form, showTitle, submitLabel, onSuccess, successMessage, redirectUrl, className, style, editor, }: FormProps): react.JSX.Element | null;
35
+
36
+ export { Form, type FormProps };