cms-renderer 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,19 +3,23 @@
3
3
 
4
4
  // lib/client-editable-block.tsx
5
5
  import { useCallback, useLayoutEffect, useRef } from "react";
6
- import { jsx } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  function ClientEditableBlock({
8
8
  blockId,
9
9
  blockType,
10
10
  contentEntries,
11
- children
11
+ children,
12
+ blockProps
12
13
  }) {
13
- const containerRef = useRef(null);
14
+ const sentinelRef = useRef(null);
14
15
  const observerRef = useRef(null);
15
16
  const isInjectingRef = useRef(false);
17
+ const getBlockRoot = useCallback(() => {
18
+ return sentinelRef.current?.nextElementSibling ?? null;
19
+ }, []);
16
20
  const injectSpans = useCallback(() => {
17
21
  if (isInjectingRef.current) return;
18
- const container = containerRef.current;
22
+ const container = getBlockRoot();
19
23
  if (!container) return;
20
24
  isInjectingRef.current = true;
21
25
  observerRef.current?.disconnect();
@@ -41,6 +45,7 @@ function ClientEditableBlock({
41
45
  n = walker.nextNode();
42
46
  }
43
47
  for (const textNode of textNodes) {
48
+ if (textNode.parentElement?.closest("svg") != null) continue;
44
49
  const text = textNode.nodeValue;
45
50
  if (!text) continue;
46
51
  for (const entry of contentEntries) {
@@ -48,6 +53,7 @@ function ClientEditableBlock({
48
53
  if (text.indexOf(entry.v) !== -1 && text.trim() === entry.v.trim()) {
49
54
  used.add(entry.p);
50
55
  const span = document.createElement("span");
56
+ span.style.display = "contents";
51
57
  span.setAttribute("data-cms-editable", "");
52
58
  span.setAttribute("data-block-id", blockId);
53
59
  span.setAttribute("data-block-type", blockType);
@@ -60,22 +66,28 @@ function ClientEditableBlock({
60
66
  }
61
67
  } finally {
62
68
  isInjectingRef.current = false;
63
- const current = containerRef.current;
64
- if (current && observerRef.current) {
65
- observerRef.current.observe(current, {
69
+ const blockRoot = getBlockRoot();
70
+ if (blockRoot && observerRef.current) {
71
+ observerRef.current.observe(blockRoot, {
66
72
  childList: true,
67
73
  subtree: true,
68
74
  characterData: true
69
75
  });
70
76
  }
71
77
  }
72
- }, [blockId, blockType, contentEntries]);
78
+ }, [blockId, blockType, contentEntries, getBlockRoot]);
73
79
  useLayoutEffect(() => {
80
+ const blockRoot = getBlockRoot();
81
+ if (blockRoot && blockProps) {
82
+ for (const [key, value] of Object.entries(blockProps)) {
83
+ blockRoot.setAttribute(key, value);
84
+ }
85
+ }
74
86
  injectSpans();
75
87
  const observer = new MutationObserver(injectSpans);
76
88
  observerRef.current = observer;
77
- if (containerRef.current) {
78
- observer.observe(containerRef.current, {
89
+ if (blockRoot) {
90
+ observer.observe(blockRoot, {
79
91
  childList: true,
80
92
  subtree: true,
81
93
  characterData: true
@@ -85,8 +97,11 @@ function ClientEditableBlock({
85
97
  observer.disconnect();
86
98
  observerRef.current = null;
87
99
  };
88
- }, [injectSpans]);
89
- return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { display: "contents" }, children });
100
+ }, [injectSpans, getBlockRoot, blockProps]);
101
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ /* @__PURE__ */ jsx("span", { ref: sentinelRef, style: { display: "none" }, "aria-hidden": "true" }),
103
+ children
104
+ ] });
90
105
  }
91
106
  export {
92
107
  ClientEditableBlock
@@ -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}\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 * 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}: ClientEditableBlockProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const observerRef = useRef<MutationObserver | null>(null);\n const isInjectingRef = useRef(false);\n\n const injectSpans = useCallback(() => {\n if (isInjectingRef.current) return;\n const container = containerRef.current;\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 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 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 current = containerRef.current;\n if (current && observerRef.current) {\n observerRef.current.observe(current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n }\n }, [blockId, blockType, contentEntries]);\n\n useLayoutEffect(() => {\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 (containerRef.current) {\n observer.observe(containerRef.current, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n return () => {\n observer.disconnect();\n observerRef.current = null;\n };\n }, [injectSpans]);\n\n return (\n <div ref={containerRef} style={{ display: 'contents' }}>\n {children}\n </div>\n );\n}\n"],"mappings":";;;;AAEA,SAAS,aAAa,iBAAiB,cAAc;AA6HjD;AAvGG,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,cAAc,OAAgC,IAAI;AACxD,QAAM,iBAAiB,OAAO,KAAK;AAEnC,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI,eAAe,QAAS;AAC5B,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAEhB,mBAAe,UAAU;AAEzB,gBAAY,SAAS,WAAW;AAEhC,QAAI;AAEF,YAAM,WAAW,MAAM;AAAA,QACrB,UAAU,iBAA8B,4CAA4C;AAAA,MACtF;AACA,iBAAW,QAAQ,UAAU;AAC3B,cAAM,SAAS,KAAK;AACpB,YAAI,CAAC,OAAQ;AACb,eAAO,KAAK,WAAY,QAAO,aAAa,KAAK,YAAY,IAAI;AACjE,eAAO,YAAY,IAAI;AAAA,MACzB;AAGA,YAAM,OAAO,oBAAI,IAAY;AAC7B,YAAM,SAAS,SAAS,iBAAiB,WAAW,WAAW,WAAW,IAAI;AAC9E,YAAM,YAAoB,CAAC;AAC3B,UAAI,IAAI,OAAO,SAAS;AACxB,aAAO,MAAM,MAAM;AACjB,cAAM,WAAW;AACjB,YACE,SAAS,eAAe,QAAQ,oBAAoB,KAAK,QACzD,SAAS,WAAW,KAAK,GACzB;AACA,oBAAU,KAAK,QAAQ;AAAA,QACzB;AACA,YAAI,OAAO,SAAS;AAAA,MACtB;AAEA,iBAAW,YAAY,WAAW;AAChC,cAAM,OAAO,SAAS;AACtB,YAAI,CAAC,KAAM;AACX,mBAAW,SAAS,gBAAgB;AAClC,cAAI,KAAK,IAAI,MAAM,CAAC,EAAG;AACvB,cAAI,KAAK,QAAQ,MAAM,CAAC,MAAM,MAAM,KAAK,KAAK,MAAM,MAAM,EAAE,KAAK,GAAG;AAClE,iBAAK,IAAI,MAAM,CAAC;AAChB,kBAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,iBAAK,aAAa,qBAAqB,EAAE;AACzC,iBAAK,aAAa,iBAAiB,OAAO;AAC1C,iBAAK,aAAa,mBAAmB,SAAS;AAC9C,iBAAK,aAAa,qBAAqB,MAAM,CAAC;AAC9C,qBAAS,YAAY,aAAa,MAAM,QAAQ;AAChD,iBAAK,YAAY,QAAQ;AACzB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,UAAE;AACA,qBAAe,UAAU;AAEzB,YAAM,UAAU,aAAa;AAC7B,UAAI,WAAW,YAAY,SAAS;AAClC,oBAAY,QAAQ,QAAQ,SAAS;AAAA,UACnC,WAAW;AAAA,UACX,SAAS;AAAA,UACT,eAAe;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,WAAW,cAAc,CAAC;AAEvC,kBAAgB,MAAM;AAEpB,gBAAY;AAIZ,UAAM,WAAW,IAAI,iBAAiB,WAAW;AACjD,gBAAY,UAAU;AAEtB,QAAI,aAAa,SAAS;AACxB,eAAS,QAAQ,aAAa,SAAS;AAAA,QACrC,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,MACjB,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,eAAS,WAAW;AACpB,kBAAY,UAAU;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,SACE,oBAAC,SAAI,KAAK,cAAc,OAAO,EAAE,SAAS,WAAW,GAClD,UACH;AAEJ;","names":[]}
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":[]}
@@ -245,13 +245,27 @@ import { notFound } from "next/navigation";
245
245
  import React from "react";
246
246
  import { BlockToolbar } from "./block-toolbar.js";
247
247
  import { ClientEditableBlock } from "./client-editable-block.js";
248
- import { jsx, jsxs } from "react/jsx-runtime";
248
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
249
+ var SKIP_SPAN_PARENTS = /* @__PURE__ */ new Set([
250
+ // SVG text-bearing elements — spans are not valid SVG children
251
+ "text",
252
+ "tspan",
253
+ "textPath",
254
+ "desc",
255
+ // Form elements whose text content must not be interrupted
256
+ "option",
257
+ "optgroup",
258
+ // Non-rendered elements
259
+ "script",
260
+ "style",
261
+ "noscript"
262
+ ]);
249
263
  function walkReactNode(node, visitors, ctx = {}) {
250
264
  const path = ctx.path ?? [];
251
265
  if (node == null || typeof node === "boolean") return node;
252
266
  if (typeof node === "string" || typeof node === "number") {
253
267
  const value = String(node);
254
- return visitors.onText ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key }) : node;
268
+ return visitors.onText ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg }) : node;
255
269
  }
256
270
  if (Array.isArray(node)) {
257
271
  return node.map((child, i) => {
@@ -259,7 +273,8 @@ function walkReactNode(node, visitors, ctx = {}) {
259
273
  const result = walkReactNode(child, visitors, {
260
274
  path: [...path, i],
261
275
  parentType: ctx.parentType,
262
- key: childKey
276
+ key: childKey,
277
+ inSvg: ctx.inSvg
263
278
  });
264
279
  if (React.isValidElement(result) && result.key == null) {
265
280
  return React.cloneElement(result, { key: childKey ?? `arr-${path.join("-")}-${i}` });
@@ -270,13 +285,15 @@ function walkReactNode(node, visitors, ctx = {}) {
270
285
  if (React.isValidElement(node)) {
271
286
  const el = node;
272
287
  const elProps = el.props;
288
+ const nextInSvg = ctx.inSvg || el.type === "svg";
273
289
  const hasChildren = elProps && "children" in elProps;
274
290
  const nextChildren = hasChildren ? React.Children.map(elProps.children, (child, i) => {
275
291
  const childKey = child?.key ?? null;
276
292
  const result = walkReactNode(child, visitors, {
277
293
  path: [...path, "children", i],
278
294
  parentType: el.type,
279
- key: childKey
295
+ key: childKey,
296
+ inSvg: nextInSvg
280
297
  });
281
298
  if (React.isValidElement(result) && result.key == null) {
282
299
  return React.cloneElement(result, { key: childKey ?? `child-${path.join("-")}-${i}` });
@@ -309,6 +326,140 @@ function extractContentValues(content, basePath = []) {
309
326
  walk(content, basePath);
310
327
  return map;
311
328
  }
329
+ var CMS_EDITABLE_STYLES = `
330
+ .cms-block-toolbar {
331
+ position: fixed;
332
+ transform: translateX(-50%);
333
+ display: flex;
334
+ gap: 4px;
335
+ background: #1f2937;
336
+ border-radius: 6px;
337
+ padding: 4px;
338
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
339
+ opacity: 0;
340
+ pointer-events: none;
341
+ transition: opacity 0.15s ease;
342
+ z-index: 9999;
343
+ }
344
+ .cms-block-toolbar button {
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ width: 28px;
349
+ height: 28px;
350
+ border: none;
351
+ background: transparent;
352
+ color: #9ca3af;
353
+ border-radius: 4px;
354
+ cursor: pointer;
355
+ transition: background 0.15s ease, color 0.15s ease;
356
+ }
357
+ .cms-block-toolbar button:hover {
358
+ background: #374151;
359
+ color: #fff;
360
+ }
361
+ .cms-block-toolbar button.delete:hover {
362
+ background: #dc2626;
363
+ color: #fff;
364
+ }
365
+ .cms-block-toolbar button:disabled {
366
+ opacity: 0.4;
367
+ cursor: not-allowed;
368
+ }
369
+ .cms-block-toolbar button:disabled:hover {
370
+ background: transparent;
371
+ color: #9ca3af;
372
+ }
373
+ .cms-block-toolbar svg {
374
+ width: 16px;
375
+ height: 16px;
376
+ }
377
+ `;
378
+ var CMS_EDITABLE_SCRIPT = `
379
+ (function() {
380
+ if (!window.__cmsEditableInitialized) {
381
+ window.__cmsEditableInitialized = true;
382
+ var _ab = null;
383
+
384
+ function _tb(bid) {
385
+ return document.querySelector('.cms-block-toolbar[data-block-id="' + bid + '"]');
386
+ }
387
+
388
+ function _show(bid, target) {
389
+ var tb = _tb(bid);
390
+ if (tb && target) {
391
+ var r = target.getBoundingClientRect();
392
+ tb.style.left = Math.round(r.left + r.width / 2) + 'px';
393
+ tb.style.top = Math.round(r.bottom + 8) + 'px';
394
+ tb.style.opacity = '1';
395
+ tb.style.pointerEvents = 'auto';
396
+ }
397
+ }
398
+
399
+ function _hide(bid) {
400
+ var tb = _tb(bid);
401
+ if (tb) { tb.style.opacity = '0'; tb.style.pointerEvents = 'none'; }
402
+ }
403
+
404
+ document.addEventListener('mouseover', function(e) {
405
+ if (e.target.closest('.cms-block-toolbar')) return;
406
+ var bel = e.target.closest('[data-cms-block]');
407
+ var bid = bel ? bel.getAttribute('data-block-id') : null;
408
+ if (bid === _ab) return;
409
+ if (_ab) _hide(_ab);
410
+ _ab = bid;
411
+ if (bid) _show(bid, e.target);
412
+ });
413
+
414
+ document.addEventListener('click', function(e) {
415
+ if (e.target.closest('.cms-block-toolbar')) return;
416
+
417
+ var path = e.composedPath ? e.composedPath() : [e.target];
418
+ var et = null;
419
+ for (var i = 0; i < path.length; i++) {
420
+ var n = path[i];
421
+ if (n.nodeType === 1 && n.hasAttribute && n.hasAttribute('data-cms-editable')) { et = n; break; }
422
+ }
423
+ if (!et) et = e.target.closest && e.target.closest('[data-cms-editable]');
424
+
425
+ if (et) {
426
+ if (window.parent && window.parent !== window) {
427
+ window.parent.postMessage({
428
+ type: 'cms-editable-click',
429
+ blockId: et.getAttribute('data-block-id'),
430
+ blockType: et.getAttribute('data-block-type'),
431
+ contentPath: et.getAttribute('data-content-path')
432
+ }, '*');
433
+ }
434
+ return;
435
+ }
436
+
437
+ var bt = e.target.closest && e.target.closest('[data-cms-block]');
438
+ if (bt) {
439
+ if (window.parent && window.parent !== window) {
440
+ window.parent.postMessage({
441
+ type: 'cms-editable-click',
442
+ blockId: bt.getAttribute('data-block-id'),
443
+ blockType: bt.getAttribute('data-block-type'),
444
+ contentPath: null
445
+ }, '*');
446
+ }
447
+ }
448
+ });
449
+ }
450
+ })();
451
+ `;
452
+ function CmsEditableInit() {
453
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
454
+ /* @__PURE__ */ jsx("style", { children: CMS_EDITABLE_STYLES }),
455
+ /* @__PURE__ */ jsx(
456
+ "script",
457
+ {
458
+ dangerouslySetInnerHTML: { __html: CMS_EDITABLE_SCRIPT }
459
+ }
460
+ )
461
+ ] });
462
+ }
312
463
  function pathMatchesPattern(path, pattern) {
313
464
  const pathSegs = path.split("/").filter(Boolean);
314
465
  const patternSegs = pattern.split("/").filter(Boolean);
@@ -398,7 +549,11 @@ function BlockRenderer({
398
549
  if (isWalkable) {
399
550
  const usedPaths = /* @__PURE__ */ new Set();
400
551
  renderedComponent = walkReactNode(renderedTree, {
401
- onText: ({ value, key, path: path2 }) => {
552
+ onText: ({ value, key, path: path2, inSvg, parentType }) => {
553
+ if (inSvg) return value;
554
+ if (parentType && typeof parentType === "string" && SKIP_SPAN_PARENTS.has(parentType)) {
555
+ return value;
556
+ }
402
557
  const matches = contentValueMap.get(value);
403
558
  if (!matches || matches.length === 0) return value;
404
559
  const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];
@@ -412,6 +567,7 @@ function BlockRenderer({
412
567
  "data-block-id": block.id,
413
568
  "data-block-type": block.type,
414
569
  "data-content-path": match.contentPath,
570
+ style: { display: "contents" },
415
571
  children: value
416
572
  },
417
573
  spanKey
@@ -423,165 +579,35 @@ function BlockRenderer({
423
579
  } catch {
424
580
  }
425
581
  const contentEntries = needsClientSideSpans ? Array.from(contentValueMap.entries()).map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath })).filter((e) => !!e.p) : [];
426
- return /* @__PURE__ */ jsxs(
427
- "div",
428
- {
429
- "data-cms-block": true,
430
- "data-block-id": block.id,
431
- "data-block-type": block.type,
432
- style: { display: "contents" },
433
- children: [
434
- /* @__PURE__ */ jsx("style", { children: `
435
- [data-cms-block] {
436
- display: contents;
437
- }
438
- [data-cms-editable] {
439
- display: contents;
440
- cursor: pointer;
441
- }
442
- .cms-block-toolbar {
443
- position: fixed;
444
- transform: translateX(-50%);
445
- display: flex;
446
- gap: 4px;
447
- background: #1f2937;
448
- border-radius: 6px;
449
- padding: 4px;
450
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
451
- opacity: 0;
452
- pointer-events: none;
453
- transition: opacity 0.15s ease;
454
- z-index: 9999;
455
- }
456
- .cms-block-toolbar button {
457
- display: flex;
458
- align-items: center;
459
- justify-content: center;
460
- width: 28px;
461
- height: 28px;
462
- border: none;
463
- background: transparent;
464
- color: #9ca3af;
465
- border-radius: 4px;
466
- cursor: pointer;
467
- transition: background 0.15s ease, color 0.15s ease;
468
- }
469
- .cms-block-toolbar button:hover {
470
- background: #374151;
471
- color: #fff;
472
- }
473
- .cms-block-toolbar button.delete:hover {
474
- background: #dc2626;
475
- color: #fff;
476
- }
477
- .cms-block-toolbar button:disabled {
478
- opacity: 0.4;
479
- cursor: not-allowed;
480
- }
481
- .cms-block-toolbar button:disabled:hover {
482
- background: transparent;
483
- color: #9ca3af;
484
- }
485
- .cms-block-toolbar svg {
486
- width: 16px;
487
- height: 16px;
488
- }
489
- ` }),
490
- /* @__PURE__ */ jsx(
491
- "script",
492
- {
493
- dangerouslySetInnerHTML: {
494
- __html: `
495
- (function() {
496
- if (!window.__cmsEditableInitialized) {
497
- window.__cmsEditableInitialized = true;
498
- var _ab = null;
499
-
500
- function _tb(bid) {
501
- return document.querySelector('.cms-block-toolbar[data-block-id="' + bid + '"]');
502
- }
503
-
504
- function _show(bid, target) {
505
- var tb = _tb(bid);
506
- if (tb && target) {
507
- var r = target.getBoundingClientRect();
508
- tb.style.left = Math.round(r.left + r.width / 2) + 'px';
509
- tb.style.top = Math.round(r.bottom + 8) + 'px';
510
- tb.style.opacity = '1';
511
- tb.style.pointerEvents = 'auto';
512
- }
513
- }
514
-
515
- function _hide(bid) {
516
- var tb = _tb(bid);
517
- if (tb) { tb.style.opacity = '0'; tb.style.pointerEvents = 'none'; }
518
- }
519
-
520
- document.addEventListener('mouseover', function(e) {
521
- if (e.target.closest('.cms-block-toolbar')) return;
522
- var bel = e.target.closest('[data-cms-block]');
523
- var bid = bel ? bel.getAttribute('data-block-id') : null;
524
- if (bid === _ab) return;
525
- if (_ab) _hide(_ab);
526
- _ab = bid;
527
- if (bid) _show(bid, e.target);
528
- });
529
-
530
- document.addEventListener('click', function(e) {
531
- if (e.target.closest('.cms-block-toolbar')) return;
532
-
533
- var path = e.composedPath ? e.composedPath() : [e.target];
534
- var et = null;
535
- for (var i = 0; i < path.length; i++) {
536
- var n = path[i];
537
- if (n.nodeType === 1 && n.hasAttribute && n.hasAttribute('data-cms-editable')) { et = n; break; }
538
- }
539
- if (!et) et = e.target.closest && e.target.closest('[data-cms-editable]');
540
-
541
- if (et) {
542
- if (window.parent && window.parent !== window) {
543
- window.parent.postMessage({
544
- type: 'cms-editable-click',
545
- blockId: et.getAttribute('data-block-id'),
546
- blockType: et.getAttribute('data-block-type'),
547
- contentPath: et.getAttribute('data-content-path')
548
- }, '*');
549
- }
550
- return;
551
- }
552
-
553
- var bt = e.target.closest && e.target.closest('[data-cms-block]');
554
- if (bt) {
555
- if (window.parent && window.parent !== window) {
556
- window.parent.postMessage({
557
- type: 'cms-editable-click',
558
- blockId: bt.getAttribute('data-block-id'),
559
- blockType: bt.getAttribute('data-block-type'),
560
- contentPath: null
561
- }, '*');
562
- }
563
- }
564
- });
565
- }
566
- })();
567
- `
568
- }
569
- }
570
- ),
571
- needsClientSideSpans && contentEntries.length > 0 ? /* @__PURE__ */ jsx(
572
- ClientEditableBlock,
573
- {
574
- blockId: block.id,
575
- blockType: block.type,
576
- contentEntries,
577
- children: renderedComponent
578
- }
579
- ) : renderedComponent,
580
- /* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
581
- ]
582
- },
583
- block.id
584
- );
582
+ const blockProps = {
583
+ "data-cms-block": "true",
584
+ "data-block-id": block.id,
585
+ "data-block-type": block.type
586
+ };
587
+ if (needsClientSideSpans) {
588
+ return /* @__PURE__ */ jsxs(
589
+ ClientEditableBlock,
590
+ {
591
+ blockId: block.id,
592
+ blockType: block.type,
593
+ contentEntries,
594
+ blockProps,
595
+ children: [
596
+ renderedComponent,
597
+ /* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
598
+ ]
599
+ },
600
+ block.id
601
+ );
602
+ }
603
+ const rootWithProps = React.isValidElement(renderedComponent) ? React.cloneElement(
604
+ renderedComponent,
605
+ blockProps
606
+ ) : renderedComponent;
607
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
608
+ rootWithProps,
609
+ /* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
610
+ ] });
585
611
  }
586
612
 
587
613
  // lib/cms-api.ts
@@ -624,7 +650,7 @@ function getCmsClient(options) {
624
650
  }
625
651
 
626
652
  // lib/renderer.tsx
627
- import { jsx as jsx2 } from "react/jsx-runtime";
653
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
628
654
  function getWebsiteId(providedWebsiteId) {
629
655
  const websiteId = providedWebsiteId ?? process.env.NEXT_PUBLIC_WEBSITE_ID ?? process.env.WEBSITE_ID ?? process.env.CMS_WEBSITE_ID;
630
656
  if (!websiteId) {
@@ -743,17 +769,20 @@ async function ParametricRoutePage({
743
769
  }
744
770
  ])
745
771
  ) : void 0;
746
- return /* @__PURE__ */ jsx2("main", { children: blocks.map((block) => /* @__PURE__ */ jsx2(
747
- BlockRenderer,
748
- {
749
- block,
750
- registry: registry ?? {},
751
- disableEditable: !editMode,
752
- routeParams,
753
- path
754
- },
755
- block.id
756
- )) });
772
+ return /* @__PURE__ */ jsxs2("main", { children: [
773
+ editMode && /* @__PURE__ */ jsx2(CmsEditableInit, {}),
774
+ blocks.map((block) => /* @__PURE__ */ jsx2(
775
+ BlockRenderer,
776
+ {
777
+ block,
778
+ registry: registry ?? {},
779
+ disableEditable: !editMode,
780
+ routeParams,
781
+ path
782
+ },
783
+ block.id
784
+ ))
785
+ ] });
757
786
  } catch (error) {
758
787
  console.error(`Route fetch error for path: ${path}`, error);
759
788
  const errorCode = error instanceof Error && "data" in error ? error.data?.code : error instanceof Error && "code" in error ? error.code : void 0;