cms-renderer 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,82 +3,101 @@
3
3
 
4
4
  // lib/block-toolbar.tsx
5
5
  import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react";
6
- import { useEffect, useState } from "react";
7
- import { createPortal } from "react-dom";
6
+ import { useLayoutEffect, useRef } from "react";
8
7
  import { jsx, jsxs } from "react/jsx-runtime";
9
- function BlockToolbar({ blockId }) {
10
- const [mounted, setMounted] = useState(false);
11
- useEffect(() => {
12
- setMounted(true);
8
+ function BlockToolbar({ blockId, style }) {
9
+ const toolbarRef = useRef(null);
10
+ useLayoutEffect(() => {
11
+ const el = toolbarRef.current;
12
+ if (!el) return;
13
+ const vw = window.innerWidth;
14
+ const vh = window.innerHeight;
15
+ const MAX_ATTEMPTS = 3;
16
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
17
+ const rect = el.getBoundingClientRect();
18
+ const offRight = rect.right > vw;
19
+ const offLeft = rect.left < 0;
20
+ const offTop = rect.top < 0;
21
+ const offBottom = rect.bottom > vh;
22
+ if (!offRight && !offLeft && !offTop && !offBottom) break;
23
+ if (offRight) {
24
+ el.style.left = `${vw - rect.width}px`;
25
+ el.style.transform = "none";
26
+ } else if (offLeft) {
27
+ el.style.left = "0px";
28
+ el.style.transform = "none";
29
+ }
30
+ if (offTop) {
31
+ el.style.bottom = `${vh - rect.height}px`;
32
+ } else if (offBottom) {
33
+ el.style.bottom = "0px";
34
+ }
35
+ }
13
36
  }, []);
14
37
  const handleAction = (action) => {
15
- if (window.parent && window.parent !== window) {
38
+ if (typeof window !== "undefined" && window.parent && window.parent !== window) {
16
39
  window.parent.postMessage({ type: "cms-block-action", action, blockId }, "*");
17
40
  }
18
41
  };
19
- if (!mounted) return null;
20
- return createPortal(
21
- /* @__PURE__ */ jsxs("div", { className: "cms-block-toolbar", "data-cms-toolbar": "true", "data-block-id": blockId, children: [
22
- /* @__PURE__ */ jsx(
23
- "button",
24
- {
25
- type: "button",
26
- title: "Move up",
27
- "aria-label": "Move up",
28
- "data-action": "move-up",
29
- onClick: (e) => {
30
- e.stopPropagation();
31
- handleAction("move-up");
32
- },
33
- children: /* @__PURE__ */ jsx(ChevronUp, {})
34
- }
35
- ),
36
- /* @__PURE__ */ jsx(
37
- "button",
38
- {
39
- type: "button",
40
- title: "Move down",
41
- "aria-label": "Move down",
42
- "data-action": "move-down",
43
- onClick: (e) => {
44
- e.stopPropagation();
45
- handleAction("move-down");
46
- },
47
- children: /* @__PURE__ */ jsx(ChevronDown, {})
48
- }
49
- ),
50
- /* @__PURE__ */ jsx(
51
- "button",
52
- {
53
- type: "button",
54
- title: "Add block",
55
- "aria-label": "Add block",
56
- "data-action": "add-block",
57
- onClick: (e) => {
58
- e.stopPropagation();
59
- handleAction("add-block");
60
- },
61
- children: /* @__PURE__ */ jsx(Plus, {})
62
- }
63
- ),
64
- /* @__PURE__ */ jsx(
65
- "button",
66
- {
67
- type: "button",
68
- className: "delete",
69
- title: "Delete block",
70
- "aria-label": "Delete block",
71
- "data-action": "delete",
72
- onClick: (e) => {
73
- e.stopPropagation();
74
- handleAction("delete");
75
- },
76
- children: /* @__PURE__ */ jsx(Trash2, {})
77
- }
78
- )
79
- ] }),
80
- document.body
81
- );
42
+ return /* @__PURE__ */ jsxs("div", { ref: toolbarRef, className: "cms-block-toolbar", "data-cms-toolbar": "true", style, children: [
43
+ /* @__PURE__ */ jsx(
44
+ "button",
45
+ {
46
+ type: "button",
47
+ title: "Move up",
48
+ "aria-label": "Move up",
49
+ "data-action": "move-up",
50
+ onClick: (e) => {
51
+ e.stopPropagation();
52
+ handleAction("move-up");
53
+ },
54
+ children: /* @__PURE__ */ jsx(ChevronUp, {})
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ title: "Move down",
62
+ "aria-label": "Move down",
63
+ "data-action": "move-down",
64
+ onClick: (e) => {
65
+ e.stopPropagation();
66
+ handleAction("move-down");
67
+ },
68
+ children: /* @__PURE__ */ jsx(ChevronDown, {})
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx(
72
+ "button",
73
+ {
74
+ type: "button",
75
+ title: "Add block",
76
+ "aria-label": "Add block",
77
+ "data-action": "add-block",
78
+ onClick: (e) => {
79
+ e.stopPropagation();
80
+ handleAction("add-block");
81
+ },
82
+ children: /* @__PURE__ */ jsx(Plus, {})
83
+ }
84
+ ),
85
+ /* @__PURE__ */ jsx(
86
+ "button",
87
+ {
88
+ type: "button",
89
+ className: "delete",
90
+ title: "Delete block",
91
+ "aria-label": "Delete block",
92
+ "data-action": "delete",
93
+ onClick: (e) => {
94
+ e.stopPropagation();
95
+ handleAction("delete");
96
+ },
97
+ children: /* @__PURE__ */ jsx(Trash2, {})
98
+ }
99
+ )
100
+ ] });
82
101
  }
83
102
  export {
84
103
  BlockToolbar
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * Portals to document.body so it never participates in the page layout.\n * Shown/positioned by the inline CMS script via data-block-id lookup.\n */\n\nexport function BlockToolbar({ blockId }: { blockId: string }) {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const handleAction = (action: string) => {\n if (window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n if (!mounted) return null;\n\n return createPortal(\n <div className=\"cms-block-toolbar\" data-cms-toolbar=\"true\" data-block-id={blockId}>\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>,\n document.body\n );\n}\n"],"mappings":";;;;AAEA,SAAS,aAAa,WAAW,MAAM,cAAc;AACrD,SAAS,WAAW,gBAAgB;AACpC,SAAS,oBAAoB;AA0BzB,SAWI,KAXJ;AAhBG,SAAS,aAAa,EAAE,QAAQ,GAAwB;AAC7D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAE5C,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,CAAC,WAAmB;AACvC,QAAI,OAAO,UAAU,OAAO,WAAW,QAAQ;AAC7C,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,MAAI,CAAC,QAAS,QAAO;AAErB,SAAO;AAAA,IACL,qBAAC,SAAI,WAAU,qBAAoB,oBAAiB,QAAO,iBAAe,SACxE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAM;AAAA,UACN,cAAW;AAAA,UACX,eAAY;AAAA,UACZ,SAAS,CAAC,MAAM;AACd,cAAE,gBAAgB;AAClB,yBAAa,SAAS;AAAA,UACxB;AAAA,UAEA,8BAAC,aAAU;AAAA;AAAA,MACb;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAM;AAAA,UACN,cAAW;AAAA,UACX,eAAY;AAAA,UACZ,SAAS,CAAC,MAAM;AACd,cAAE,gBAAgB;AAClB,yBAAa,WAAW;AAAA,UAC1B;AAAA,UAEA,8BAAC,eAAY;AAAA;AAAA,MACf;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAM;AAAA,UACN,cAAW;AAAA,UACX,eAAY;AAAA,UACZ,SAAS,CAAC,MAAM;AACd,cAAE,gBAAgB;AAClB,yBAAa,WAAW;AAAA,UAC1B;AAAA,UAEA,8BAAC,QAAK;AAAA;AAAA,MACR;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,OAAM;AAAA,UACN,cAAW;AAAA,UACX,eAAY;AAAA,UACZ,SAAS,CAAC,MAAM;AACd,cAAE,gBAAgB;AAClB,yBAAa,QAAQ;AAAA,UACvB;AAAA,UAEA,8BAAC,UAAO;AAAA;AAAA,MACV;AAAA,OACF;AAAA,IACA,SAAS;AAAA,EACX;AACF;","names":[]}
1
+ {"version":3,"sources":["../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport type React from 'react';\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId, style }: { blockId: string; style?: React.CSSProperties }) {\n const toolbarRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = toolbarRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const MAX_ATTEMPTS = 3;\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const rect = el.getBoundingClientRect();\n\n const offRight = rect.right > vw;\n const offLeft = rect.left < 0;\n const offTop = rect.top < 0;\n const offBottom = rect.bottom > vh;\n\n if (!offRight && !offLeft && !offTop && !offBottom) break;\n\n // Horizontal: remove the centering transform and pin to the edge that's clipped\n if (offRight) {\n el.style.left = `${vw - rect.width}px`;\n el.style.transform = 'none';\n } else if (offLeft) {\n el.style.left = '0px';\n el.style.transform = 'none';\n }\n\n // Vertical: toolbar uses `bottom` (distance from viewport bottom), so\n // top < 0 means bottom is too large — clamp it so top aligns to viewport edge\n if (offTop) {\n el.style.bottom = `${vh - rect.height}px`;\n } else if (offBottom) {\n el.style.bottom = '0px';\n }\n }\n }, []);\n\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div ref={toolbarRef} className=\"cms-block-toolbar\" data-cms-toolbar=\"true\" style={style}>\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n}\n"],"mappings":";;;;AAEA,SAAS,aAAa,WAAW,MAAM,cAAc;AAErD,SAAS,iBAAiB,cAAc;AAwDpC,SAWI,KAXJ;AA/CG,SAAS,aAAa,EAAE,SAAS,MAAM,GAAqD;AACjG,QAAM,aAAa,OAAuB,IAAI;AAE9C,kBAAgB,MAAM;AACpB,UAAM,KAAK,WAAW;AACtB,QAAI,CAAC,GAAI;AAET,UAAM,KAAK,OAAO;AAClB,UAAM,KAAK,OAAO;AAClB,UAAM,eAAe;AAErB,aAAS,UAAU,GAAG,UAAU,cAAc,WAAW;AACvD,YAAM,OAAO,GAAG,sBAAsB;AAEtC,YAAM,WAAW,KAAK,QAAQ;AAC9B,YAAM,UAAU,KAAK,OAAO;AAC5B,YAAM,SAAS,KAAK,MAAM;AAC1B,YAAM,YAAY,KAAK,SAAS;AAEhC,UAAI,CAAC,YAAY,CAAC,WAAW,CAAC,UAAU,CAAC,UAAW;AAGpD,UAAI,UAAU;AACZ,WAAG,MAAM,OAAO,GAAG,KAAK,KAAK,KAAK;AAClC,WAAG,MAAM,YAAY;AAAA,MACvB,WAAW,SAAS;AAClB,WAAG,MAAM,OAAO;AAChB,WAAG,MAAM,YAAY;AAAA,MACvB;AAIA,UAAI,QAAQ;AACV,WAAG,MAAM,SAAS,GAAG,KAAK,KAAK,MAAM;AAAA,MACvC,WAAW,WAAW;AACpB,WAAG,MAAM,SAAS;AAAA,MACpB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,eAAe,CAAC,WAAmB;AACvC,QAAI,OAAO,WAAW,eAAe,OAAO,UAAU,OAAO,WAAW,QAAQ;AAC9E,aAAO,OAAO,YAAY,EAAE,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,KAAK,YAAY,WAAU,qBAAoB,oBAAiB,QAAO,OAC1E;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,SAAS;AAAA,QACxB;AAAA,QAEA,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,WAAW;AAAA,QAC1B;AAAA,QAEA,8BAAC,eAAY;AAAA;AAAA,IACf;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,WAAW;AAAA,QAC1B;AAAA,QAEA,8BAAC,QAAK;AAAA;AAAA,IACR;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QACZ,SAAS,CAAC,MAAM;AACd,YAAE,gBAAgB;AAClB,uBAAa,QAAQ;AAAA,QACvB;AAAA,QAEA,8BAAC,UAAO;AAAA;AAAA,IACV;AAAA,KACF;AAEJ;","names":[]}
@@ -1,4 +1,4 @@
1
- import * as react from 'react';
1
+ import * as React$1 from 'react';
2
2
 
3
3
  interface ContentEntry {
4
4
  v: string;
@@ -9,19 +9,20 @@ interface ClientEditableBlockProps {
9
9
  blockType: string;
10
10
  contentEntries: ContentEntry[];
11
11
  children: React.ReactNode;
12
- blockProps?: Record<string, string>;
13
12
  }
14
13
  /**
15
- * Client-side span injector for blocks whose components use React hooks
16
- * and therefore can't be walked server-side.
14
+ * Client-side block editable wrapper.
17
15
  *
18
- * Renders a hidden sentinel element, then locates the block's actual root DOM
19
- * element via nextElementSibling no wrapper div is added to the tree.
16
+ * Renders a hidden sentinel span followed by the block's children (no wrapper div).
17
+ * On mount, finds the component's root element via nextElementSibling and:
18
+ * - Stamps data-cms-block, data-block-id, data-block-type directly on it
19
+ * - Injects data-cms-editable spans around matching text nodes
20
+ * - Portals the BlockToolbar into document.body with position:fixed so it is
21
+ * never clipped by overflow:hidden or stacking contexts on the block itself
20
22
  *
21
- * Uses a MutationObserver to re-inject [data-cms-editable] spans after every
22
- * React commit (including state-driven re-renders like animations), so the
23
- * overlays survive React's reconciliation.
23
+ * Uses a MutationObserver to re-inject spans after every DOM mutation
24
+ * (e.g. animated state changes that swap visible elements).
24
25
  */
25
- declare function ClientEditableBlock({ blockId, blockType, contentEntries, children, blockProps, }: ClientEditableBlockProps): react.JSX.Element;
26
+ declare function ClientEditableBlock({ blockId, blockType, contentEntries, children, }: ClientEditableBlockProps): React$1.JSX.Element;
26
27
 
27
28
  export { ClientEditableBlock };
@@ -2,30 +2,230 @@
2
2
  "use client";
3
3
 
4
4
  // lib/client-editable-block.tsx
5
- import { useCallback, useLayoutEffect, useRef } from "react";
6
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { useCallback, useLayoutEffect as useLayoutEffect3, useRef as useRef3, useState } from "react";
6
+ import { createPortal } from "react-dom";
7
+
8
+ // lib/block-outline.tsx
9
+ import { useLayoutEffect, useRef } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ function BlockOutline({ blockRect }) {
12
+ const outlineRef = useRef(null);
13
+ useLayoutEffect(() => {
14
+ const el = outlineRef.current;
15
+ if (!el) return;
16
+ const vw = window.innerWidth;
17
+ const vh = window.innerHeight;
18
+ const offsets = [4, 0, -2];
19
+ for (const offset of offsets) {
20
+ const pad = offset + 2;
21
+ el.style.top = `${blockRect.top - pad}px`;
22
+ el.style.left = `${blockRect.left - pad}px`;
23
+ el.style.width = `${blockRect.width + pad * 2}px`;
24
+ el.style.height = `${blockRect.height + pad * 2}px`;
25
+ const rect = el.getBoundingClientRect();
26
+ if (rect.top >= 0 && rect.left >= 0 && rect.right <= vw && rect.bottom <= vh) break;
27
+ }
28
+ }, [blockRect]);
29
+ return /* @__PURE__ */ jsx(
30
+ "div",
31
+ {
32
+ ref: outlineRef,
33
+ "data-cms-outline": "true",
34
+ style: {
35
+ position: "fixed",
36
+ border: "2px solid #3b82f6",
37
+ borderRadius: "2px",
38
+ pointerEvents: "none",
39
+ zIndex: 99998,
40
+ boxSizing: "border-box"
41
+ }
42
+ }
43
+ );
44
+ }
45
+
46
+ // lib/block-toolbar.tsx
47
+ import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react";
48
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef2 } from "react";
49
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
50
+ function BlockToolbar({ blockId, style }) {
51
+ const toolbarRef = useRef2(null);
52
+ useLayoutEffect2(() => {
53
+ const el = toolbarRef.current;
54
+ if (!el) return;
55
+ const vw = window.innerWidth;
56
+ const vh = window.innerHeight;
57
+ const MAX_ATTEMPTS = 3;
58
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
59
+ const rect = el.getBoundingClientRect();
60
+ const offRight = rect.right > vw;
61
+ const offLeft = rect.left < 0;
62
+ const offTop = rect.top < 0;
63
+ const offBottom = rect.bottom > vh;
64
+ if (!offRight && !offLeft && !offTop && !offBottom) break;
65
+ if (offRight) {
66
+ el.style.left = `${vw - rect.width}px`;
67
+ el.style.transform = "none";
68
+ } else if (offLeft) {
69
+ el.style.left = "0px";
70
+ el.style.transform = "none";
71
+ }
72
+ if (offTop) {
73
+ el.style.bottom = `${vh - rect.height}px`;
74
+ } else if (offBottom) {
75
+ el.style.bottom = "0px";
76
+ }
77
+ }
78
+ }, []);
79
+ const handleAction = (action) => {
80
+ if (typeof window !== "undefined" && window.parent && window.parent !== window) {
81
+ window.parent.postMessage({ type: "cms-block-action", action, blockId }, "*");
82
+ }
83
+ };
84
+ return /* @__PURE__ */ jsxs("div", { ref: toolbarRef, className: "cms-block-toolbar", "data-cms-toolbar": "true", style, children: [
85
+ /* @__PURE__ */ jsx2(
86
+ "button",
87
+ {
88
+ type: "button",
89
+ title: "Move up",
90
+ "aria-label": "Move up",
91
+ "data-action": "move-up",
92
+ onClick: (e) => {
93
+ e.stopPropagation();
94
+ handleAction("move-up");
95
+ },
96
+ children: /* @__PURE__ */ jsx2(ChevronUp, {})
97
+ }
98
+ ),
99
+ /* @__PURE__ */ jsx2(
100
+ "button",
101
+ {
102
+ type: "button",
103
+ title: "Move down",
104
+ "aria-label": "Move down",
105
+ "data-action": "move-down",
106
+ onClick: (e) => {
107
+ e.stopPropagation();
108
+ handleAction("move-down");
109
+ },
110
+ children: /* @__PURE__ */ jsx2(ChevronDown, {})
111
+ }
112
+ ),
113
+ /* @__PURE__ */ jsx2(
114
+ "button",
115
+ {
116
+ type: "button",
117
+ title: "Add block",
118
+ "aria-label": "Add block",
119
+ "data-action": "add-block",
120
+ onClick: (e) => {
121
+ e.stopPropagation();
122
+ handleAction("add-block");
123
+ },
124
+ children: /* @__PURE__ */ jsx2(Plus, {})
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsx2(
128
+ "button",
129
+ {
130
+ type: "button",
131
+ className: "delete",
132
+ title: "Delete block",
133
+ "aria-label": "Delete block",
134
+ "data-action": "delete",
135
+ onClick: (e) => {
136
+ e.stopPropagation();
137
+ handleAction("delete");
138
+ },
139
+ children: /* @__PURE__ */ jsx2(Trash2, {})
140
+ }
141
+ )
142
+ ] });
143
+ }
144
+
145
+ // lib/client-editable-block.tsx
146
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
147
+ var CMS_STYLES = `
148
+ [data-cms-editable] { cursor: pointer; border-radius: 2px; }
149
+ [data-cms-editable]:hover { outline: 2px solid #3b82f6; outline-offset: 2px; }
150
+ .cms-block-toolbar {
151
+ position: fixed;
152
+ display: flex; gap: 4px; background: #1f2937; border-radius: 6px; padding: 4px;
153
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
154
+ transition: opacity 0.15s ease; z-index: 99999;
155
+ pointer-events: auto;
156
+ }
157
+ .cms-block-toolbar button {
158
+ display: flex; align-items: center; justify-content: center;
159
+ width: 28px; height: 28px; border: none; background: transparent;
160
+ color: #9ca3af; border-radius: 4px; cursor: pointer;
161
+ transition: background 0.15s ease, color 0.15s ease;
162
+ }
163
+ .cms-block-toolbar button:hover { background: #374151; color: #fff; }
164
+ .cms-block-toolbar button.delete:hover { background: #dc2626; color: #fff; }
165
+ .cms-block-toolbar button:disabled { opacity: 0.4; cursor: not-allowed; }
166
+ .cms-block-toolbar button:disabled:hover { background: transparent; color: #9ca3af; }
167
+ .cms-block-toolbar svg { width: 16px; height: 16px; }
168
+ `;
169
+ var cmsGlobalInjected = false;
170
+ function ensureCmsGlobals() {
171
+ if (cmsGlobalInjected) return;
172
+ cmsGlobalInjected = true;
173
+ const style = document.createElement("style");
174
+ style.setAttribute("data-cms", "");
175
+ style.textContent = CMS_STYLES;
176
+ document.head.appendChild(style);
177
+ if (!window.__cmsEditableInitialized) {
178
+ window.__cmsEditableInitialized = true;
179
+ document.addEventListener("click", (e) => {
180
+ const target = e.target;
181
+ if (target.closest(".cms-block-toolbar")) return;
182
+ const editable = target.closest("[data-cms-editable]");
183
+ if (editable) {
184
+ const msg = {
185
+ type: "cms-editable-click",
186
+ blockId: editable.getAttribute("data-block-id"),
187
+ blockType: editable.getAttribute("data-block-type"),
188
+ contentPath: editable.getAttribute("data-content-path")
189
+ };
190
+ if (window.parent && window.parent !== window) window.parent.postMessage(msg, "*");
191
+ return;
192
+ }
193
+ const block = target.closest("[data-cms-block]");
194
+ if (block) {
195
+ const msg = {
196
+ type: "cms-editable-click",
197
+ blockId: block.getAttribute("data-block-id"),
198
+ blockType: block.getAttribute("data-block-type"),
199
+ contentPath: null
200
+ };
201
+ if (window.parent && window.parent !== window) window.parent.postMessage(msg, "*");
202
+ }
203
+ });
204
+ }
205
+ }
7
206
  function ClientEditableBlock({
8
207
  blockId,
9
208
  blockType,
10
209
  contentEntries,
11
- children,
12
- blockProps
210
+ children
13
211
  }) {
14
- const sentinelRef = useRef(null);
15
- const observerRef = useRef(null);
16
- const isInjectingRef = useRef(false);
212
+ const sentinelRef = useRef3(null);
213
+ const observerRef = useRef3(null);
214
+ const isInjectingRef = useRef3(false);
215
+ const [toolbarStyle, setToolbarStyle] = useState(null);
216
+ const [outlineRect, setOutlineRect] = useState(null);
17
217
  const getBlockRoot = useCallback(() => {
18
218
  return sentinelRef.current?.nextElementSibling ?? null;
19
219
  }, []);
20
220
  const injectSpans = useCallback(() => {
21
221
  if (isInjectingRef.current) return;
22
- const container = getBlockRoot();
23
- if (!container) return;
222
+ const blockRoot = getBlockRoot();
223
+ if (!blockRoot) return;
24
224
  isInjectingRef.current = true;
25
225
  observerRef.current?.disconnect();
26
226
  try {
27
227
  const existing = Array.from(
28
- container.querySelectorAll("span[data-cms-editable][data-content-path]")
228
+ blockRoot.querySelectorAll("span[data-cms-editable][data-content-path]")
29
229
  );
30
230
  for (const span of existing) {
31
231
  const parent = span.parentNode;
@@ -34,7 +234,7 @@ function ClientEditableBlock({
34
234
  parent.removeChild(span);
35
235
  }
36
236
  const used = /* @__PURE__ */ new Set();
37
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
237
+ const walker = document.createTreeWalker(blockRoot, NodeFilter.SHOW_TEXT, null);
38
238
  const textNodes = [];
39
239
  let n = walker.nextNode();
40
240
  while (n !== null) {
@@ -66,9 +266,9 @@ function ClientEditableBlock({
66
266
  }
67
267
  } finally {
68
268
  isInjectingRef.current = false;
69
- const blockRoot = getBlockRoot();
70
- if (blockRoot && observerRef.current) {
71
- observerRef.current.observe(blockRoot, {
269
+ const root = getBlockRoot();
270
+ if (root && observerRef.current) {
271
+ observerRef.current.observe(root, {
72
272
  childList: true,
73
273
  subtree: true,
74
274
  characterData: true
@@ -76,31 +276,48 @@ function ClientEditableBlock({
76
276
  }
77
277
  }
78
278
  }, [blockId, blockType, contentEntries, getBlockRoot]);
79
- useLayoutEffect(() => {
279
+ useLayoutEffect3(() => {
280
+ ensureCmsGlobals();
80
281
  const blockRoot = getBlockRoot();
81
- if (blockRoot && blockProps) {
82
- for (const [key, value] of Object.entries(blockProps)) {
83
- blockRoot.setAttribute(key, value);
84
- }
85
- }
282
+ if (!blockRoot) return;
283
+ blockRoot.setAttribute("data-cms-block", "");
284
+ blockRoot.setAttribute("data-block-id", blockId);
285
+ blockRoot.setAttribute("data-block-type", blockType);
286
+ const showToolbar = () => {
287
+ const rect = blockRoot.getBoundingClientRect();
288
+ setToolbarStyle({
289
+ bottom: window.innerHeight - rect.bottom + 8,
290
+ left: rect.left + rect.width / 2,
291
+ transform: "translateX(-50%)"
292
+ });
293
+ setOutlineRect(rect);
294
+ };
295
+ const hideToolbar = () => {
296
+ setToolbarStyle(null);
297
+ setOutlineRect(null);
298
+ };
299
+ blockRoot.addEventListener("mouseenter", showToolbar);
300
+ blockRoot.addEventListener("mouseleave", hideToolbar);
86
301
  injectSpans();
87
302
  const observer = new MutationObserver(injectSpans);
88
303
  observerRef.current = observer;
89
- if (blockRoot) {
90
- observer.observe(blockRoot, {
91
- childList: true,
92
- subtree: true,
93
- characterData: true
94
- });
95
- }
304
+ observer.observe(blockRoot, {
305
+ childList: true,
306
+ subtree: true,
307
+ characterData: true
308
+ });
96
309
  return () => {
97
310
  observer.disconnect();
98
311
  observerRef.current = null;
312
+ blockRoot.removeEventListener("mouseenter", showToolbar);
313
+ blockRoot.removeEventListener("mouseleave", hideToolbar);
99
314
  };
100
- }, [injectSpans, getBlockRoot, blockProps]);
101
- return /* @__PURE__ */ jsxs(Fragment, { children: [
102
- /* @__PURE__ */ jsx("span", { ref: sentinelRef, style: { display: "none" }, "aria-hidden": "true" }),
103
- children
315
+ }, [injectSpans, blockId, blockType, getBlockRoot]);
316
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
317
+ /* @__PURE__ */ jsx3("span", { ref: sentinelRef, style: { display: "none" }, "aria-hidden": true }),
318
+ children,
319
+ outlineRect && createPortal(/* @__PURE__ */ jsx3(BlockOutline, { blockRect: outlineRect }), document.body),
320
+ toolbarStyle && createPortal(/* @__PURE__ */ jsx3(BlockToolbar, { blockId, style: toolbarStyle }), document.body)
104
321
  ] });
105
322
  }
106
323
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/client-editable-block.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useLayoutEffect, useRef } from 'react';\n\ninterface ContentEntry {\n v: string;\n p: string;\n}\n\ninterface ClientEditableBlockProps {\n blockId: string;\n blockType: string;\n contentEntries: ContentEntry[];\n children: React.ReactNode;\n blockProps?: Record<string, string>;\n}\n\n/**\n * Client-side span injector for blocks whose components use React hooks\n * and therefore can't be walked server-side.\n *\n * Renders a hidden sentinel element, then locates the block's actual root DOM\n * element via nextElementSibling — no wrapper div is added to the tree.\n *\n * Uses a MutationObserver to re-inject [data-cms-editable] spans after every\n * React commit (including state-driven re-renders like animations), so the\n * overlays survive React's reconciliation.\n */\nexport function ClientEditableBlock({\n blockId,\n blockType,\n contentEntries,\n children,\n blockProps,\n}: ClientEditableBlockProps) {\n const sentinelRef = useRef<HTMLSpanElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n\n const getBlockRoot = useCallback((): Element | null => {\n return sentinelRef.current?.nextElementSibling ?? null;\n }, []);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const container = getBlockRoot();\n if (!container) return;\n\n isInjectingRef.current = true;\n // Disconnect observer while we mutate the DOM to avoid re-entrant calls.\n observerRef.current?.disconnect();\n\n try {\n // Remove previously injected text-level spans, restoring the original text nodes.\n const existing = Array.from(\n container.querySelectorAll<HTMLElement>('span[data-cms-editable][data-content-path]')\n );\n for (const span of existing) {\n const parent = span.parentNode;\n if (!parent) continue;\n while (span.firstChild) parent.insertBefore(span.firstChild, span);\n parent.removeChild(span);\n }\n\n // Walk all visible text nodes and wrap the ones that match content values.\n const used = new Set<string>();\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);\n const textNodes: Text[] = [];\n let n = walker.nextNode();\n while (n !== null) {\n const textNode = n as Text;\n if (\n textNode.parentElement?.closest('.cms-block-toolbar') == null &&\n textNode.nodeValue?.trim()\n ) {\n textNodes.push(textNode);\n }\n n = walker.nextNode();\n }\n\n for (const textNode of textNodes) {\n // Never inject spans inside SVG — <span> is not valid SVG content\n if (textNode.parentElement?.closest('svg') != null) continue;\n\n const text = textNode.nodeValue;\n if (!text) continue;\n for (const entry of contentEntries) {\n if (used.has(entry.p)) continue;\n if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {\n used.add(entry.p);\n const span = document.createElement('span');\n // Inline style ensures layout-transparency before the stylesheet cascade applies\n span.style.display = 'contents';\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', entry.p);\n textNode.parentNode?.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n } finally {\n isInjectingRef.current = false;\n // Reconnect after our mutations are done.\n const blockRoot = getBlockRoot();\n if (blockRoot && observerRef.current) {\n observerRef.current.observe(blockRoot, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries, getBlockRoot]);\n\n useLayoutEffect(() => {\n const blockRoot = getBlockRoot();\n\n // Stamp block data attributes directly onto the component's root DOM element.\n if (blockRoot && blockProps) {\n for (const [key, value] of Object.entries(blockProps)) {\n blockRoot.setAttribute(key, value);\n }\n }\n\n // Initial injection after React's first commit.\n injectSpans();\n\n // Re-inject whenever the child component mutates the DOM\n // (e.g. animated state changes that swap visible elements).\n const observer = new MutationObserver(injectSpans);\n observerRef.current = observer;\n\n if (blockRoot) {\n observer.observe(blockRoot, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n };\n }, [injectSpans, getBlockRoot, blockProps]);\n\n return (\n <>\n {/* Sentinel: zero-size, invisible anchor used only to locate the block's root element via nextElementSibling */}\n <span ref={sentinelRef} style={{ display: 'none' }} aria-hidden=\"true\" />\n {children}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/client-editable-block.tsx","../../lib/block-outline.tsx","../../lib/block-toolbar.tsx"],"sourcesContent":["'use client';\n\nimport { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { BlockOutline } from './block-outline';\nimport { BlockToolbar } from './block-toolbar';\n\n// ---------------------------------------------------------------------------\n// Global styles + click-routing — injected once into <head> on first mount.\n// We do this programmatically because Next.js RSC marks body-level <style> and\n// <script> tags as inactive (they appear greyed-out in DevTools and are not\n// applied by the browser).\n// ---------------------------------------------------------------------------\n\nconst CMS_STYLES = `\n [data-cms-editable] { cursor: pointer; border-radius: 2px; }\n [data-cms-editable]:hover { outline: 2px solid #3b82f6; outline-offset: 2px; }\n .cms-block-toolbar {\n position: fixed;\n display: flex; gap: 4px; background: #1f2937; border-radius: 6px; padding: 4px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n transition: opacity 0.15s ease; z-index: 99999;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex; align-items: center; justify-content: center;\n width: 28px; height: 28px; border: none; background: transparent;\n color: #9ca3af; border-radius: 4px; cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover { background: #374151; color: #fff; }\n .cms-block-toolbar button.delete:hover { background: #dc2626; color: #fff; }\n .cms-block-toolbar button:disabled { opacity: 0.4; cursor: not-allowed; }\n .cms-block-toolbar button:disabled:hover { background: transparent; color: #9ca3af; }\n .cms-block-toolbar svg { width: 16px; height: 16px; }\n`;\n\nlet cmsGlobalInjected = false;\n\nfunction ensureCmsGlobals() {\n if (cmsGlobalInjected) return;\n cmsGlobalInjected = true;\n\n const style = document.createElement('style');\n style.setAttribute('data-cms', '');\n style.textContent = CMS_STYLES;\n document.head.appendChild(style);\n\n if (!(window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized) {\n (window as Window & { __cmsEditableInitialized?: boolean }).__cmsEditableInitialized = true;\n document.addEventListener('click', (e) => {\n const target = e.target as Element;\n if (target.closest('.cms-block-toolbar')) return;\n\n const editable = target.closest('[data-cms-editable]');\n if (editable) {\n const msg = {\n type: 'cms-editable-click',\n blockId: editable.getAttribute('data-block-id'),\n blockType: editable.getAttribute('data-block-type'),\n contentPath: editable.getAttribute('data-content-path'),\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n return;\n }\n\n const block = target.closest('[data-cms-block]');\n if (block) {\n const msg = {\n type: 'cms-editable-click',\n blockId: block.getAttribute('data-block-id'),\n blockType: block.getAttribute('data-block-type'),\n contentPath: null,\n };\n if (window.parent && window.parent !== window) window.parent.postMessage(msg, '*');\n }\n });\n }\n}\n\ninterface ContentEntry {\n v: string;\n p: string;\n}\n\ninterface ClientEditableBlockProps {\n blockId: string;\n blockType: string;\n contentEntries: ContentEntry[];\n children: React.ReactNode;\n}\n\n/**\n * Client-side block editable wrapper.\n *\n * Renders a hidden sentinel span followed by the block's children (no wrapper div).\n * On mount, finds the component's root element via nextElementSibling and:\n * - Stamps data-cms-block, data-block-id, data-block-type directly on it\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into document.body with position:fixed so it is\n * never clipped by overflow:hidden or stacking contexts on the block itself\n *\n * Uses a MutationObserver to re-inject spans after every DOM mutation\n * (e.g. animated state changes that swap visible elements).\n */\nexport function ClientEditableBlock({\n blockId,\n blockType,\n contentEntries,\n children,\n}: ClientEditableBlockProps) {\n const sentinelRef = useRef<HTMLSpanElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n const [toolbarStyle, setToolbarStyle] = useState<React.CSSProperties | null>(null);\n const [outlineRect, setOutlineRect] = useState<DOMRect | null>(null);\n\n const getBlockRoot = useCallback((): HTMLElement | null => {\n return (sentinelRef.current?.nextElementSibling as HTMLElement) ?? null;\n }, []);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n isInjectingRef.current = true;\n // Disconnect observer while we mutate the DOM to avoid re-entrant calls.\n observerRef.current?.disconnect();\n\n try {\n // Remove previously injected text-level spans, restoring the original text nodes.\n const existing = Array.from(\n blockRoot.querySelectorAll<HTMLElement>('span[data-cms-editable][data-content-path]')\n );\n for (const span of existing) {\n const parent = span.parentNode;\n if (!parent) continue;\n while (span.firstChild) parent.insertBefore(span.firstChild, span);\n parent.removeChild(span);\n }\n\n // Walk all visible text nodes and wrap the ones that match content values.\n const used = new Set<string>();\n const walker = document.createTreeWalker(blockRoot, NodeFilter.SHOW_TEXT, null);\n const textNodes: Text[] = [];\n let n = walker.nextNode();\n while (n !== null) {\n const textNode = n as Text;\n if (\n textNode.parentElement?.closest('.cms-block-toolbar') == null &&\n textNode.nodeValue?.trim()\n ) {\n textNodes.push(textNode);\n }\n n = walker.nextNode();\n }\n\n for (const textNode of textNodes) {\n // Never inject spans inside SVG — <span> is not valid SVG content\n if (textNode.parentElement?.closest('svg') != null) continue;\n\n const text = textNode.nodeValue;\n if (!text) continue;\n for (const entry of contentEntries) {\n if (used.has(entry.p)) continue;\n if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {\n used.add(entry.p);\n const span = document.createElement('span');\n // Inline style ensures layout-transparency before the stylesheet cascade applies\n span.style.display = 'contents';\n span.setAttribute('data-cms-editable', '');\n span.setAttribute('data-block-id', blockId);\n span.setAttribute('data-block-type', blockType);\n span.setAttribute('data-content-path', entry.p);\n textNode.parentNode?.insertBefore(span, textNode);\n span.appendChild(textNode);\n break;\n }\n }\n }\n } finally {\n isInjectingRef.current = false;\n // Reconnect after our mutations are done.\n const root = getBlockRoot();\n if (root && observerRef.current) {\n observerRef.current.observe(root, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries, getBlockRoot]);\n\n useLayoutEffect(() => {\n ensureCmsGlobals();\n\n const blockRoot = getBlockRoot();\n if (!blockRoot) return;\n\n // Stamp CMS attributes directly on the component's root element.\n blockRoot.setAttribute('data-cms-block', '');\n blockRoot.setAttribute('data-block-id', blockId);\n blockRoot.setAttribute('data-block-type', blockType);\n\n // Toolbar: portal to document.body with position:fixed so it escapes\n // any overflow:hidden or stacking context on the block or its ancestors.\n const showToolbar = () => {\n const rect = blockRoot.getBoundingClientRect();\n setToolbarStyle({\n bottom: window.innerHeight - rect.bottom + 8,\n left: rect.left + rect.width / 2,\n transform: 'translateX(-50%)',\n });\n setOutlineRect(rect);\n };\n const hideToolbar = () => {\n setToolbarStyle(null);\n setOutlineRect(null);\n };\n\n blockRoot.addEventListener('mouseenter', showToolbar);\n blockRoot.addEventListener('mouseleave', hideToolbar);\n\n // Initial span injection after React's first commit.\n injectSpans();\n\n // Re-inject whenever the child component mutates the DOM.\n const observer = new MutationObserver(injectSpans);\n observerRef.current = observer;\n observer.observe(blockRoot, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n blockRoot.removeEventListener('mouseenter', showToolbar);\n blockRoot.removeEventListener('mouseleave', hideToolbar);\n };\n }, [injectSpans, blockId, blockType, getBlockRoot]);\n\n return (\n <>\n <span ref={sentinelRef} style={{ display: 'none' }} aria-hidden />\n {children}\n {outlineRect && createPortal(<BlockOutline blockRect={outlineRect} />, document.body)}\n {toolbarStyle &&\n createPortal(<BlockToolbar blockId={blockId} style={toolbarStyle} />, document.body)}\n </>\n );\n}\n","'use client';\n\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Outline Component\n *\n * Renders a position:fixed border overlay around a hovered block.\n * Tries up to 3 offset values (4px → 0px → -2px inset) to keep every\n * edge of the border visible inside the viewport before giving up.\n */\nexport function BlockOutline({ blockRect }: { blockRect: DOMRect }) {\n const outlineRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = outlineRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Attempt progressively smaller offsets until the border is fully visible.\n // offset=4 → normal outline-offset feel\n // offset=0 → border flush with block edge\n // offset=-2 → border drawn inside the block (inset)\n const offsets = [4, 0, -2];\n\n for (const offset of offsets) {\n const pad = offset + 2; // 2px border + offset\n el.style.top = `${blockRect.top - pad}px`;\n el.style.left = `${blockRect.left - pad}px`;\n el.style.width = `${blockRect.width + pad * 2}px`;\n el.style.height = `${blockRect.height + pad * 2}px`;\n\n const rect = el.getBoundingClientRect();\n if (rect.top >= 0 && rect.left >= 0 && rect.right <= vw && rect.bottom <= vh) break;\n }\n }, [blockRect]);\n\n return (\n <div\n ref={outlineRef}\n data-cms-outline=\"true\"\n style={{\n position: 'fixed',\n border: '2px solid #3b82f6',\n borderRadius: '2px',\n pointerEvents: 'none',\n zIndex: 99998,\n boxSizing: 'border-box',\n }}\n />\n );\n}\n","'use client';\n\nimport { ChevronDown, ChevronUp, Plus, Trash2 } from 'lucide-react';\nimport type React from 'react';\nimport { useLayoutEffect, useRef } from 'react';\n\n/**\n * Block Toolbar Component\n *\n * Provides move up/down and delete controls for blocks in edit mode.\n * This is a Client Component because it requires onClick handlers.\n */\n\nexport function BlockToolbar({ blockId, style }: { blockId: string; style?: React.CSSProperties }) {\n const toolbarRef = useRef<HTMLDivElement>(null);\n\n useLayoutEffect(() => {\n const el = toolbarRef.current;\n if (!el) return;\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const MAX_ATTEMPTS = 3;\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n const rect = el.getBoundingClientRect();\n\n const offRight = rect.right > vw;\n const offLeft = rect.left < 0;\n const offTop = rect.top < 0;\n const offBottom = rect.bottom > vh;\n\n if (!offRight && !offLeft && !offTop && !offBottom) break;\n\n // Horizontal: remove the centering transform and pin to the edge that's clipped\n if (offRight) {\n el.style.left = `${vw - rect.width}px`;\n el.style.transform = 'none';\n } else if (offLeft) {\n el.style.left = '0px';\n el.style.transform = 'none';\n }\n\n // Vertical: toolbar uses `bottom` (distance from viewport bottom), so\n // top < 0 means bottom is too large — clamp it so top aligns to viewport edge\n if (offTop) {\n el.style.bottom = `${vh - rect.height}px`;\n } else if (offBottom) {\n el.style.bottom = '0px';\n }\n }\n }, []);\n\n const handleAction = (action: string) => {\n if (typeof window !== 'undefined' && window.parent && window.parent !== window) {\n window.parent.postMessage({ type: 'cms-block-action', action, blockId }, '*');\n }\n };\n\n return (\n <div ref={toolbarRef} className=\"cms-block-toolbar\" data-cms-toolbar=\"true\" style={style}>\n <button\n type=\"button\"\n title=\"Move up\"\n aria-label=\"Move up\"\n data-action=\"move-up\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-up');\n }}\n >\n <ChevronUp />\n </button>\n <button\n type=\"button\"\n title=\"Move down\"\n aria-label=\"Move down\"\n data-action=\"move-down\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('move-down');\n }}\n >\n <ChevronDown />\n </button>\n <button\n type=\"button\"\n title=\"Add block\"\n aria-label=\"Add block\"\n data-action=\"add-block\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('add-block');\n }}\n >\n <Plus />\n </button>\n <button\n type=\"button\"\n className=\"delete\"\n title=\"Delete block\"\n aria-label=\"Delete block\"\n data-action=\"delete\"\n onClick={(e) => {\n e.stopPropagation();\n handleAction('delete');\n }}\n >\n <Trash2 />\n </button>\n </div>\n );\n}\n"],"mappings":"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","names":["useLayoutEffect","useRef","useLayoutEffect","useRef","jsx","jsx","jsxs","useRef","useLayoutEffect"]}