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.
@@ -474,7 +474,7 @@ function generateCmsOverlayScript(cmsParentOrigin) {
474
474
  border-radius: 2px;
475
475
  }
476
476
  [data-cms-editable]:not([data-cms-block]):hover {
477
- outline: 2px solid #3b82f6;
477
+ outline: 2px solid var(--component-accent, #A78BFA);
478
478
  outline-offset: 2px;
479
479
  }
480
480
  #cms-overlay-root {
@@ -486,20 +486,43 @@ function generateCmsOverlayScript(cmsParentOrigin) {
486
486
  pointer-events: none;
487
487
  z-index: 99998;
488
488
  }
489
- #cms-overlay-root > * {
489
+ #cms-overlay-root > *:not(.cms-block-toolbar) {
490
490
  pointer-events: none;
491
491
  }
492
492
  .cms-block-outline {
493
493
  position: fixed;
494
- border: 2px solid #3b82f6;
494
+ box-sizing: border-box;
495
+ border: 4px solid var(--component-accent, #A78BFA);
495
496
  border-radius: 4px;
496
497
  pointer-events: none;
497
498
  z-index: 99997;
498
499
  transition: all 0.15s ease;
499
500
  }
500
501
  .cms-block-outline.cms-outline-selected {
501
- border-color: #2563eb;
502
- border-width: 3px;
502
+ border-color: var(--component-accent, #A78BFA);
503
+ border-width: 4px;
504
+ }
505
+ .cms-block-label {
506
+ position: absolute;
507
+ top: 0;
508
+ left: 0;
509
+ display: none;
510
+ max-width: 240px;
511
+ padding: 2px 8px;
512
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
513
+ font-size: 12px;
514
+ font-weight: 600;
515
+ line-height: 1.4;
516
+ color: #fff;
517
+ background: var(--component-accent, #A78BFA);
518
+ border-radius: 4px 4px 4px 0;
519
+ white-space: nowrap;
520
+ overflow: hidden;
521
+ text-overflow: ellipsis;
522
+ pointer-events: none;
523
+ }
524
+ .cms-block-label.cms-label-visible {
525
+ display: block;
503
526
  }
504
527
  .cms-block-cursor {
505
528
  position: fixed;
@@ -583,9 +606,69 @@ function generateCmsOverlayScript(cmsParentOrigin) {
583
606
  blockEl.setAttribute('data-cms-block', '');
584
607
  blockEl.setAttribute('data-block-id', blockId);
585
608
  blockEl.setAttribute('data-block-type', blockType);
609
+
610
+ injectEditableSpans(
611
+ blockEl,
612
+ blockId,
613
+ blockType,
614
+ sentinel.getAttribute('data-content-entries')
615
+ );
586
616
  });
587
617
  }
588
618
 
619
+ function injectEditableSpans(blockEl, blockId, blockType, rawEntries) {
620
+ if (!rawEntries || blockEl.querySelector('[data-cms-editable][data-content-path]')) return;
621
+
622
+ var entries;
623
+ try {
624
+ entries = JSON.parse(rawEntries);
625
+ } catch (_) {
626
+ return;
627
+ }
628
+ if (!Array.isArray(entries) || entries.length === 0) return;
629
+
630
+ var used = {};
631
+ var walker = document.createTreeWalker(blockEl, NodeFilter.SHOW_TEXT, null);
632
+ var textNodes = [];
633
+ var node = walker.nextNode();
634
+ while (node !== null) {
635
+ if (node.nodeValue && node.nodeValue.trim()) {
636
+ textNodes.push(node);
637
+ }
638
+ node = walker.nextNode();
639
+ }
640
+
641
+ for (var i = 0; i < textNodes.length; i++) {
642
+ var textNode = textNodes[i];
643
+ var parentEl = textNode.parentElement;
644
+ if (!parentEl || parentEl.closest('svg') || parentEl.closest('[data-cms-editable]')) {
645
+ continue;
646
+ }
647
+
648
+ var text = textNode.nodeValue;
649
+ if (!text) continue;
650
+
651
+ for (var j = 0; j < entries.length; j++) {
652
+ var entry = entries[j];
653
+ if (!entry || typeof entry.v !== 'string' || typeof entry.p !== 'string' || used[entry.p]) {
654
+ continue;
655
+ }
656
+ if (text.trim() !== entry.v.trim()) continue;
657
+
658
+ used[entry.p] = true;
659
+ var span = document.createElement('span');
660
+ span.setAttribute('data-cms-editable', '');
661
+ span.setAttribute('data-block-id', blockId);
662
+ span.setAttribute('data-block-type', blockType);
663
+ span.setAttribute('data-content-path', entry.p);
664
+ span.setAttribute('contenteditable', 'true');
665
+ parentEl.insertBefore(span, textNode);
666
+ span.appendChild(textNode);
667
+ break;
668
+ }
669
+ }
670
+ }
671
+
589
672
  stampBlockElements();
590
673
 
591
674
  var stampObserver = new MutationObserver(function(mutations) {
@@ -604,15 +687,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
604
687
  cursor.className = 'cms-block-cursor';
605
688
  overlayRoot.appendChild(cursor);
606
689
 
607
- var outline = document.createElement('div');
608
- outline.className = 'cms-block-outline';
609
- outline.style.display = 'none';
610
- overlayRoot.appendChild(outline);
690
+ function createOutline(extraClass) {
691
+ var el = document.createElement('div');
692
+ el.className = 'cms-block-outline' + (extraClass ? ' ' + extraClass : '');
693
+ el.style.display = 'none';
694
+ var label = document.createElement('span');
695
+ label.className = 'cms-block-label';
696
+ el.appendChild(label);
697
+ overlayRoot.appendChild(el);
698
+ return el;
699
+ }
611
700
 
612
- var selectedOutline = document.createElement('div');
613
- selectedOutline.className = 'cms-block-outline cms-outline-selected';
614
- selectedOutline.style.display = 'none';
615
- overlayRoot.appendChild(selectedOutline);
701
+ var outline = createOutline();
702
+ var selectedOutline = createOutline('cms-outline-selected');
616
703
 
617
704
  var toolbar = document.createElement('div');
618
705
  toolbar.className = 'cms-block-toolbar';
@@ -640,35 +727,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
640
727
  var toolbarVisible = false;
641
728
  var selectedBlockId = null;
642
729
 
643
- function decoratePreviewUrl(rawHref) {
644
- if (!rawHref) return rawHref;
645
- try {
646
- var url = new URL(rawHref, window.location.href);
647
- if (url.origin !== window.location.origin) return rawHref;
648
- url.searchParams.set('edit_mode', 'true');
649
- if (CMS_PARENT_ORIGIN) {
650
- url.searchParams.set('cms_parent_origin', CMS_PARENT_ORIGIN);
651
- }
652
- return url.toString();
653
- } catch (_) {
654
- return rawHref;
655
- }
656
- }
657
-
658
- function preserveEditParamsOnNavigation(e) {
659
- if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
660
- return;
661
- }
662
-
663
- var anchor = e.target.closest('a[href]');
664
- if (!anchor || (anchor.target && anchor.target !== '_self') || anchor.hasAttribute('download')) {
665
- return;
666
- }
667
-
668
- var href = anchor.getAttribute('href');
669
- var decorated = decoratePreviewUrl(href);
670
- if (decorated && decorated !== href) {
671
- anchor.setAttribute('href', decorated);
730
+ // In edit mode we hijack every link/button activation. Following a link or
731
+ // firing a button's own handler makes inline editing on that element
732
+ // impossible (the click navigates away or triggers an action instead of
733
+ // placing the caret). We block the default action and stop the event from
734
+ // reaching the element's own listeners, while still letting our selection /
735
+ // edit routing run below.
736
+ function blockInteractiveActivation(e) {
737
+ var interactive = e.target.closest(
738
+ 'a[href], button, [role="button"], input[type="submit"], input[type="button"], input[type="reset"]'
739
+ );
740
+ if (interactive) {
741
+ e.preventDefault();
742
+ e.stopPropagation();
672
743
  }
673
744
  }
674
745
 
@@ -703,9 +774,18 @@ function generateCmsOverlayScript(cmsParentOrigin) {
703
774
  return -1;
704
775
  }
705
776
 
777
+ function formatBlockType(type) {
778
+ if (!type) return 'Block';
779
+ return type
780
+ .replace(/[-_]+/g, ' ')
781
+ .replace(/\\b\\w/g, function(c) { return c.toUpperCase(); });
782
+ }
783
+
706
784
  function updateOutline(el, outlineEl) {
785
+ var label = outlineEl.querySelector('.cms-block-label');
707
786
  if (!el) {
708
787
  outlineEl.style.display = 'none';
788
+ if (label) label.classList.remove('cms-label-visible');
709
789
  return;
710
790
  }
711
791
  var rect = el.getBoundingClientRect();
@@ -714,6 +794,10 @@ function generateCmsOverlayScript(cmsParentOrigin) {
714
794
  outlineEl.style.left = rect.left + 'px';
715
795
  outlineEl.style.width = rect.width + 'px';
716
796
  outlineEl.style.height = rect.height + 'px';
797
+ if (label) {
798
+ label.textContent = formatBlockType(el.getAttribute('data-block-type'));
799
+ label.classList.add('cms-label-visible');
800
+ }
717
801
  }
718
802
 
719
803
  function positionToolbar(x, y) {
@@ -790,17 +874,19 @@ function generateCmsOverlayScript(cmsParentOrigin) {
790
874
  });
791
875
 
792
876
  document.addEventListener('click', function(e) {
793
- preserveEditParamsOnNavigation(e);
877
+ // The floating toolbar is interactive (and its buttons are real <button>s);
878
+ // bail out before any blocking so its own handlers run.
879
+ if (e.target.closest('[data-cms-toolbar]')) return;
794
880
 
795
- if (toolbarVisible && !e.target.closest('[data-cms-toolbar]')) {
796
- var block = e.target.closest('[data-cms-block]');
797
- if (!block || block.getAttribute('data-block-id') !== currentBlockId) {
881
+ blockInteractiveActivation(e);
882
+
883
+ if (toolbarVisible) {
884
+ var activeBlock = e.target.closest('[data-cms-block]');
885
+ if (!activeBlock || activeBlock.getAttribute('data-block-id') !== currentBlockId) {
798
886
  hideToolbar();
799
887
  }
800
888
  }
801
889
 
802
- if (e.target.closest('[data-cms-toolbar]')) return;
803
-
804
890
  var editable = e.target.closest('[data-cms-editable]');
805
891
  if (editable) {
806
892
  postToParent({
@@ -931,6 +1017,27 @@ function getCmsParentTargetOrigin(preferredCmsUrl, explicitParentOrigin) {
931
1017
 
932
1018
  // lib/block-renderer.tsx
933
1019
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1020
+ function extractContentValues(content, basePath = []) {
1021
+ const map = /* @__PURE__ */ new Map();
1022
+ function walk(obj, path) {
1023
+ if (typeof obj === "string" && obj.trim() !== "") {
1024
+ const contentPath = path.join(".");
1025
+ const existing = map.get(obj) || [];
1026
+ existing.push({ contentPath, value: obj });
1027
+ map.set(obj, existing);
1028
+ } else if (Array.isArray(obj)) {
1029
+ for (let index = 0; index < obj.length; index++) {
1030
+ walk(obj[index], [...path, String(index)]);
1031
+ }
1032
+ } else if (obj && typeof obj === "object") {
1033
+ for (const [key, value] of Object.entries(obj)) {
1034
+ walk(value, [...path, key]);
1035
+ }
1036
+ }
1037
+ }
1038
+ walk(content, basePath);
1039
+ return map;
1040
+ }
934
1041
  function CmsEditableInit({
935
1042
  cmsUrl,
936
1043
  cmsParentOrigin
@@ -979,6 +1086,7 @@ function BlockRenderer({
979
1086
  block,
980
1087
  registry,
981
1088
  disableEditable,
1089
+ enableContentEditable,
982
1090
  routeParams,
983
1091
  path
984
1092
  }) {
@@ -994,6 +1102,7 @@ function BlockRenderer({
994
1102
  if (disableEditable) {
995
1103
  return component;
996
1104
  }
1105
+ const contentEntries = enableContentEditable ? [...extractContentValues(block.content).values()].flat().map(({ value, contentPath }) => ({ v: value, p: contentPath })) : void 0;
997
1106
  return /* @__PURE__ */ jsxs(Fragment, { children: [
998
1107
  /* @__PURE__ */ jsx(
999
1108
  "span",
@@ -1001,6 +1110,7 @@ function BlockRenderer({
1001
1110
  "data-cms-sentinel": "",
1002
1111
  "data-block-id": block.id,
1003
1112
  "data-block-type": block.type,
1113
+ "data-content-entries": contentEntries ? JSON.stringify(contentEntries) : void 0,
1004
1114
  style: { display: "none" },
1005
1115
  "aria-hidden": "true"
1006
1116
  }
@@ -1034,7 +1144,7 @@ async function renderParametricRoute({
1034
1144
  apiKey,
1035
1145
  cmsUrl,
1036
1146
  websiteId: providedWebsiteId
1037
- }) {
1147
+ }, { preview = false } = {}) {
1038
1148
  const websiteId = getWebsiteId(providedWebsiteId);
1039
1149
  const { slug } = "then" in params ? await params : params;
1040
1150
  const resolvedSearchParams = searchParams && "then" in searchParams ? await searchParams : searchParams;
@@ -1049,10 +1159,11 @@ async function renderParametricRoute({
1049
1159
  }
1050
1160
  }
1051
1161
  }
1052
- const editModeParam = resolvedSearchParams?.edit_mode;
1053
- const editMode = editModeParam === true || editModeParam === "true" || editModeParam === "1";
1054
1162
  const cmsParentOriginParam = resolvedSearchParams?.cms_parent_origin;
1055
1163
  const cmsParentOrigin = typeof cmsParentOriginParam === "boolean" ? void 0 : Array.isArray(cmsParentOriginParam) ? cmsParentOriginParam[0] : cmsParentOriginParam;
1164
+ const previewMode = preview === true;
1165
+ const editModeParam = resolvedSearchParams?.edit_mode;
1166
+ const editMode = previewMode || editModeParam === true || editModeParam === "true" || editModeParam === "1";
1056
1167
  const rawPath = `/${slug.join("/")}`;
1057
1168
  const path = normalizePath(rawPath);
1058
1169
  if (/\.[a-zA-Z0-9]+$/.test(path)) {
@@ -1092,7 +1203,9 @@ async function renderParametricRoute({
1092
1203
  ]);
1093
1204
  const blocks = [];
1094
1205
  for (const block of blockResults) {
1095
- if (!block || block.published_content === null) continue;
1206
+ if (!block) continue;
1207
+ const blockContent = previewMode ? block.draft_content : block.published_content;
1208
+ if (blockContent == null) continue;
1096
1209
  let content = null;
1097
1210
  if (aiPreviewIndex !== null) {
1098
1211
  const generatedBlock = generatedBlocks[block.id];
@@ -1102,7 +1215,7 @@ async function renderParametricRoute({
1102
1215
  content = variants[variantIndex].content;
1103
1216
  }
1104
1217
  }
1105
- content = content ?? block.published_content;
1218
+ content = content ?? blockContent;
1106
1219
  if (!content) continue;
1107
1220
  if (block.schema_name === "article") {
1108
1221
  const article = normalizeArticleContent(content);
@@ -1150,6 +1263,7 @@ async function renderParametricRoute({
1150
1263
  block,
1151
1264
  registry: registry ?? {},
1152
1265
  disableEditable: !editMode,
1266
+ enableContentEditable: previewMode,
1153
1267
  routeParams,
1154
1268
  path
1155
1269
  },
@@ -1178,6 +1292,16 @@ async function ParametricRoutePage(props) {
1178
1292
  }
1179
1293
  return result.node;
1180
1294
  }
1295
+ async function ParametricRoutePreviewPage(props) {
1296
+ const result = await renderParametricRoute(props, { preview: true });
1297
+ if (result.status === "not_found") {
1298
+ notFound();
1299
+ }
1300
+ if (result.status === "error") {
1301
+ throw result.error;
1302
+ }
1303
+ return result.node;
1304
+ }
1181
1305
  async function generateMetadata({
1182
1306
  params,
1183
1307
  apiKey,
@@ -1204,6 +1328,7 @@ async function generateMetadata({
1204
1328
  }
1205
1329
  }
1206
1330
  export {
1331
+ ParametricRoutePreviewPage,
1207
1332
  ParametricRoutePage as default,
1208
1333
  generateMetadata
1209
1334
  };