cms-renderer 0.6.12 → 0.7.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.
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  interface DocsMarkdownProps {
@@ -13,6 +13,6 @@ interface DocsMarkdownProps {
13
13
  }
14
14
  declare function markdownStartsWithHeading(markdown: string): boolean;
15
15
  declare function initMarkdown(): Promise<void>;
16
- declare function DocsMarkdown({ content, className, renderImage, }: DocsMarkdownProps): Promise<React.JSX.Element>;
16
+ declare function DocsMarkdown({ content, className, renderImage, }: DocsMarkdownProps): Promise<react.JSX.Element>;
17
17
 
18
18
  export { DocsMarkdown, type DocsMarkdownProps, initMarkdown, markdownStartsWithHeading };
@@ -25,6 +25,19 @@ type ParametricRouteProps = CmsConfig & {
25
25
  };
26
26
  registry?: Partial<BlockComponentRegistry>;
27
27
  };
28
+ /**
29
+ * Render-time options that are NOT part of the page props surface. `preview` is
30
+ * supplied only by the dedicated preview page wrapper (`ParametricRoutePreviewPage`),
31
+ * never by query params, so the production page can never accidentally opt into
32
+ * edit overlays or draft content.
33
+ */
34
+ type RenderParametricRouteOptions = {
35
+ /**
36
+ * Renders the preview experience: forces the edit-mode overlay/hovers and pulls
37
+ * block content from `draft_content` instead of `published_content`.
38
+ */
39
+ preview?: boolean;
40
+ };
28
41
  type ParametricRouteResult = {
29
42
  status: 'ok';
30
43
  node: ReactNode;
@@ -40,6 +53,6 @@ declare function getWebsiteId(providedWebsiteId?: string): string;
40
53
  * Renders CMS-backed blocks for a multi-segment path. Maps NOT_FOUND-style API
41
54
  * outcomes to `{ status: 'not_found' }` instead of throwing.
42
55
  */
43
- declare function renderParametricRoute({ params, searchParams, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: ParametricRouteProps): Promise<ParametricRouteResult>;
56
+ declare function renderParametricRoute({ params, searchParams, registry, apiKey, cmsUrl, websiteId: providedWebsiteId, }: ParametricRouteProps, { preview }?: RenderParametricRouteOptions): Promise<ParametricRouteResult>;
44
57
 
45
- export { type ParametricRouteProps, type ParametricRouteResult, getWebsiteId, renderParametricRoute };
58
+ export { type ParametricRouteProps, type ParametricRouteResult, type RenderParametricRouteOptions, getWebsiteId, renderParametricRoute };
@@ -402,7 +402,7 @@ function generateCmsOverlayScript(cmsParentOrigin) {
402
402
  border-radius: 2px;
403
403
  }
404
404
  [data-cms-editable]:not([data-cms-block]):hover {
405
- outline: 2px solid #3b82f6;
405
+ outline: 2px solid var(--component-accent, #A78BFA);
406
406
  outline-offset: 2px;
407
407
  }
408
408
  #cms-overlay-root {
@@ -414,20 +414,43 @@ function generateCmsOverlayScript(cmsParentOrigin) {
414
414
  pointer-events: none;
415
415
  z-index: 99998;
416
416
  }
417
- #cms-overlay-root > * {
417
+ #cms-overlay-root > *:not(.cms-block-toolbar) {
418
418
  pointer-events: none;
419
419
  }
420
420
  .cms-block-outline {
421
421
  position: fixed;
422
- border: 2px solid #3b82f6;
422
+ box-sizing: border-box;
423
+ border: 4px solid var(--component-accent, #A78BFA);
423
424
  border-radius: 4px;
424
425
  pointer-events: none;
425
426
  z-index: 99997;
426
427
  transition: all 0.15s ease;
427
428
  }
428
429
  .cms-block-outline.cms-outline-selected {
429
- border-color: #2563eb;
430
- border-width: 3px;
430
+ border-color: var(--component-accent, #A78BFA);
431
+ border-width: 4px;
432
+ }
433
+ .cms-block-label {
434
+ position: absolute;
435
+ top: 0;
436
+ left: 0;
437
+ display: none;
438
+ max-width: 240px;
439
+ padding: 2px 8px;
440
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
441
+ font-size: 12px;
442
+ font-weight: 600;
443
+ line-height: 1.4;
444
+ color: #fff;
445
+ background: var(--component-accent, #A78BFA);
446
+ border-radius: 4px 4px 4px 0;
447
+ white-space: nowrap;
448
+ overflow: hidden;
449
+ text-overflow: ellipsis;
450
+ pointer-events: none;
451
+ }
452
+ .cms-block-label.cms-label-visible {
453
+ display: block;
431
454
  }
432
455
  .cms-block-cursor {
433
456
  position: fixed;
@@ -511,9 +534,69 @@ function generateCmsOverlayScript(cmsParentOrigin) {
511
534
  blockEl.setAttribute('data-cms-block', '');
512
535
  blockEl.setAttribute('data-block-id', blockId);
513
536
  blockEl.setAttribute('data-block-type', blockType);
537
+
538
+ injectEditableSpans(
539
+ blockEl,
540
+ blockId,
541
+ blockType,
542
+ sentinel.getAttribute('data-content-entries')
543
+ );
514
544
  });
515
545
  }
516
546
 
547
+ function injectEditableSpans(blockEl, blockId, blockType, rawEntries) {
548
+ if (!rawEntries || blockEl.querySelector('[data-cms-editable][data-content-path]')) return;
549
+
550
+ var entries;
551
+ try {
552
+ entries = JSON.parse(rawEntries);
553
+ } catch (_) {
554
+ return;
555
+ }
556
+ if (!Array.isArray(entries) || entries.length === 0) return;
557
+
558
+ var used = {};
559
+ var walker = document.createTreeWalker(blockEl, NodeFilter.SHOW_TEXT, null);
560
+ var textNodes = [];
561
+ var node = walker.nextNode();
562
+ while (node !== null) {
563
+ if (node.nodeValue && node.nodeValue.trim()) {
564
+ textNodes.push(node);
565
+ }
566
+ node = walker.nextNode();
567
+ }
568
+
569
+ for (var i = 0; i < textNodes.length; i++) {
570
+ var textNode = textNodes[i];
571
+ var parentEl = textNode.parentElement;
572
+ if (!parentEl || parentEl.closest('svg') || parentEl.closest('[data-cms-editable]')) {
573
+ continue;
574
+ }
575
+
576
+ var text = textNode.nodeValue;
577
+ if (!text) continue;
578
+
579
+ for (var j = 0; j < entries.length; j++) {
580
+ var entry = entries[j];
581
+ if (!entry || typeof entry.v !== 'string' || typeof entry.p !== 'string' || used[entry.p]) {
582
+ continue;
583
+ }
584
+ if (text.trim() !== entry.v.trim()) continue;
585
+
586
+ used[entry.p] = true;
587
+ var span = document.createElement('span');
588
+ span.setAttribute('data-cms-editable', '');
589
+ span.setAttribute('data-block-id', blockId);
590
+ span.setAttribute('data-block-type', blockType);
591
+ span.setAttribute('data-content-path', entry.p);
592
+ span.setAttribute('contenteditable', 'true');
593
+ parentEl.insertBefore(span, textNode);
594
+ span.appendChild(textNode);
595
+ break;
596
+ }
597
+ }
598
+ }
599
+
517
600
  stampBlockElements();
518
601
 
519
602
  var stampObserver = new MutationObserver(function(mutations) {
@@ -532,15 +615,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
532
615
  cursor.className = 'cms-block-cursor';
533
616
  overlayRoot.appendChild(cursor);
534
617
 
535
- var outline = document.createElement('div');
536
- outline.className = 'cms-block-outline';
537
- outline.style.display = 'none';
538
- overlayRoot.appendChild(outline);
618
+ function createOutline(extraClass) {
619
+ var el = document.createElement('div');
620
+ el.className = 'cms-block-outline' + (extraClass ? ' ' + extraClass : '');
621
+ el.style.display = 'none';
622
+ var label = document.createElement('span');
623
+ label.className = 'cms-block-label';
624
+ el.appendChild(label);
625
+ overlayRoot.appendChild(el);
626
+ return el;
627
+ }
539
628
 
540
- var selectedOutline = document.createElement('div');
541
- selectedOutline.className = 'cms-block-outline cms-outline-selected';
542
- selectedOutline.style.display = 'none';
543
- overlayRoot.appendChild(selectedOutline);
629
+ var outline = createOutline();
630
+ var selectedOutline = createOutline('cms-outline-selected');
544
631
 
545
632
  var toolbar = document.createElement('div');
546
633
  toolbar.className = 'cms-block-toolbar';
@@ -568,35 +655,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
568
655
  var toolbarVisible = false;
569
656
  var selectedBlockId = null;
570
657
 
571
- function decoratePreviewUrl(rawHref) {
572
- if (!rawHref) return rawHref;
573
- try {
574
- var url = new URL(rawHref, window.location.href);
575
- if (url.origin !== window.location.origin) return rawHref;
576
- url.searchParams.set('edit_mode', 'true');
577
- if (CMS_PARENT_ORIGIN) {
578
- url.searchParams.set('cms_parent_origin', CMS_PARENT_ORIGIN);
579
- }
580
- return url.toString();
581
- } catch (_) {
582
- return rawHref;
583
- }
584
- }
585
-
586
- function preserveEditParamsOnNavigation(e) {
587
- if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
588
- return;
589
- }
590
-
591
- var anchor = e.target.closest('a[href]');
592
- if (!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download')) {
593
- return;
594
- }
595
-
596
- var href = anchor.getAttribute('href');
597
- var decorated = decoratePreviewUrl(href);
598
- if (decorated && decorated !== href) {
599
- anchor.setAttribute('href', decorated);
658
+ // In edit mode we hijack every link/button activation. Following a link or
659
+ // firing a button's own handler makes inline editing on that element
660
+ // impossible (the click navigates away or triggers an action instead of
661
+ // placing the caret). We block the default action and stop the event from
662
+ // reaching the element's own listeners, while still letting our selection /
663
+ // edit routing run below.
664
+ function blockInteractiveActivation(e) {
665
+ var interactive = e.target.closest(
666
+ 'a[href], button, [role="button"], input[type="submit"], input[type="button"], input[type="reset"]'
667
+ );
668
+ if (interactive) {
669
+ e.preventDefault();
670
+ e.stopPropagation();
600
671
  }
601
672
  }
602
673
 
@@ -631,9 +702,18 @@ function generateCmsOverlayScript(cmsParentOrigin) {
631
702
  return -1;
632
703
  }
633
704
 
705
+ function formatBlockType(type) {
706
+ if (!type) return 'Block';
707
+ return type
708
+ .replace(/[-_]+/g, ' ')
709
+ .replace(/\\b\\w/g, function(c) { return c.toUpperCase(); });
710
+ }
711
+
634
712
  function updateOutline(el, outlineEl) {
713
+ var label = outlineEl.querySelector('.cms-block-label');
635
714
  if (!el) {
636
715
  outlineEl.style.display = 'none';
716
+ if (label) label.classList.remove('cms-label-visible');
637
717
  return;
638
718
  }
639
719
  var rect = el.getBoundingClientRect();
@@ -642,6 +722,10 @@ function generateCmsOverlayScript(cmsParentOrigin) {
642
722
  outlineEl.style.left = rect.left + 'px';
643
723
  outlineEl.style.width = rect.width + 'px';
644
724
  outlineEl.style.height = rect.height + 'px';
725
+ if (label) {
726
+ label.textContent = formatBlockType(el.getAttribute('data-block-type'));
727
+ label.classList.add('cms-label-visible');
728
+ }
645
729
  }
646
730
 
647
731
  function positionToolbar(x, y) {
@@ -718,17 +802,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
718
802
  });
719
803
 
720
804
  document.addEventListener('click', function(e) {
721
- preserveEditParamsOnNavigation(e);
805
+ // The floating toolbar is interactive (and its buttons are real <button>s);
806
+ // bail out before any blocking so its own handlers run.
807
+ if (e.target.closest('[data-cms-toolbar]')) return;
722
808
 
723
- if (toolbarVisible && !e.target.closest('[data-cms-toolbar]')) {
724
- var block = e.target.closest('[data-cms-block]');
725
- if (!block || block.getAttribute('data-block-id') !== currentBlockId) {
809
+ blockInteractiveActivation(e);
810
+
811
+ if (toolbarVisible) {
812
+ var activeBlock = e.target.closest('[data-cms-block]');
813
+ if (!activeBlock || activeBlock.getAttribute('data-block-id') !== currentBlockId) {
726
814
  hideToolbar();
727
815
  }
728
816
  }
729
817
 
730
- if (e.target.closest('[data-cms-toolbar]')) return;
731
-
732
818
  var editable = e.target.closest('[data-cms-editable]');
733
819
  if (editable) {
734
820
  postToParent({
@@ -859,6 +945,27 @@ function getCmsParentTargetOrigin(preferredCmsUrl, explicitParentOrigin) {
859
945
 
860
946
  // lib/block-renderer.tsx
861
947
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
948
+ function extractContentValues(content, basePath = []) {
949
+ const map = /* @__PURE__ */ new Map();
950
+ function walk(obj, path) {
951
+ if (typeof obj === "string" && obj.trim() !== "") {
952
+ const contentPath = path.join(".");
953
+ const existing = map.get(obj) || [];
954
+ existing.push({ contentPath, value: obj });
955
+ map.set(obj, existing);
956
+ } else if (Array.isArray(obj)) {
957
+ for (let index = 0; index < obj.length; index++) {
958
+ walk(obj[index], [...path, String(index)]);
959
+ }
960
+ } else if (obj && typeof obj === "object") {
961
+ for (const [key, value] of Object.entries(obj)) {
962
+ walk(value, [...path, key]);
963
+ }
964
+ }
965
+ }
966
+ walk(content, basePath);
967
+ return map;
968
+ }
862
969
  function CmsEditableInit({
863
970
  cmsUrl,
864
971
  cmsParentOrigin
@@ -907,6 +1014,7 @@ function BlockRenderer({
907
1014
  block,
908
1015
  registry,
909
1016
  disableEditable,
1017
+ enableContentEditable,
910
1018
  routeParams,
911
1019
  path
912
1020
  }) {
@@ -922,6 +1030,7 @@ function BlockRenderer({
922
1030
  if (disableEditable) {
923
1031
  return component;
924
1032
  }
1033
+ const contentEntries = enableContentEditable ? [...extractContentValues(block.content).values()].flat().map(({ value, contentPath }) => ({ v: value, p: contentPath })) : void 0;
925
1034
  return /* @__PURE__ */ jsxs(Fragment, { children: [
926
1035
  /* @__PURE__ */ jsx(
927
1036
  "span",
@@ -929,6 +1038,7 @@ function BlockRenderer({
929
1038
  "data-cms-sentinel": "",
930
1039
  "data-block-id": block.id,
931
1040
  "data-block-type": block.type,
1041
+ "data-content-entries": contentEntries ? JSON.stringify(contentEntries) : void 0,
932
1042
  style: { display: "none" },
933
1043
  "aria-hidden": "true"
934
1044
  }
@@ -1010,7 +1120,7 @@ async function renderParametricRoute({
1010
1120
  apiKey,
1011
1121
  cmsUrl,
1012
1122
  websiteId: providedWebsiteId
1013
- }) {
1123
+ }, { preview = false } = {}) {
1014
1124
  const websiteId = getWebsiteId(providedWebsiteId);
1015
1125
  const { slug } = "then" in params ? await params : params;
1016
1126
  const resolvedSearchParams = searchParams && "then" in searchParams ? await searchParams : searchParams;
@@ -1025,10 +1135,11 @@ async function renderParametricRoute({
1025
1135
  }
1026
1136
  }
1027
1137
  }
1028
- const editModeParam = resolvedSearchParams?.edit_mode;
1029
- const editMode = editModeParam === true || editModeParam === "true" || editModeParam === "1";
1030
1138
  const cmsParentOriginParam = resolvedSearchParams?.cms_parent_origin;
1031
1139
  const cmsParentOrigin = typeof cmsParentOriginParam === "boolean" ? void 0 : Array.isArray(cmsParentOriginParam) ? cmsParentOriginParam[0] : cmsParentOriginParam;
1140
+ const previewMode = preview === true;
1141
+ const editModeParam = resolvedSearchParams?.edit_mode;
1142
+ const editMode = previewMode || editModeParam === true || editModeParam === "true" || editModeParam === "1";
1032
1143
  const rawPath = `/${slug.join("/")}`;
1033
1144
  const path = normalizePath(rawPath);
1034
1145
  if (/\.[a-zA-Z0-9]+$/.test(path)) {
@@ -1068,7 +1179,9 @@ async function renderParametricRoute({
1068
1179
  ]);
1069
1180
  const blocks = [];
1070
1181
  for (const block of blockResults) {
1071
- if (!block || block.published_content === null) continue;
1182
+ if (!block) continue;
1183
+ const blockContent = previewMode ? block.draft_content : block.published_content;
1184
+ if (blockContent == null) continue;
1072
1185
  let content = null;
1073
1186
  if (aiPreviewIndex !== null) {
1074
1187
  const generatedBlock = generatedBlocks[block.id];
@@ -1078,7 +1191,7 @@ async function renderParametricRoute({
1078
1191
  content = variants[variantIndex].content;
1079
1192
  }
1080
1193
  }
1081
- content = content ?? block.published_content;
1194
+ content = content ?? blockContent;
1082
1195
  if (!content) continue;
1083
1196
  if (block.schema_name === "article") {
1084
1197
  const article = normalizeArticleContent(content);
@@ -1126,6 +1239,7 @@ async function renderParametricRoute({
1126
1239
  block,
1127
1240
  registry: registry ?? {},
1128
1241
  disableEditable: !editMode,
1242
+ enableContentEditable: previewMode,
1129
1243
  routeParams,
1130
1244
  path
1131
1245
  },