@duffcloudservices/cms 0.1.3 → 0.1.5

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.
@@ -20,6 +20,15 @@ var arrayEditIconElement = null;
20
20
  var arrayEditIconTarget = null;
21
21
  var editingEnabled = true;
22
22
  var originalTextValues = /* @__PURE__ */ new Map();
23
+ var sectionResizeObserver = null;
24
+ var rediscoveryTimer = null;
25
+ function scheduleRediscovery() {
26
+ if (rediscoveryTimer) clearTimeout(rediscoveryTimer);
27
+ rediscoveryTimer = setTimeout(() => {
28
+ rediscoveryTimer = null;
29
+ rediscoverAndNotify();
30
+ }, 300);
31
+ }
23
32
  function discoverSections() {
24
33
  const elements = document.querySelectorAll("[data-section]");
25
34
  return Array.from(elements).map((el) => {
@@ -43,6 +52,25 @@ function discoverTextKeys() {
43
52
  (el) => el.dataset.textKey
44
53
  );
45
54
  }
55
+ function observeSectionLayout() {
56
+ if (sectionResizeObserver) {
57
+ sectionResizeObserver.disconnect();
58
+ }
59
+ const allImages = document.querySelectorAll("img");
60
+ allImages.forEach((img) => {
61
+ if (!img.complete) {
62
+ img.addEventListener("load", scheduleRediscovery, { once: true });
63
+ img.addEventListener("error", scheduleRediscovery, { once: true });
64
+ }
65
+ });
66
+ if (typeof ResizeObserver !== "undefined") {
67
+ sectionResizeObserver = new ResizeObserver(scheduleRediscovery);
68
+ sectionResizeObserver.observe(document.body);
69
+ document.querySelectorAll("[data-section]").forEach((el) => {
70
+ sectionResizeObserver.observe(el);
71
+ });
72
+ }
73
+ }
46
74
  function showSectionHighlight(_sectionId) {
47
75
  }
48
76
  function monitorNavigation() {
@@ -104,12 +132,13 @@ function rediscoverAndNotify() {
104
132
  activeEditElement = null;
105
133
  const sections = discoverSections();
106
134
  const textKeys = discoverTextKeys();
107
- const contentHeight = document.body.scrollHeight;
135
+ const contentHeight = measureContentHeight(sections);
108
136
  postToParent("dcs:ready", { sections, textKeys, contentHeight });
109
137
  if (editorActive) {
110
138
  editorActive = false;
111
139
  enableEditorMode();
112
140
  }
141
+ observeSectionLayout();
113
142
  }
114
143
  function enableEditorMode() {
115
144
  if (editorActive) return;
@@ -477,8 +506,32 @@ function handleMessage(event) {
477
506
  break;
478
507
  }
479
508
  }
509
+ function injectEditorHeightFix() {
510
+ if (document.getElementById("dcs-editor-height-fix")) return;
511
+ const style = document.createElement("style");
512
+ style.id = "dcs-editor-height-fix";
513
+ style.textContent = `
514
+ /* DCS Editor Bridge: Neutralize viewport-relative min-heights */
515
+ .min-h-screen, .min-h-dvh, .min-h-svh,
516
+ .min-h-\\[100vh\\], .min-h-\\[100dvh\\], .min-h-\\[100svh\\] {
517
+ min-height: 0 !important;
518
+ }
519
+ `;
520
+ document.head.appendChild(style);
521
+ }
522
+ function measureContentHeight(sections) {
523
+ let maxBottom = 0;
524
+ for (const s of sections) {
525
+ const bottom = s.bounds.y + s.bounds.height;
526
+ if (bottom > maxBottom) maxBottom = bottom;
527
+ }
528
+ const sectionHeight = maxBottom > 0 ? Math.ceil(maxBottom) : 0;
529
+ const bodyHeight = document.body.scrollHeight;
530
+ return Math.max(sectionHeight, bodyHeight);
531
+ }
480
532
  function initEditorBridge() {
481
533
  if (!isInIframe()) return;
534
+ injectEditorHeightFix();
482
535
  if (!bridgeInitialized) {
483
536
  bridgeInitialized = true;
484
537
  monitorNavigation();
@@ -504,12 +557,13 @@ function initEditorBridge() {
504
557
  }
505
558
  const sections = discoverSections();
506
559
  const textKeys = discoverTextKeys();
507
- const contentHeight = document.body.scrollHeight;
560
+ const contentHeight = measureContentHeight(sections);
508
561
  postToParent("dcs:ready", {
509
562
  sections,
510
563
  textKeys,
511
564
  contentHeight
512
565
  });
566
+ observeSectionLayout();
513
567
  }
514
568
  if (typeof globalThis.self !== "undefined" && isInIframe()) {
515
569
  if (document.readyState === "loading") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/editorBridge.ts"],"names":[],"mappings":";AAyDA,SAAS,UAAA,GAAsB;AAC7B,EAAA,IAAI;AAAE,IAAA,OAAO,UAAA,CAAW,IAAA,KAAS,UAAA,CAAW,IAAA,CAAK,MAAA;AAAA,EAAO,CAAA,CAAA,MAAQ;AAAE,IAAA,OAAO,IAAA;AAAA,EAAK;AAChF;AAEA,SAAS,YAAA,CAAa,MAA2B,IAAA,EAAe;AAC9D,EAAA,IAAI,CAAC,YAAW,EAAG;AAKnB,EAAA,UAAA,CAAW,KAAK,MAAA,CAAO,WAAA,CAAY,EAAE,IAAA,EAAM,IAAA,IAAQ,GAAG,CAAA;AACxD;AAIA,IAAI,YAAA,GAAe,KAAA;AACnB,IAAI,iBAAA,GAAoB,KAAA;AACxB,IAAI,iBAAA,GAAwC,IAAA;AAE5C,IAAI,iBAAA,GAA4B,EAAA;AAEhC,IAAI,eAAA,GAAsC,IAAA;AAE1C,IAAI,cAAA,GAAqC,IAAA;AAEzC,IAAI,oBAAA,GAA2C,IAAA;AAE/C,IAAI,mBAAA,GAA0C,IAAA;AAE9C,IAAI,cAAA,GAAiB,IAAA;AAErB,IAAM,kBAAA,uBAAyB,GAAA,EAAyB;AAIxD,SAAS,gBAAA,GAAkC;AACzC,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,gBAAA,CAA8B,gBAAgB,CAAA;AACxE,EAAA,OAAO,MAAM,IAAA,CAAK,QAAQ,CAAA,CAAE,GAAA,CAAI,CAAC,EAAA,KAAO;AACtC,IAAA,MAAM,IAAA,GAAO,GAAG,qBAAA,EAAsB;AACtC,IAAA,MAAM,YAAA,GAAe,EAAA,CAAG,gBAAA,CAAiB,iBAAiB,CAAA,CAAE,MAAA;AAC5D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,GAAG,OAAA,CAAQ,OAAA;AAAA,MACf,KAAA,EAAO,EAAA,CAAG,OAAA,CAAQ,YAAA,IAAgB,IAAA;AAAA,MAClC,MAAA,EAAQ;AAAA,QACN,CAAA,EAAG,KAAK,IAAA,GAAO,OAAA;AAAA,QACf,CAAA,EAAG,KAAK,GAAA,GAAM,OAAA;AAAA,QACd,OAAO,IAAA,CAAK,KAAA;AAAA,QACZ,QAAQ,IAAA,CAAK;AAAA,OACf;AAAA,MACA;AAAA,KACF;AAAA,EACF,CAAC,CAAA;AACH;AAEA,SAAS,gBAAA,GAA6B;AACpC,EAAA,OAAO,MAAM,IAAA,CAAK,QAAA,CAAS,gBAAA,CAA8B,iBAAiB,CAAC,CAAA,CAAE,GAAA;AAAA,IAC3E,CAAC,EAAA,KAAO,EAAA,CAAG,OAAA,CAAQ;AAAA,GACrB;AACF;AAUA,SAAS,qBAAqB,UAAA,EAAoB;AAElD;AAuBA,SAAS,iBAAA,GAAoB;AAC3B,EAAA,iBAAA,GAAoB,WAAW,QAAA,CAAS,QAAA;AAMxC,EAAA,WAAA,CAAY,MAAM;AAChB,IAAA,kBAAA,EAAmB;AAAA,EACrB,GAAG,GAAG,CAAA;AAGN,EAAA,UAAA,CAAW,gBAAA,CAAiB,YAAY,MAAM;AAC5C,IAAA,kBAAA,EAAmB;AAAA,EACrB,CAAC,CAAA;AAGD,EAAA,MAAM,eAAA,GAAkB,CAAC,CAAA,KAAkB;AACzC,IAAA,MAAM,IAAA,GAAQ,CAAA,CAAE,MAAA,CAAuB,OAAA,GAAU,SAAS,CAAA;AAC1D,IAAA,IAAI,CAAC,IAAA,EAAM;AAEX,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,YAAA,CAAa,MAAM,CAAA;AACrC,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,WAAW,GAAG,CAAA,IAAK,SAAS,oBAAA,EAAsB;AAGpE,IAAA,IAAI;AACF,MAAA,MAAM,WAAW,IAAI,GAAA,CAAI,IAAA,EAAM,UAAA,CAAW,SAAS,IAAI,CAAA;AACvD,MAAA,IAAI,QAAA,CAAS,MAAA,KAAW,UAAA,CAAW,QAAA,CAAS,MAAA,EAAQ;AAElD,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,QAAA,CAAA,CAAE,wBAAA,EAAyB;AAAA,MAC7B;AAAA,IAGF,CAAA,CAAA,MAAQ;AAEN,MAAA,CAAA,CAAE,cAAA,EAAe;AAAA,IACnB;AAAA,EACF,CAAA;AAGA,EAAA,UAAA,CAAW,gBAAA,CAAiB,OAAA,EAAS,eAAA,EAAiB,IAAI,CAAA;AAC1D,EAAA,QAAA,CAAS,gBAAA,CAAiB,OAAA,EAAS,eAAA,EAAiB,IAAI,CAAA;AAGxD,EAAA,QAAA,CAAS,gBAAA,CAAiB,QAAA,EAAU,CAAC,CAAA,KAAa;AAChD,IAAA,CAAA,CAAE,cAAA,EAAe;AACjB,IAAA,CAAA,CAAE,eAAA,EAAgB;AAAA,EACpB,GAAG,IAAI,CAAA;AAGP,EAAA,MAAA,CAAO,gBAAA,CAAiB,cAAA,EAAgB,CAAC,CAAA,KAAyB;AAChE,IAAA,CAAA,CAAE,cAAA,EAAe;AAAA,EACnB,CAAC,CAAA;AACH;AAMA,SAAS,kBAAA,GAAqB;AAC5B,EAAA,MAAM,eAAA,GAAkB,WAAW,QAAA,CAAS,QAAA;AAC5C,EAAA,IAAI,oBAAoB,iBAAA,EAAmB;AAE3C,EAAA,iBAAA,GAAoB,eAAA;AAGpB,EAAA,YAAA,CAAa,gBAAA,EAAkB,EAAE,QAAA,EAAU,eAAA,EAAiB,CAAA;AAG5D,EAAA,IAAI,iBAAA,EAAmB;AACrB,IAAA,UAAA,CAAW,iBAAiB,CAAA;AAAA,EAC9B;AACA,EAAA,cAAA,EAAe;AACf,EAAA,mBAAA,EAAoB;AAIpB,EAAA,UAAA,CAAW,MAAM;AACf,IAAA,mBAAA,EAAoB;AAAA,EACtB,GAAG,GAAG,CAAA;AACR;AASA,SAAS,QAAQ,IAAA,EAA0B;AACzC,EAAA,IAAI,SAAS,SAAA,EAAW;AAEtB,IAAA,IAAI,iBAAA,EAAmB;AACrB,MAAA,UAAA,CAAW,iBAAiB,CAAA;AAAA,IAC9B;AAAA,EACF;AACF;AAQA,SAAS,mBAAA,GAAsB;AAC7B,EAAA,iBAAA,GAAoB,IAAA;AAEpB,EAAA,MAAM,WAAW,gBAAA,EAAiB;AAClC,EAAA,MAAM,WAAW,gBAAA,EAAiB;AAClC,EAAA,MAAM,aAAA,GAAgB,SAAS,IAAA,CAAK,YAAA;AACpC,EAAA,YAAA,CAAa,WAAA,EAAa,EAAE,QAAA,EAAU,QAAA,EAAU,eAA4C,CAAA;AAG5F,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,YAAA,GAAe,KAAA;AACf,IAAA,gBAAA,EAAiB;AAAA,EACnB;AACF;AAEA,SAAS,gBAAA,GAAmB;AAC1B,EAAA,IAAI,YAAA,EAAc;AAClB,EAAA,YAAA,GAAe,IAAA;AAGf,EAAA,kBAAA,EAAmB;AAGnB,EAAA,MAAM,YAAA,GAAe,QAAA,CAAS,gBAAA,CAA8B,iBAAiB,CAAA;AAC7E,EAAA,YAAA,CAAa,OAAA,CAAQ,CAAC,MAAA,KAAW;AAC/B,IAAA,MAAA,CAAO,SAAA,CAAU,IAAI,cAAc,CAAA;AAGnC,IAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,qBAAqB,CAAA;AAEzD,IAAA,MAAA,CAAO,gBAAA,CAAiB,QAAQ,iBAAiB,CAAA;AAEjD,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,oBAAoB,CAAA;AAGvD,IAAA,MAAA,CAAO,gBAAA,CAAiB,cAAc,MAAM;AAE1C,MAAA,IAAI,iBAAA,EAAmB;AACvB,MAAA,MAAM,GAAA,GAAM,OAAO,OAAA,CAAQ,OAAA;AAC3B,MAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,QAAA,iBAAA,CAAkB,MAAM,CAAA;AAAA,MAC1B,CAAA,MAAO;AACL,QAAA,YAAA,CAAa,MAAM,CAAA;AAAA,MACrB;AAAA,IACF,CAAC,CAAA;AACD,IAAA,MAAA,CAAO,gBAAA,CAAiB,YAAA,EAAc,CAAC,CAAA,KAAkB;AAEvD,MAAA,MAAM,UAAU,CAAA,CAAE,aAAA;AAClB,MAAA,IAAI,YACF,OAAA,CAAQ,SAAA,EAAW,SAAS,eAAe,CAAA,IAAK,QAAQ,OAAA,GAAU,gBAAgB,CAAA,IAClF,OAAA,CAAQ,WAAW,QAAA,CAAS,qBAAqB,KAAK,OAAA,CAAQ,OAAA,GAAU,sBAAsB,CAAA,CAAA,EAC7F;AACH,MAAA,cAAA,EAAe;AACf,MAAA,mBAAA,EAAoB;AAAA,IACtB,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AAGD,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,gBAAA,CAA8B,gBAAgB,CAAA;AACxE,EAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,EAAA,KAAO;AACvB,IAAA,MAAM,SAAA,GAAY,GAAG,OAAA,CAAQ,OAAA;AAE7B,IAAA,EAAA,CAAG,gBAAA,CAAiB,cAAc,MAAM;AACtC,MAAA,YAAA,CAAa,mBAAA,EAAqB,EAAE,SAAA,EAAW,CAAA;AAAA,IACjD,CAAC,CAAA;AACD,IAAA,EAAA,CAAG,gBAAA,CAAiB,cAAc,MAAM;AACtC,MAAA,YAAA,CAAa,mBAAA,EAAqB,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAAA,IACvD,CAAC,CAAA;AACD,IAAA,EAAA,CAAG,gBAAA,CAAiB,OAAA,EAAS,CAAC,CAAA,KAAM;AAElC,MAAA,MAAM,SAAS,CAAA,CAAE,MAAA;AACjB,MAAA,IAAI,CAAC,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA,EAAG;AACtC,QAAA,YAAA,CAAa,mBAAA,EAAqB,EAAE,SAAA,EAAW,CAAA;AAAA,MACjD;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AAMA,SAAS,sBAAsB,CAAA,EAAU;AACvC,EAAA,IAAI,CAAC,cAAA,EAAgB;AAErB,EAAA,CAAA,CAAE,cAAA,EAAe;AACjB,EAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,EAAA,CAAA,CAAE,wBAAA,EAAyB;AAE3B,EAAA,MAAM,KAAK,CAAA,CAAE,aAAA;AACb,EAAA,MAAM,GAAA,GAAM,GAAG,OAAA,CAAQ,OAAA;AACvB,EAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAqB,gBAAgB,CAAA;AAC9D,EAAA,MAAM,SAAA,GAAY,aAAA,EAAe,OAAA,CAAQ,OAAA,IAAW,IAAA;AAIpD,EAAA,YAAA,CAAa,uBAAA,EAAyB,EAAE,GAAA,EAAK,SAAA,EAAW,CAAA;AAGxD,EAAA,eAAA,CAAgB,EAAA,EAAI,KAAK,SAAS,CAAA;AACpC;AAMA,SAAS,eAAA,CAAgB,EAAA,EAAiB,GAAA,EAAa,SAAA,EAA0B;AAE/E,EAAA,IAAI,iBAAA,IAAqB,sBAAsB,EAAA,EAAI;AACjD,IAAA,UAAA,CAAW,iBAAiB,CAAA;AAAA,EAC9B;AAGA,EAAA,kBAAA,CAAmB,GAAA,CAAI,EAAA,EAAI,EAAA,CAAG,SAAA,CAAU,MAAM,CAAA;AAG9C,EAAA,EAAA,CAAG,YAAA,CAAa,mBAAmB,MAAM,CAAA;AACzC,EAAA,EAAA,CAAG,SAAA,CAAU,IAAI,aAAa,CAAA;AAC9B,EAAA,EAAA,CAAG,KAAA,EAAM;AACT,EAAA,iBAAA,GAAoB,EAAA;AAGpB,EAAA,MAAM,YAAY,YAAA,EAAa;AAC/B,EAAA,MAAM,KAAA,GAAQ,SAAS,WAAA,EAAY;AACnC,EAAA,KAAA,CAAM,mBAAmB,EAAE,CAAA;AAC3B,EAAA,SAAA,EAAW,eAAA,EAAgB;AAC3B,EAAA,SAAA,EAAW,SAAS,KAAK,CAAA;AAEzB,EAAA,YAAA,CAAa,oBAAA,EAAsB,EAAE,GAAA,EAAK,SAAA,EAAW,CAAA;AACvD;AAEA,SAAS,kBAAkB,CAAA,EAAU;AACnC,EAAA,MAAM,KAAK,CAAA,CAAE,aAAA;AACb,EAAA,UAAA,CAAW,EAAE,CAAA;AACf;AAEA,SAAS,qBAAqB,CAAA,EAAkB;AAC9C,EAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,IAAW,CAAC,EAAE,QAAA,EAAU;AACpC,IAAA,CAAA,CAAE,cAAA,EAAe;AAChB,IAAC,CAAA,CAAE,cAA8B,IAAA,EAAK;AAAA,EACzC;AACA,EAAA,IAAI,CAAA,CAAE,QAAQ,QAAA,EAAU;AACtB,IAAA,CAAA,CAAE,cAAA,EAAe;AAChB,IAAC,CAAA,CAAE,cAA8B,IAAA,EAAK;AAAA,EACzC;AACF;AAEA,SAAS,WAAW,EAAA,EAAiB;AACnC,EAAA,IAAI,CAAC,EAAA,CAAG,YAAA,CAAa,iBAAiB,CAAA,EAAG;AAEzC,EAAA,EAAA,CAAG,gBAAgB,iBAAiB,CAAA;AACpC,EAAA,EAAA,CAAG,SAAA,CAAU,OAAO,aAAa,CAAA;AAEjC,EAAA,MAAM,GAAA,GAAM,GAAG,OAAA,CAAQ,OAAA;AACvB,EAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAqB,gBAAgB,CAAA;AAC9D,EAAA,MAAM,SAAA,GAAY,aAAA,EAAe,OAAA,CAAQ,OAAA,IAAW,IAAA;AACpD,EAAA,MAAM,QAAA,GAAW,EAAA,CAAG,SAAA,CAAU,IAAA,EAAK;AACnC,EAAA,MAAM,aAAA,GAAgB,kBAAA,CAAmB,GAAA,CAAI,EAAE,CAAA,IAAK,EAAA;AAEpD,EAAA,IAAI,sBAAsB,EAAA,EAAI;AAC5B,IAAA,iBAAA,GAAoB,IAAA;AAAA,EACtB;AAGA,EAAA,IAAI,aAAa,aAAA,EAAe;AAC9B,IAAA,YAAA,CAAa,wBAAwB,EAAE,GAAA,EAAK,KAAA,EAAO,QAAA,EAAU,WAAW,CAAA;AAAA,EAC1E;AAEA,EAAA,kBAAA,CAAmB,OAAO,EAAE,CAAA;AAC9B;AAEA,SAAS,aAAa,GAAA,EAAa;AACjC,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAA2B,CAAA,gBAAA,EAAmB,GAAG,CAAA,EAAA,CAAI,CAAA;AACzE,EAAA,IAAI,CAAC,EAAA,EAAI;AACT,EAAA,EAAA,CAAG,eAAe,EAAE,QAAA,EAAU,QAAA,EAAU,KAAA,EAAO,UAAU,CAAA;AAEzD,EAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAqB,gBAAgB,CAAA;AAC9D,EAAA,MAAM,SAAA,GAAY,aAAA,EAAe,OAAA,CAAQ,OAAA,IAAW,IAAA;AAGpD,EAAA,eAAA,CAAgB,EAAA,EAAI,KAAK,SAAS,CAAA;AACpC;AAEA,SAAS,iBAAA,CAAkB,KAAa,KAAA,EAAe;AACrD,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAA2B,CAAA,gBAAA,EAAmB,GAAG,CAAA,EAAA,CAAI,CAAA;AACzE,EAAA,IAAI,CAAC,EAAA,EAAI;AAET,EAAA,IAAI,OAAO,iBAAA,EAAmB;AAC9B,EAAA,EAAA,CAAG,SAAA,GAAY,KAAA;AACjB;AAUA,SAAS,cAAA,GAA8B;AACrC,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC5C,EAAA,IAAA,CAAK,SAAA,GAAY,eAAA;AACjB,EAAA,IAAA,CAAK,YAAA,CAAa,QAAQ,QAAQ,CAAA;AAClC,EAAA,IAAA,CAAK,YAAA,CAAa,SAAS,WAAW,CAAA;AACtC,EAAA,IAAA,CAAK,YAAA,CAAa,cAAc,kBAAkB,CAAA;AAElD,EAAA,IAAA,CAAK,SAAA,GAAY,CAAA,6SAAA,CAAA;AAEjB,EAAA,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,CAAC,CAAA,KAAM;AACpC,IAAA,CAAA,CAAE,cAAA,EAAe;AACjB,IAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,IAAA,CAAA,CAAE,wBAAA,EAAyB;AAE3B,IAAA,IAAI,CAAC,cAAA,EAAgB;AACrB,IAAA,MAAM,EAAA,GAAK,cAAA;AACX,IAAA,MAAM,GAAA,GAAM,GAAG,OAAA,CAAQ,OAAA;AACvB,IAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAqB,gBAAgB,CAAA;AAC9D,IAAA,MAAM,SAAA,GAAY,aAAA,EAAe,OAAA,CAAQ,OAAA,IAAW,IAAA;AAGpD,IAAA,YAAA,CAAa,uBAAA,EAAyB,EAAE,GAAA,EAAK,SAAA,EAAW,CAAA;AAExD,IAAA,eAAA,CAAgB,EAAA,EAAI,KAAK,SAAS,CAAA;AAElC,IAAA,cAAA,EAAe;AAAA,EACjB,GAAG,IAAI,CAAA;AAGP,EAAA,IAAA,CAAK,gBAAA,CAAiB,YAAA,EAAc,CAAC,CAAA,KAAkB;AACrD,IAAA,MAAM,UAAU,CAAA,CAAE,aAAA;AAClB,IAAA,IAAI,WAAW,cAAA,KAAmB,OAAA,KAAY,kBAAkB,cAAA,CAAe,QAAA,CAAS,OAAO,CAAA,CAAA,EAAI;AACnG,IAAA,cAAA,EAAe;AAAA,EACjB,CAAC,CAAA;AAED,EAAA,OAAO,IAAA;AACT;AAKA,SAAS,aAAa,EAAA,EAAiB;AACrC,EAAA,IAAI,CAAC,cAAA,EAAgB;AAErB,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,eAAA,GAAkB,cAAA,EAAe;AACjC,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,eAAe,CAAA;AAAA,EAC3C;AAEA,EAAA,cAAA,GAAiB,EAAA;AAGjB,EAAA,MAAM,IAAA,GAAO,GAAG,qBAAA,EAAsB;AACtC,EAAA,eAAA,CAAgB,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,GAAA,GAAM,UAAU,CAAC,CAAA,EAAA,CAAA;AACrD,EAAA,eAAA,CAAgB,MAAM,IAAA,GAAO,CAAA,EAAG,IAAA,CAAK,IAAA,GAAO,UAAU,CAAC,CAAA,EAAA,CAAA;AACvD,EAAA,eAAA,CAAgB,MAAM,OAAA,GAAU,MAAA;AAClC;AAKA,SAAS,cAAA,GAAiB;AACxB,EAAA,IAAI,eAAA,EAAiB;AACnB,IAAA,eAAA,CAAgB,MAAM,OAAA,GAAU,MAAA;AAAA,EAClC;AACA,EAAA,cAAA,GAAiB,IAAA;AACnB;AAUA,SAAS,eAAe,GAAA,EAAsB;AAE5C,EAAA,OAAO,UAAU,IAAA,CAAK,GAAG,CAAA,IAAK,aAAA,CAAc,KAAK,GAAG,CAAA;AACtD;AAOA,SAAS,gBAAgB,GAAA,EAA4B;AAEnD,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,KAAA,CAAM,cAAc,CAAA;AAC5C,EAAA,IAAI,WAAA,EAAa,OAAO,WAAA,CAAY,CAAC,CAAA;AAErC,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,eAAe,CAAA;AAC1C,EAAA,OAAO,QAAA,GAAW,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAClC;AAUA,SAAS,mBAAA,GAAmC;AAC1C,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC5C,EAAA,IAAA,CAAK,SAAA,GAAY,qBAAA;AACjB,EAAA,IAAA,CAAK,YAAA,CAAa,QAAQ,QAAQ,CAAA;AAClC,EAAA,IAAA,CAAK,YAAA,CAAa,SAAS,iBAAiB,CAAA;AAC5C,EAAA,IAAA,CAAK,YAAA,CAAa,cAAc,0BAA0B,CAAA;AAE1D,EAAA,IAAA,CAAK,SAAA,GAAY,CAAA,iaAAA,CAAA;AAEjB,EAAA,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,CAAC,CAAA,KAAM;AACpC,IAAA,CAAA,CAAE,cAAA,EAAe;AACjB,IAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,IAAA,CAAA,CAAE,wBAAA,EAAyB;AAE3B,IAAA,IAAI,CAAC,mBAAA,EAAqB;AAC1B,IAAA,MAAM,EAAA,GAAK,mBAAA;AACX,IAAA,MAAM,GAAA,GAAM,GAAG,OAAA,CAAQ,OAAA;AACvB,IAAA,MAAM,QAAA,GAAW,gBAAgB,GAAG,CAAA;AACpC,IAAA,IAAI,CAAC,QAAA,EAAU;AAEf,IAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAqB,gBAAgB,CAAA;AAC9D,IAAA,MAAM,SAAA,GAAY,aAAA,EAAe,OAAA,CAAQ,OAAA,IAAW,IAAA;AAGpD,IAAA,YAAA,CAAa,qBAAA,EAAuB,EAAE,QAAA,EAAU,SAAA,EAAW,CAAA;AAE3D,IAAA,mBAAA,EAAoB;AAAA,EACtB,GAAG,IAAI,CAAA;AAGP,EAAA,IAAA,CAAK,gBAAA,CAAiB,YAAA,EAAc,CAAC,CAAA,KAAkB;AACrD,IAAA,MAAM,UAAU,CAAA,CAAE,aAAA;AAClB,IAAA,IAAI,WAAW,mBAAA,KAAwB,OAAA,KAAY,uBAAuB,mBAAA,CAAoB,QAAA,CAAS,OAAO,CAAA,CAAA,EAAI;AAClH,IAAA,mBAAA,EAAoB;AAAA,EACtB,CAAC,CAAA;AAED,EAAA,OAAO,IAAA;AACT;AAKA,SAAS,kBAAkB,EAAA,EAAiB;AAC1C,EAAA,IAAI,CAAC,cAAA,EAAgB;AAErB,EAAA,IAAI,CAAC,oBAAA,EAAsB;AACzB,IAAA,oBAAA,GAAuB,mBAAA,EAAoB;AAC3C,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,oBAAoB,CAAA;AAAA,EAChD;AAEA,EAAA,mBAAA,GAAsB,EAAA;AAGtB,EAAA,MAAM,IAAA,GAAO,GAAG,qBAAA,EAAsB;AACtC,EAAA,oBAAA,CAAqB,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,GAAA,GAAM,UAAU,CAAC,CAAA,EAAA,CAAA;AAC1D,EAAA,oBAAA,CAAqB,MAAM,IAAA,GAAO,CAAA,EAAG,IAAA,CAAK,IAAA,GAAO,UAAU,CAAC,CAAA,EAAA,CAAA;AAC5D,EAAA,oBAAA,CAAqB,MAAM,OAAA,GAAU,MAAA;AACvC;AAKA,SAAS,mBAAA,GAAsB;AAC7B,EAAA,IAAI,oBAAA,EAAsB;AACxB,IAAA,oBAAA,CAAqB,MAAM,OAAA,GAAU,MAAA;AAAA,EACvC;AACA,EAAA,mBAAA,GAAsB,IAAA;AACxB;AAEA,SAAS,kBAAA,GAAqB;AAC5B,EAAA,IAAI,QAAA,CAAS,cAAA,CAAe,mBAAmB,CAAA,EAAG;AAElD,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,EAAA,KAAA,CAAM,EAAA,GAAK,mBAAA;AACX,EAAA,KAAA,CAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA,EAAA,CAAA;AA2FpB,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AACjC;AAIA,SAAS,cAAc,KAAA,EAAqB;AAG1C,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,EAAA,IAAI,CAAC,OAAO,OAAO,GAAA,KAAQ,YAAY,OAAO,GAAA,CAAI,SAAS,QAAA,EAAU;AACrE,EAAA,IAAI,CAAC,GAAA,CAAI,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA,EAAG;AAElC,EAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAK,GAAI,GAAA;AAEvB,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,iBAAA;AACH,MAAA,gBAAA,EAAiB;AACjB,MAAA;AAAA,IAEF,KAAK,uBAAA;AACH,MAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,eAAe,IAAA,EAAM;AAC3D,QAAA,oBAAA,CAAsB,KAA+B,SAAS,CAAA;AAAA,MAChE;AACA,MAAA;AAAA,IAEF,KAAK,qBAAA;AAEH,MAAA;AAAA,IAEF,KAAK,qBAAA;AACH,MAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,SAAS,IAAA,EAAM;AACrD,QAAA,YAAA,CAAc,KAAyB,GAAG,CAAA;AAAA,MAC5C;AACA,MAAA;AAAA,IAEF,KAAK,iBAAA;AACH,MAAA,IAAI,QAAQ,OAAO,IAAA,KAAS,YAAY,KAAA,IAAS,IAAA,IAAQ,WAAW,IAAA,EAAM;AACxE,QAAA,MAAM,CAAA,GAAI,IAAA;AACV,QAAA,iBAAA,CAAkB,CAAA,CAAE,GAAA,EAAK,CAAA,CAAE,KAAK,CAAA;AAAA,MAClC;AACA,MAAA;AAAA,IAEF,KAAK,cAAA;AACH,MAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,UAAU,IAAA,EAAM;AACtD,QAAA,OAAA,CAAS,KAAsC,IAAI,CAAA;AAAA,MACrD;AACA,MAAA;AAAA,IAEF,KAAK,yBAAA;AACH,MAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,IAAY,aAAa,IAAA,EAAM;AACzD,QAAA,cAAA,GAAkB,IAAA,CAA8B,OAAA;AAChD,QAAA,IAAI,cAAA,EAAgB;AAElB,UAAA,QAAA,CAAS,gBAAA,CAA8B,iBAAiB,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAA,KAAM;AACtE,YAAA,EAAA,CAAG,SAAA,CAAU,IAAI,cAAc,CAAA;AAAA,UACjC,CAAC,CAAA;AAAA,QACH,CAAA,MAAO;AAEL,UAAA,IAAI,iBAAA,EAAmB;AACrB,YAAA,UAAA,CAAW,iBAAiB,CAAA;AAAA,UAC9B;AACA,UAAA,cAAA,EAAe;AACf,UAAA,mBAAA,EAAoB;AAEpB,UAAA,QAAA,CAAS,gBAAA,CAA8B,eAAe,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAA,KAAM;AACpE,YAAA,EAAA,CAAG,SAAA,CAAU,OAAO,cAAc,CAAA;AAAA,UACpC,CAAC,CAAA;AAAA,QACH;AAAA,MACF;AACA,MAAA;AAAA;AAEN;AAIO,SAAS,gBAAA,GAAmB;AACjC,EAAA,IAAI,CAAC,YAAW,EAAG;AAGnB,EAAA,IAAI,CAAC,iBAAA,EAAmB;AACtB,IAAA,iBAAA,GAAoB,IAAA;AAGpB,IAAA,iBAAA,EAAkB;AAKlB,IAAA,QAAA,CAAS,gBAAA,CAAiB,aAAA,EAAe,CAAC,CAAA,KAAkB;AAC1D,MAAA,CAAA,CAAE,cAAA,EAAe;AACjB,MAAA,MAAM,SAAA,GAAa,CAAA,CAAE,MAAA,CAAuB,OAAA,GAAU,gBAAgB,CAAA;AACtE,MAAA,YAAA,CAAa,iBAAA,EAAmB;AAAA,QAC9B,GAAG,CAAA,CAAE,OAAA;AAAA,QACL,GAAG,CAAA,CAAE,OAAA;AAAA,QACL,SAAA,EAAW,SAAA,EAAW,OAAA,CAAQ,OAAA,IAAW,IAAA;AAAA,QACzC,YAAA,EAAc,SAAA,EAAW,OAAA,CAAQ,YAAA,IAAgB;AAAA,OAClD,CAAA;AAAA,IACH,GAAG,IAAI,CAAA;AAIP,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAS,MAAM;AACvC,MAAA,YAAA,CAAa,WAAA,EAAa,EAAE,CAAA;AAAA,IAC9B,GAAG,IAAI,CAAA;AAOP,IAAA,UAAA,CAAW,IAAA,CAAK,gBAAA,CAAiB,SAAA,EAAW,aAAa,CAAA;AAKzD,IAAA,IAAI,YAAY,GAAA,EAAK;AACnB,MAAA,MAAA,CAAA,IAAA,CAAY,GAAA,CAAI,EAAA,CAAG,kBAAA,EAAoB,MAAM;AAE3C,QAAA,UAAA,CAAW,qBAAqB,GAAG,CAAA;AAAA,MACrC,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAGA,EAAA,MAAM,WAAW,gBAAA,EAAiB;AAClC,EAAA,MAAM,WAAW,gBAAA,EAAiB;AAClC,EAAA,MAAM,aAAA,GAAgB,SAAS,IAAA,CAAK,YAAA;AAEpC,EAAA,YAAA,CAAa,WAAA,EAAa;AAAA,IACxB,QAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GAC4B,CAAA;AAChC;AAIA,IAAI,OAAO,UAAA,CAAW,IAAA,KAAS,WAAA,IAAe,YAAW,EAAG;AAC1D,EAAA,IAAI,QAAA,CAAS,eAAe,SAAA,EAAW;AACrC,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,MAAM;AAElD,MAAA,UAAA,CAAW,kBAAkB,GAAG,CAAA;AAAA,IAClC,CAAC,CAAA;AAAA,EACH,CAAA,MAAO;AACL,IAAA,UAAA,CAAW,kBAAkB,GAAG,CAAA;AAAA,EAClC;AACF","file":"editorBridge.js","sourcesContent":["/**\r\n * Editor Bridge — injected into customer site iframes by the DCS visual editor.\r\n *\r\n * Responsibilities:\r\n * 1. Report available sections and text keys to the parent portal\r\n * 2. Enable inline contenteditable editing on data-text-key elements (double-click)\r\n * 3. Render section overlay highlights on hover/select from parent\r\n * 4. Communicate all interactions back to the parent via postMessage\r\n * 5. Monitor navigation within the iframe and notify the parent\r\n * 6. Show floating edit icon on hover for text-key elements\r\n *\r\n * NOTE: AI ✨ buttons are rendered by the portal-side SectionOverlayLayer,\r\n * NOT by this bridge. The bridge only reports section/text key data and\r\n * handles inline text editing.\r\n *\r\n * This script runs inside the iframe (customer site). The parent portal\r\n * communicates via postMessage using the `dcs:*` message protocol.\r\n */\r\n\r\n// ─── Types ───────────────────────────────────────────────────────\r\n\r\nexport interface SectionInfo {\r\n id: string\r\n label: string | null\r\n bounds: { x: number; y: number; width: number; height: number }\r\n textKeyCount: number\r\n}\r\n\r\nexport interface EditorReadyPayload {\r\n sections: SectionInfo[]\r\n textKeys: string[]\r\n contentHeight: number\r\n}\r\n\r\n// ─── Message Protocol ────────────────────────────────────────────\r\n\r\ntype InboundMessageType =\r\n | 'dcs:init-editor'\r\n | 'dcs:highlight-section'\r\n | 'dcs:clear-highlight'\r\n | 'dcs:select-text-key'\r\n | 'dcs:update-text'\r\n | 'dcs:set-mode'\r\n | 'dcs:set-editing-enabled'\r\n\r\ntype OutboundMessageType =\r\n | 'dcs:ready'\r\n | 'dcs:text-key-click'\r\n | 'dcs:text-key-changed'\r\n | 'dcs:section-click'\r\n | 'dcs:section-hover'\r\n | 'dcs:text-key-dblclick'\r\n | 'dcs:contextmenu'\r\n | 'dcs:click'\r\n | 'dcs:navigation'\r\n | 'dcs:array-key-click'\r\n\r\nfunction isInIframe(): boolean {\r\n try { return globalThis.self !== globalThis.self.parent } catch { return true }\r\n}\r\n\r\nfunction postToParent(type: OutboundMessageType, data: unknown) {\r\n if (!isInIframe()) return\r\n // Use '*' because the portal origin varies between dev and production\r\n // and this script doesn't know the parent origin at injection time.\r\n // Security: messages are validated by the portal's useIframeBridge.\r\n // Origin validated via dcs: prefix check on all messages\r\n globalThis.self.parent.postMessage({ type, data }, '*') // NOSONAR\r\n}\r\n\r\n// ─── State ───────────────────────────────────────────────────────\r\n\r\nlet editorActive = false\r\nlet bridgeInitialized = false\r\nlet activeEditElement: HTMLElement | null = null\r\n/** The current pathname tracked for navigation detection */\r\nlet lastKnownPathname: string = ''\r\n/** The floating edit icon element shown on hover over text keys */\r\nlet editIconElement: HTMLElement | null = null\r\n/** The element the edit icon is currently attached to */\r\nlet editIconTarget: HTMLElement | null = null\r\n/** The floating edit icon for array/list text keys */\r\nlet arrayEditIconElement: HTMLElement | null = null\r\n/** The element the array edit icon is currently attached to */\r\nlet arrayEditIconTarget: HTMLElement | null = null\r\n/** Whether editing features are enabled (disabled on unmanaged pages) */\r\nlet editingEnabled = true\r\n/** Original text values captured when inline editing starts, for change detection */\r\nconst originalTextValues = new Map<HTMLElement, string>()\r\n\r\n// ─── Section Discovery ───────────────────────────────────────────\r\n\r\nfunction discoverSections(): SectionInfo[] {\r\n const elements = document.querySelectorAll<HTMLElement>('[data-section]')\r\n return Array.from(elements).map((el) => {\r\n const rect = el.getBoundingClientRect()\r\n const textKeyCount = el.querySelectorAll('[data-text-key]').length\r\n return {\r\n id: el.dataset.section!,\r\n label: el.dataset.sectionLabel ?? null,\r\n bounds: {\r\n x: rect.left + scrollX,\r\n y: rect.top + scrollY,\r\n width: rect.width,\r\n height: rect.height,\r\n },\r\n textKeyCount,\r\n }\r\n })\r\n}\r\n\r\nfunction discoverTextKeys(): string[] {\r\n return Array.from(document.querySelectorAll<HTMLElement>('[data-text-key]')).map(\r\n (el) => el.dataset.textKey!,\r\n )\r\n}\r\n\r\n// ─── Section Overlay ─────────────────────────────────────────────\r\n\r\n/**\r\n * Section highlight is now handled entirely by the portal-side\r\n * SectionOverlayLayer rendered on top of the iframe. The bridge\r\n * only reports section bounds and hover events — no visual\r\n * overlays are rendered inside the iframe itself.\r\n */\r\nfunction showSectionHighlight(_sectionId: string) {\r\n // No-op: portal overlay layer handles section highlights\r\n}\r\n\r\nfunction clearSectionHighlight() {\r\n // No-op: portal overlay layer handles section highlights\r\n}\r\n\r\n// ─── Navigation Monitoring ───────────────────────────────────────\r\n\r\n/**\r\n * Monitor navigation within the iframe and notify the parent portal.\r\n * Instead of blocking all navigation, we allow SPA routing and inform\r\n * the portal when the iframe navigates to a different page. The portal\r\n * then updates its own URL and loads editing context for the new page.\r\n *\r\n * Strategy:\r\n * 1. Poll location.pathname on a short interval — this reliably detects\r\n * SPA navigation regardless of which router or framework is used,\r\n * since frameworks may cache History API references before our script loads.\r\n * 2. Block external/cross-origin link clicks (actual page reloads).\r\n * 3. Block form submissions.\r\n * 4. Monitor popstate for back/forward navigation.\r\n * 5. After navigation, re-discover sections and report them to the portal.\r\n */\r\nfunction monitorNavigation() {\r\n lastKnownPathname = globalThis.location.pathname\r\n\r\n // ── Poll for URL changes — reliable with any SPA router ──────\r\n // Frameworks like VitePress/Vue Router may cache history.pushState before\r\n // our bridge loads, so prototype overrides are unreliable. Polling\r\n // location.pathname directly is simple and always works.\r\n setInterval(() => {\r\n checkForNavigation()\r\n }, 250)\r\n\r\n // ── Catch back/forward navigation ────────────────────────────\r\n globalThis.addEventListener('popstate', () => {\r\n checkForNavigation()\r\n })\r\n\r\n // ── Block external link clicks (cross-origin / full page reloads) ──\r\n const handleLinkClick = (e: MouseEvent) => {\r\n const link = (e.target as HTMLElement).closest?.('a[href]') as HTMLAnchorElement | null\r\n if (!link) return\r\n\r\n const href = link.getAttribute('href')\r\n if (!href || href.startsWith('#') || href === 'javascript:void(0)') return\r\n\r\n // Check if this is an external link (different origin)\r\n try {\r\n const resolved = new URL(href, globalThis.location.href)\r\n if (resolved.origin !== globalThis.location.origin) {\r\n // Block external navigation — can't leave the iframe\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n }\r\n // Same-origin links: let the SPA router handle them.\r\n // The History API override above will detect the navigation.\r\n } catch {\r\n // Malformed URL — block it\r\n e.preventDefault()\r\n }\r\n }\r\n\r\n // Register on window first to beat VitePress's capture-phase handler\r\n globalThis.addEventListener('click', handleLinkClick, true)\r\n document.addEventListener('click', handleLinkClick, true)\r\n\r\n // Intercept form submissions\r\n document.addEventListener('submit', (e: Event) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n }, true)\r\n\r\n // Block actual page reloads (location.href = ..., etc.)\r\n window.addEventListener('beforeunload', (e: BeforeUnloadEvent) => {\r\n e.preventDefault()\r\n })\r\n}\r\n\r\n/**\r\n * Check if the iframe pathname has changed and notify the portal.\r\n * Also re-initializes the editor bridge for the new page.\r\n */\r\nfunction checkForNavigation() {\r\n const currentPathname = globalThis.location.pathname\r\n if (currentPathname === lastKnownPathname) return\r\n\r\n lastKnownPathname = currentPathname\r\n\r\n // Notify parent about the navigation\r\n postToParent('dcs:navigation', { pathname: currentPathname })\r\n\r\n // Clean up current edit state\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n\r\n // Wait for the SPA router to finish rendering the new page,\r\n // then re-discover sections and text keys\r\n setTimeout(() => {\r\n rediscoverAndNotify()\r\n }, 500)\r\n}\r\n\r\n// ─── Mode Management ─────────────────────────────────────────────\r\n\r\n/**\r\n * Set the current interaction mode. Both modes allow normal browsing —\r\n * inline editing works via double-click regardless of mode.\r\n * Switching to explore finishes any active inline edit.\r\n */\r\nfunction setMode(mode: 'edit' | 'explore') {\r\n if (mode === 'explore') {\r\n // When explicitly switching to explore, finish any active inline edit\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n }\r\n}\r\n\r\n// ─── Inline Text Editing ─────────────────────────────────────────\r\n\r\n/**\r\n * Re-discover sections and text keys after an HMR update and\r\n * notify the parent portal so the overlay layer updates.\r\n */\r\nfunction rediscoverAndNotify() {\r\n activeEditElement = null\r\n\r\n const sections = discoverSections()\r\n const textKeys = discoverTextKeys()\r\n const contentHeight = document.body.scrollHeight\r\n postToParent('dcs:ready', { sections, textKeys, contentHeight } satisfies EditorReadyPayload)\r\n\r\n // Re-enable editor mode on the new elements\r\n if (editorActive) {\r\n editorActive = false // allow re-initialization\r\n enableEditorMode()\r\n }\r\n}\r\n\r\nfunction enableEditorMode() {\r\n if (editorActive) return\r\n editorActive = true\r\n\r\n // Inject editor styles\r\n injectEditorStyles()\r\n\r\n // Set up all text key elements — double-click to start editing\r\n const textElements = document.querySelectorAll<HTMLElement>('[data-text-key]')\r\n textElements.forEach((htmlEl) => {\r\n htmlEl.classList.add('dcs-editable')\r\n\r\n // Double-click to start inline editing (signals parent to switch to edit mode)\r\n htmlEl.addEventListener('dblclick', handleTextKeyDblClick)\r\n // Blur to finish editing\r\n htmlEl.addEventListener('blur', handleTextKeyBlur)\r\n // Keyboard: Enter to finish, Escape to cancel\r\n htmlEl.addEventListener('keydown', handleTextKeyKeydown)\r\n\r\n // Show/hide the floating edit icon on hover (T icon for regular text, List icon for array keys)\r\n htmlEl.addEventListener('mouseenter', () => {\r\n // Don't show icon while actively editing this or another element\r\n if (activeEditElement) return\r\n const key = htmlEl.dataset.textKey!\r\n if (isArrayTextKey(key)) {\r\n showArrayEditIcon(htmlEl)\r\n } else {\r\n showEditIcon(htmlEl)\r\n }\r\n })\r\n htmlEl.addEventListener('mouseleave', (e: MouseEvent) => {\r\n // Don't hide if moving to the edit icon or array edit icon\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && (\r\n related.classList?.contains('dcs-edit-icon') || related.closest?.('.dcs-edit-icon') ||\r\n related.classList?.contains('dcs-array-edit-icon') || related.closest?.('.dcs-array-edit-icon')\r\n )) return\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n })\r\n })\r\n\r\n // Set up section hover reporting (portal overlay handles the visual treatment)\r\n const sections = document.querySelectorAll<HTMLElement>('[data-section]')\r\n sections.forEach((el) => {\r\n const sectionId = el.dataset.section!\r\n\r\n el.addEventListener('mouseenter', () => {\r\n postToParent('dcs:section-hover', { sectionId })\r\n })\r\n el.addEventListener('mouseleave', () => {\r\n postToParent('dcs:section-hover', { sectionId: null })\r\n })\r\n el.addEventListener('click', (e) => {\r\n // Only fire section click if not clicking on a text-key element\r\n const target = e.target as HTMLElement\r\n if (!target.closest('[data-text-key]')) {\r\n postToParent('dcs:section-click', { sectionId })\r\n }\r\n })\r\n })\r\n}\r\n\r\n/**\r\n * Handle double-click on a text key element.\r\n * Notifies the parent portal to switch to edit mode and begins inline editing.\r\n */\r\nfunction handleTextKeyDblClick(e: Event) {\r\n if (!editingEnabled) return\r\n\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n const el = e.currentTarget as HTMLElement\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Always notify the parent to switch to edit mode when starting inline editing.\r\n // In explore mode this triggers the mode switch; in edit mode it's a no-op on the parent side.\r\n postToParent('dcs:text-key-dblclick', { key, sectionId })\r\n\r\n // Start inline editing\r\n startInlineEdit(el, key, sectionId)\r\n}\r\n\r\n/**\r\n * Start inline contenteditable editing on a text key element.\r\n * Called by double-click or when the parent sends dcs:select-text-key.\r\n */\r\nfunction startInlineEdit(el: HTMLElement, key: string, sectionId: string | null) {\r\n // Finish any active edit first\r\n if (activeEditElement && activeEditElement !== el) {\r\n finishEdit(activeEditElement)\r\n }\r\n\r\n // Store original text for change detection on finish\r\n originalTextValues.set(el, el.innerText.trim())\r\n\r\n // Enable contenteditable\r\n el.setAttribute('contenteditable', 'true')\r\n el.classList.add('dcs-editing')\r\n el.focus()\r\n activeEditElement = el\r\n\r\n // Select all text for easy replacement\r\n const selection = getSelection()\r\n const range = document.createRange()\r\n range.selectNodeContents(el)\r\n selection?.removeAllRanges()\r\n selection?.addRange(range)\r\n\r\n postToParent('dcs:text-key-click', { key, sectionId })\r\n}\r\n\r\nfunction handleTextKeyBlur(e: Event) {\r\n const el = e.currentTarget as HTMLElement\r\n finishEdit(el)\r\n}\r\n\r\nfunction handleTextKeyKeydown(e: KeyboardEvent) {\r\n if (e.key === 'Enter' && !e.shiftKey) {\r\n e.preventDefault()\r\n ;(e.currentTarget as HTMLElement).blur()\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n ;(e.currentTarget as HTMLElement).blur()\r\n }\r\n}\r\n\r\nfunction finishEdit(el: HTMLElement) {\r\n if (!el.hasAttribute('contenteditable')) return\r\n\r\n el.removeAttribute('contenteditable')\r\n el.classList.remove('dcs-editing')\r\n\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n const newValue = el.innerText.trim()\r\n const originalValue = originalTextValues.get(el) ?? ''\r\n\r\n if (activeEditElement === el) {\r\n activeEditElement = null\r\n }\r\n\r\n // Only report to the portal when text was actually modified\r\n if (newValue !== originalValue) {\r\n postToParent('dcs:text-key-changed', { key, value: newValue, sectionId })\r\n }\r\n\r\n originalTextValues.delete(el)\r\n}\r\n\r\nfunction focusTextKey(key: string) {\r\n const el = document.querySelector<HTMLElement>(`[data-text-key=\"${key}\"]`)\r\n if (!el) return\r\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\r\n\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Start inline editing directly\r\n startInlineEdit(el, key, sectionId)\r\n}\r\n\r\nfunction updateTextInPlace(key: string, value: string) {\r\n const el = document.querySelector<HTMLElement>(`[data-text-key=\"${key}\"]`)\r\n if (!el) return\r\n // Don't update if we're currently editing this element\r\n if (el === activeEditElement) return\r\n el.innerText = value\r\n}\r\n\r\n// ─── Edit Icon (Floating Text Cursor Button) ────────────────────\r\n\r\n/**\r\n * Create the floating edit icon element. This small button appears\r\n * in the top-left corner of a hovered [data-text-key] element,\r\n * providing a click target to start inline editing without triggering\r\n * navigation (important for text inside buttons/links).\r\n */\r\nfunction createEditIcon(): HTMLElement {\r\n const icon = document.createElement('button')\r\n icon.className = 'dcs-edit-icon'\r\n icon.setAttribute('type', 'button')\r\n icon.setAttribute('title', 'Edit text')\r\n icon.setAttribute('aria-label', 'Edit text inline')\r\n // SVG: Type icon (text cursor) — matches lucide \"Type\" icon\r\n icon.innerHTML = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"4 7 4 4 20 4 20 7\"/><line x1=\"9\" y1=\"20\" x2=\"15\" y2=\"20\"/><line x1=\"12\" y1=\"4\" x2=\"12\" y2=\"20\"/></svg>`\r\n\r\n icon.addEventListener('click', (e) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n if (!editIconTarget) return\r\n const el = editIconTarget\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Signal parent to switch to edit mode\r\n postToParent('dcs:text-key-dblclick', { key, sectionId })\r\n // Start inline editing\r\n startInlineEdit(el, key, sectionId)\r\n // Hide the icon while editing\r\n removeEditIcon()\r\n }, true)\r\n\r\n // Hide icon when mouse leaves it (and isn't going to the target element)\r\n icon.addEventListener('mouseleave', (e: MouseEvent) => {\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && editIconTarget && (related === editIconTarget || editIconTarget.contains(related))) return\r\n removeEditIcon()\r\n })\r\n\r\n return icon\r\n}\r\n\r\n/**\r\n * Show the edit icon positioned at the top-left corner of the given element.\r\n */\r\nfunction showEditIcon(el: HTMLElement) {\r\n if (!editingEnabled) return\r\n\r\n if (!editIconElement) {\r\n editIconElement = createEditIcon()\r\n document.body.appendChild(editIconElement)\r\n }\r\n\r\n editIconTarget = el\r\n\r\n // Position relative to the element's bounding box\r\n const rect = el.getBoundingClientRect()\r\n editIconElement.style.top = `${rect.top + scrollY - 4}px`\r\n editIconElement.style.left = `${rect.left + scrollX - 4}px`\r\n editIconElement.style.display = 'flex'\r\n}\r\n\r\n/**\r\n * Hide and detach the edit icon.\r\n */\r\nfunction removeEditIcon() {\r\n if (editIconElement) {\r\n editIconElement.style.display = 'none'\r\n }\r\n editIconTarget = null\r\n}\r\n\r\n// ─── Array Key Detection ─────────────────────────────────────────\r\n\r\n/**\r\n * Check if a text key is part of an array pattern (e.g., \"conditions.0.title\").\r\n * Array text keys contain an indexed segment — either:\r\n * - Pure numeric: `baseKey.N.field` (e.g., `conditions.0.title`)\r\n * - Hyphenated index: `baseKey.prefix-N.field` (e.g., `steps.step-0.title`)\r\n */\r\nfunction isArrayTextKey(key: string): boolean {\r\n // Match pure numeric segment (.0.) or hyphenated index (.prefix-0.)\r\n return /\\.\\d+\\./.test(key) || /\\.\\w+-\\d+\\./.test(key)\r\n}\r\n\r\n/**\r\n * Extract the base array key from an array text key.\r\n * - `conditions.0.title` → `conditions`\r\n * - `steps.step-0.title` → `steps.step`\r\n */\r\nfunction getArrayBaseKey(key: string): string | null {\r\n // Try hyphenated pattern first (more specific)\r\n const hyphenMatch = key.match(/^(.+?)-\\d+\\./)\r\n if (hyphenMatch) return hyphenMatch[1]\r\n // Fall back to pure numeric pattern\r\n const numMatch = key.match(/^(.+?)\\.\\d+\\./)\r\n return numMatch ? numMatch[1] : null\r\n}\r\n\r\n// ─── Array Edit Icon (Floating List Button) ──────────────────────\r\n\r\n/**\r\n * Create the floating array edit icon element. This small button appears\r\n * in the top-left corner of a hovered [data-text-key] element whose key\r\n * matches an array pattern. Clicking it sends dcs:array-key-click to the\r\n * portal to open the array editor sheet.\r\n */\r\nfunction createArrayEditIcon(): HTMLElement {\r\n const icon = document.createElement('button')\r\n icon.className = 'dcs-array-edit-icon'\r\n icon.setAttribute('type', 'button')\r\n icon.setAttribute('title', 'Edit list items')\r\n icon.setAttribute('aria-label', 'Edit list items in panel')\r\n // SVG: List icon — matches lucide \"List\" icon\r\n icon.innerHTML = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"/><line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\"/></svg>`\r\n\r\n icon.addEventListener('click', (e) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n if (!arrayEditIconTarget) return\r\n const el = arrayEditIconTarget\r\n const key = el.dataset.textKey!\r\n const arrayKey = getArrayBaseKey(key)\r\n if (!arrayKey) return\r\n\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Signal parent to open array editor sheet\r\n postToParent('dcs:array-key-click', { arrayKey, sectionId })\r\n // Hide the icon\r\n removeArrayEditIcon()\r\n }, true)\r\n\r\n // Hide icon when mouse leaves it (and isn't going to the target element)\r\n icon.addEventListener('mouseleave', (e: MouseEvent) => {\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && arrayEditIconTarget && (related === arrayEditIconTarget || arrayEditIconTarget.contains(related))) return\r\n removeArrayEditIcon()\r\n })\r\n\r\n return icon\r\n}\r\n\r\n/**\r\n * Show the array edit icon positioned at the top-left corner of the given element.\r\n */\r\nfunction showArrayEditIcon(el: HTMLElement) {\r\n if (!editingEnabled) return\r\n\r\n if (!arrayEditIconElement) {\r\n arrayEditIconElement = createArrayEditIcon()\r\n document.body.appendChild(arrayEditIconElement)\r\n }\r\n\r\n arrayEditIconTarget = el\r\n\r\n // Position relative to the element's bounding box\r\n const rect = el.getBoundingClientRect()\r\n arrayEditIconElement.style.top = `${rect.top + scrollY - 4}px`\r\n arrayEditIconElement.style.left = `${rect.left + scrollX - 4}px`\r\n arrayEditIconElement.style.display = 'flex'\r\n}\r\n\r\n/**\r\n * Hide and detach the array edit icon.\r\n */\r\nfunction removeArrayEditIcon() {\r\n if (arrayEditIconElement) {\r\n arrayEditIconElement.style.display = 'none'\r\n }\r\n arrayEditIconTarget = null\r\n}\r\n\r\nfunction injectEditorStyles() {\r\n if (document.getElementById('dcs-editor-styles')) return\r\n\r\n const style = document.createElement('style')\r\n style.id = 'dcs-editor-styles'\r\n style.textContent = `\r\n [data-text-key].dcs-editable {\r\n cursor: text !important;\r\n border-radius: 4px;\r\n position: relative;\r\n }\r\n\r\n [data-text-key].dcs-editable:hover {\r\n outline: 2px dashed hsl(221.2 83.2% 53.3% / 0.4) !important;\r\n outline-offset: 4px;\r\n background-color: hsl(221.2 83.2% 53.3% / 0.03);\r\n }\r\n\r\n [data-text-key].dcs-editing {\r\n outline: 2px solid hsl(221.2 83.2% 53.3%) !important;\r\n outline-offset: 4px;\r\n background-color: hsl(221.2 83.2% 53.3% / 0.06);\r\n min-height: 1em;\r\n }\r\n\r\n [data-text-key].dcs-editing:focus {\r\n outline: 2px solid hsl(221.2 83.2% 53.3%) !important;\r\n outline-offset: 4px;\r\n }\r\n\r\n .dcs-section-highlight {\r\n pointer-events: none;\r\n }\r\n\r\n /* Floating edit icon button */\r\n .dcs-edit-icon {\r\n position: absolute;\r\n z-index: 99999;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 0;\r\n margin: 0;\r\n border: 1.5px solid hsl(221.2 83.2% 53.3%);\r\n border-radius: 4px;\r\n background: hsl(221.2 83.2% 53.3% / 0.1);\r\n backdrop-filter: blur(4px);\r\n color: hsl(221.2 83.2% 53.3%);\r\n cursor: pointer;\r\n pointer-events: auto;\r\n transition: background 0.15s, transform 0.1s;\r\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\r\n }\r\n\r\n .dcs-edit-icon:hover {\r\n background: hsl(221.2 83.2% 53.3% / 0.25);\r\n transform: scale(1.1);\r\n }\r\n\r\n .dcs-edit-icon svg {\r\n display: block;\r\n }\r\n\r\n /* Floating array edit icon button */\r\n .dcs-array-edit-icon {\r\n position: absolute;\r\n z-index: 99999;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 0;\r\n margin: 0;\r\n border: 1.5px solid hsl(271 91% 65%);\r\n border-radius: 4px;\r\n background: hsl(271 91% 65% / 0.1);\r\n backdrop-filter: blur(4px);\r\n color: hsl(271 91% 65%);\r\n cursor: pointer;\r\n pointer-events: auto;\r\n transition: background 0.15s, transform 0.1s;\r\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\r\n }\r\n\r\n .dcs-array-edit-icon:hover {\r\n background: hsl(271 91% 65% / 0.25);\r\n transform: scale(1.1);\r\n }\r\n\r\n .dcs-array-edit-icon svg {\r\n display: block;\r\n }\r\n `\r\n document.head.appendChild(style)\r\n}\r\n\r\n// ─── Inbound Message Handler ─────────────────────────────────────\r\n\r\nfunction handleMessage(event: MessageEvent) {\r\n // Validate message structure (origin is not checked because\r\n // the parent portal origin varies between dev and production)\r\n const msg = event.data\r\n if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return\r\n if (!msg.type.startsWith('dcs:')) return\r\n\r\n const { type, data } = msg as { type: InboundMessageType; data: unknown }\r\n\r\n switch (type) {\r\n case 'dcs:init-editor':\r\n enableEditorMode()\r\n break\r\n\r\n case 'dcs:highlight-section':\r\n if (data && typeof data === 'object' && 'sectionId' in data) {\r\n showSectionHighlight((data as { sectionId: string }).sectionId)\r\n }\r\n break\r\n\r\n case 'dcs:clear-highlight':\r\n clearSectionHighlight()\r\n break\r\n\r\n case 'dcs:select-text-key':\r\n if (data && typeof data === 'object' && 'key' in data) {\r\n focusTextKey((data as { key: string }).key)\r\n }\r\n break\r\n\r\n case 'dcs:update-text':\r\n if (data && typeof data === 'object' && 'key' in data && 'value' in data) {\r\n const d = data as { key: string; value: string }\r\n updateTextInPlace(d.key, d.value)\r\n }\r\n break\r\n\r\n case 'dcs:set-mode':\r\n if (data && typeof data === 'object' && 'mode' in data) {\r\n setMode((data as { mode: 'edit' | 'explore' }).mode)\r\n }\r\n break\r\n\r\n case 'dcs:set-editing-enabled':\r\n if (data && typeof data === 'object' && 'enabled' in data) {\r\n editingEnabled = (data as { enabled: boolean }).enabled\r\n if (editingEnabled) {\r\n // Re-add dcs-editable class to text key elements\r\n document.querySelectorAll<HTMLElement>('[data-text-key]').forEach(el => {\r\n el.classList.add('dcs-editable')\r\n })\r\n } else {\r\n // Clean up: finish any active edit, remove edit icon, remove hover styles\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n // Remove dcs-editable class to disable hover outlines\r\n document.querySelectorAll<HTMLElement>('.dcs-editable').forEach(el => {\r\n el.classList.remove('dcs-editable')\r\n })\r\n }\r\n }\r\n break\r\n }\r\n}\r\n\r\n// ─── Bootstrap ───────────────────────────────────────────────────\r\n\r\nexport function initEditorBridge() {\r\n if (!isInIframe()) return // Not in iframe\r\n\r\n // Prevent duplicate listener registration on HMR re-execution\r\n if (!bridgeInitialized) {\r\n bridgeInitialized = true\r\n\r\n // Monitor navigation within the iframe — notify the portal of page changes\r\n monitorNavigation()\r\n\r\n // Forward right-click events to the portal so the context menu\r\n // works when right-clicking inside the iframe (cross-origin boundary\r\n // prevents the parent from receiving native contextmenu events).\r\n document.addEventListener('contextmenu', (e: MouseEvent) => {\r\n e.preventDefault()\r\n const sectionEl = (e.target as HTMLElement).closest?.('[data-section]') as HTMLElement | null\r\n postToParent('dcs:contextmenu', {\r\n x: e.clientX,\r\n y: e.clientY,\r\n sectionId: sectionEl?.dataset.section ?? null,\r\n sectionLabel: sectionEl?.dataset.sectionLabel ?? null,\r\n })\r\n }, true)\r\n\r\n // Forward click events to the portal so it can dismiss context menus\r\n // when the user clicks inside the iframe.\r\n document.addEventListener('click', () => {\r\n postToParent('dcs:click', {})\r\n }, true)\r\n\r\n // Listen for portal commands. Origin is not validated because\r\n // the portal origin varies between environments and all inbound\r\n // messages are type-checked before processing.\r\n // Origin cannot be pre-validated because the portal URL varies by environment.\r\n // All inbound messages are type-checked via the dcs: prefix guard.\r\n globalThis.self.addEventListener('message', handleMessage) // NOSONAR\r\n\r\n // Watch for VitePress / Vite HMR page updates. When the framework\r\n // replaces DOM content, our event listeners and AI buttons are lost.\r\n // Re-discover sections after each update so the portal overlay stays in sync.\r\n if (import.meta.hot) {\r\n import.meta.hot.on('vite:afterUpdate', () => {\r\n // Small delay for the framework to finish DOM updates\r\n setTimeout(rediscoverAndNotify, 300)\r\n })\r\n }\r\n }\r\n\r\n // Report available sections, text keys, and content height\r\n const sections = discoverSections()\r\n const textKeys = discoverTextKeys()\r\n const contentHeight = document.body.scrollHeight\r\n\r\n postToParent('dcs:ready', {\r\n sections,\r\n textKeys,\r\n contentHeight,\r\n } satisfies EditorReadyPayload)\r\n}\r\n\r\n// Auto-initialize when the script is loaded (e.g., injected by Vite plugin)\r\n// Delay slightly to ensure the page is fully rendered\r\nif (typeof globalThis.self !== 'undefined' && isInIframe()) {\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', () => {\r\n // Small delay for Vue/framework hydration\r\n setTimeout(initEditorBridge, 200)\r\n })\r\n } else {\r\n setTimeout(initEditorBridge, 200)\r\n }\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/editor/editorBridge.ts"],"names":[],"mappings":"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","file":"editorBridge.js","sourcesContent":["/**\r\n * Editor Bridge — injected into customer site iframes by the DCS visual editor.\r\n *\r\n * Responsibilities:\r\n * 1. Report available sections and text keys to the parent portal\r\n * 2. Enable inline contenteditable editing on data-text-key elements (double-click)\r\n * 3. Render section overlay highlights on hover/select from parent\r\n * 4. Communicate all interactions back to the parent via postMessage\r\n * 5. Monitor navigation within the iframe and notify the parent\r\n * 6. Show floating edit icon on hover for text-key elements\r\n *\r\n * NOTE: AI ✨ buttons are rendered by the portal-side SectionOverlayLayer,\r\n * NOT by this bridge. The bridge only reports section/text key data and\r\n * handles inline text editing.\r\n *\r\n * This script runs inside the iframe (customer site). The parent portal\r\n * communicates via postMessage using the `dcs:*` message protocol.\r\n */\r\n\r\n// ─── Types ───────────────────────────────────────────────────────\r\n\r\nexport interface SectionInfo {\r\n id: string\r\n label: string | null\r\n bounds: { x: number; y: number; width: number; height: number }\r\n textKeyCount: number\r\n}\r\n\r\nexport interface EditorReadyPayload {\r\n sections: SectionInfo[]\r\n textKeys: string[]\r\n contentHeight: number\r\n}\r\n\r\n// ─── Message Protocol ────────────────────────────────────────────\r\n\r\ntype InboundMessageType =\r\n | 'dcs:init-editor'\r\n | 'dcs:highlight-section'\r\n | 'dcs:clear-highlight'\r\n | 'dcs:select-text-key'\r\n | 'dcs:update-text'\r\n | 'dcs:set-mode'\r\n | 'dcs:set-editing-enabled'\r\n\r\ntype OutboundMessageType =\r\n | 'dcs:ready'\r\n | 'dcs:text-key-click'\r\n | 'dcs:text-key-changed'\r\n | 'dcs:section-click'\r\n | 'dcs:section-hover'\r\n | 'dcs:text-key-dblclick'\r\n | 'dcs:contextmenu'\r\n | 'dcs:click'\r\n | 'dcs:navigation'\r\n | 'dcs:array-key-click'\r\n\r\nfunction isInIframe(): boolean {\r\n try { return globalThis.self !== globalThis.self.parent } catch { return true }\r\n}\r\n\r\nfunction postToParent(type: OutboundMessageType, data: unknown) {\r\n if (!isInIframe()) return\r\n // Use '*' because the portal origin varies between dev and production\r\n // and this script doesn't know the parent origin at injection time.\r\n // Security: messages are validated by the portal's useIframeBridge.\r\n // Origin validated via dcs: prefix check on all messages\r\n globalThis.self.parent.postMessage({ type, data }, '*') // NOSONAR\r\n}\r\n\r\n// ─── State ───────────────────────────────────────────────────────\r\n\r\nlet editorActive = false\r\nlet bridgeInitialized = false\r\nlet activeEditElement: HTMLElement | null = null\r\n/** The current pathname tracked for navigation detection */\r\nlet lastKnownPathname: string = ''\r\n/** The floating edit icon element shown on hover over text keys */\r\nlet editIconElement: HTMLElement | null = null\r\n/** The element the edit icon is currently attached to */\r\nlet editIconTarget: HTMLElement | null = null\r\n/** The floating edit icon for array/list text keys */\r\nlet arrayEditIconElement: HTMLElement | null = null\r\n/** The element the array edit icon is currently attached to */\r\nlet arrayEditIconTarget: HTMLElement | null = null\r\n/** Whether editing features are enabled (disabled on unmanaged pages) */\r\nlet editingEnabled = true\r\n/** Original text values captured when inline editing starts, for change detection */\r\nconst originalTextValues = new Map<HTMLElement, string>()\r\n/** ResizeObserver for tracking section layout changes */\r\nlet sectionResizeObserver: ResizeObserver | null = null\r\n/** Debounce timer for re-discovery triggered by layout changes */\r\nlet rediscoveryTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n/** Debounced re-discovery — collapses rapid layout changes into one update */\r\nfunction scheduleRediscovery() {\r\n if (rediscoveryTimer) clearTimeout(rediscoveryTimer)\r\n rediscoveryTimer = setTimeout(() => {\r\n rediscoveryTimer = null\r\n rediscoverAndNotify()\r\n }, 300)\r\n}\r\n\r\n// ─── Section Discovery ───────────────────────────────────────────\r\n\r\nfunction discoverSections(): SectionInfo[] {\r\n const elements = document.querySelectorAll<HTMLElement>('[data-section]')\r\n return Array.from(elements).map((el) => {\r\n const rect = el.getBoundingClientRect()\r\n const textKeyCount = el.querySelectorAll('[data-text-key]').length\r\n return {\r\n id: el.dataset.section!,\r\n label: el.dataset.sectionLabel ?? null,\r\n bounds: {\r\n x: rect.left + scrollX,\r\n y: rect.top + scrollY,\r\n width: rect.width,\r\n height: rect.height,\r\n },\r\n textKeyCount,\r\n }\r\n })\r\n}\r\n\r\nfunction discoverTextKeys(): string[] {\r\n return Array.from(document.querySelectorAll<HTMLElement>('[data-text-key]')).map(\r\n (el) => el.dataset.textKey!,\r\n )\r\n}\r\n\r\n// ─── Layout Change Observation ───────────────────────────────────\r\n\r\n/**\r\n * Watch for layout changes that affect section bounds and overall page height:\r\n * 1. ALL lazy-loaded images on the page — their load events shift subsequent sections\r\n * 2. ResizeObserver on the document body — detects overall page height changes\r\n * 3. ResizeObserver on section elements — detects any size change (font loading,\r\n * dynamic content expansion, CSS transitions, etc.)\r\n *\r\n * When changes are detected, we schedule a debounced re-discovery so the portal\r\n * overlay layer receives updated section bounds and content height that match\r\n * the live layout.\r\n */\r\nfunction observeSectionLayout() {\r\n // Clean up any previous observer (e.g., after SPA navigation)\r\n if (sectionResizeObserver) {\r\n sectionResizeObserver.disconnect()\r\n }\r\n\r\n // 1. Watch ALL lazy images on the page — not just inside sections.\r\n // Images outside sections (e.g., contact form, unmapped content) can still\r\n // push content down and change overall page height and section positions.\r\n const allImages = document.querySelectorAll<HTMLImageElement>('img')\r\n allImages.forEach(img => {\r\n if (!img.complete) {\r\n img.addEventListener('load', scheduleRediscovery, { once: true })\r\n img.addEventListener('error', scheduleRediscovery, { once: true })\r\n }\r\n })\r\n\r\n // 2. ResizeObserver on the document body + each section element.\r\n // Body observation catches overall page height changes from any source.\r\n if (typeof ResizeObserver !== 'undefined') {\r\n sectionResizeObserver = new ResizeObserver(scheduleRediscovery)\r\n // Observe body for overall height changes\r\n sectionResizeObserver.observe(document.body)\r\n // Observe each section for individual bound changes\r\n document.querySelectorAll<HTMLElement>('[data-section]').forEach(el => {\r\n sectionResizeObserver!.observe(el)\r\n })\r\n }\r\n}\r\n\r\n// ─── Section Overlay ─────────────────────────────────────────────\r\n\r\n/**\r\n * Section highlight is now handled entirely by the portal-side\r\n * SectionOverlayLayer rendered on top of the iframe. The bridge\r\n * only reports section bounds and hover events — no visual\r\n * overlays are rendered inside the iframe itself.\r\n */\r\nfunction showSectionHighlight(_sectionId: string) {\r\n // No-op: portal overlay layer handles section highlights\r\n}\r\n\r\nfunction clearSectionHighlight() {\r\n // No-op: portal overlay layer handles section highlights\r\n}\r\n\r\n// ─── Navigation Monitoring ───────────────────────────────────────\r\n\r\n/**\r\n * Monitor navigation within the iframe and notify the parent portal.\r\n * Instead of blocking all navigation, we allow SPA routing and inform\r\n * the portal when the iframe navigates to a different page. The portal\r\n * then updates its own URL and loads editing context for the new page.\r\n *\r\n * Strategy:\r\n * 1. Poll location.pathname on a short interval — this reliably detects\r\n * SPA navigation regardless of which router or framework is used,\r\n * since frameworks may cache History API references before our script loads.\r\n * 2. Block external/cross-origin link clicks (actual page reloads).\r\n * 3. Block form submissions.\r\n * 4. Monitor popstate for back/forward navigation.\r\n * 5. After navigation, re-discover sections and report them to the portal.\r\n */\r\nfunction monitorNavigation() {\r\n lastKnownPathname = globalThis.location.pathname\r\n\r\n // ── Poll for URL changes — reliable with any SPA router ──────\r\n // Frameworks like VitePress/Vue Router may cache history.pushState before\r\n // our bridge loads, so prototype overrides are unreliable. Polling\r\n // location.pathname directly is simple and always works.\r\n setInterval(() => {\r\n checkForNavigation()\r\n }, 250)\r\n\r\n // ── Catch back/forward navigation ────────────────────────────\r\n globalThis.addEventListener('popstate', () => {\r\n checkForNavigation()\r\n })\r\n\r\n // ── Block external link clicks (cross-origin / full page reloads) ──\r\n const handleLinkClick = (e: MouseEvent) => {\r\n const link = (e.target as HTMLElement).closest?.('a[href]') as HTMLAnchorElement | null\r\n if (!link) return\r\n\r\n const href = link.getAttribute('href')\r\n if (!href || href.startsWith('#') || href === 'javascript:void(0)') return\r\n\r\n // Check if this is an external link (different origin)\r\n try {\r\n const resolved = new URL(href, globalThis.location.href)\r\n if (resolved.origin !== globalThis.location.origin) {\r\n // Block external navigation — can't leave the iframe\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n }\r\n // Same-origin links: let the SPA router handle them.\r\n // The History API override above will detect the navigation.\r\n } catch {\r\n // Malformed URL — block it\r\n e.preventDefault()\r\n }\r\n }\r\n\r\n // Register on window first to beat VitePress's capture-phase handler\r\n globalThis.addEventListener('click', handleLinkClick, true)\r\n document.addEventListener('click', handleLinkClick, true)\r\n\r\n // Intercept form submissions\r\n document.addEventListener('submit', (e: Event) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n }, true)\r\n\r\n // Block actual page reloads (location.href = ..., etc.)\r\n window.addEventListener('beforeunload', (e: BeforeUnloadEvent) => {\r\n e.preventDefault()\r\n })\r\n}\r\n\r\n/**\r\n * Check if the iframe pathname has changed and notify the portal.\r\n * Also re-initializes the editor bridge for the new page.\r\n */\r\nfunction checkForNavigation() {\r\n const currentPathname = globalThis.location.pathname\r\n if (currentPathname === lastKnownPathname) return\r\n\r\n lastKnownPathname = currentPathname\r\n\r\n // Notify parent about the navigation\r\n postToParent('dcs:navigation', { pathname: currentPathname })\r\n\r\n // Clean up current edit state\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n\r\n // Wait for the SPA router to finish rendering the new page,\r\n // then re-discover sections and text keys\r\n setTimeout(() => {\r\n rediscoverAndNotify()\r\n }, 500)\r\n}\r\n\r\n// ─── Mode Management ─────────────────────────────────────────────\r\n\r\n/**\r\n * Set the current interaction mode. Both modes allow normal browsing —\r\n * inline editing works via double-click regardless of mode.\r\n * Switching to explore finishes any active inline edit.\r\n */\r\nfunction setMode(mode: 'edit' | 'explore') {\r\n if (mode === 'explore') {\r\n // When explicitly switching to explore, finish any active inline edit\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n }\r\n}\r\n\r\n// ─── Inline Text Editing ─────────────────────────────────────────\r\n\r\n/**\r\n * Re-discover sections and text keys after an HMR update and\r\n * notify the parent portal so the overlay layer updates.\r\n */\r\nfunction rediscoverAndNotify() {\r\n activeEditElement = null\r\n\r\n const sections = discoverSections()\r\n const textKeys = discoverTextKeys()\r\n const contentHeight = measureContentHeight(sections)\r\n\r\n // Skip re-report if height hasn't meaningfully changed (< 2px drift)\r\n // AND sections/textKeys are the same count — avoids portal re-render churn.\r\n if (Math.abs(contentHeight - lastReportedHeight) <= 2) {\r\n // Height is stable — still report in case sections/textKeys changed\r\n }\r\n lastReportedHeight = contentHeight\r\n\r\n // Always report — sections/textKeys may have changed even if height is stable\r\n postToParent('dcs:ready', { sections, textKeys, contentHeight } satisfies EditorReadyPayload)\r\n\r\n // Re-enable editor mode on the new elements\r\n if (editorActive) {\r\n editorActive = false // allow re-initialization\r\n enableEditorMode()\r\n }\r\n\r\n // Re-observe layout changes on the new DOM\r\n observeSectionLayout()\r\n}\r\n\r\nfunction enableEditorMode() {\r\n if (editorActive) return\r\n editorActive = true\r\n\r\n // Inject editor styles\r\n injectEditorStyles()\r\n\r\n // Set up all text key elements — double-click to start editing\r\n const textElements = document.querySelectorAll<HTMLElement>('[data-text-key]')\r\n textElements.forEach((htmlEl) => {\r\n htmlEl.classList.add('dcs-editable')\r\n\r\n // Double-click to start inline editing (signals parent to switch to edit mode)\r\n htmlEl.addEventListener('dblclick', handleTextKeyDblClick)\r\n // Blur to finish editing\r\n htmlEl.addEventListener('blur', handleTextKeyBlur)\r\n // Keyboard: Enter to finish, Escape to cancel\r\n htmlEl.addEventListener('keydown', handleTextKeyKeydown)\r\n\r\n // Show/hide the floating edit icon on hover (T icon for regular text, List icon for array keys)\r\n htmlEl.addEventListener('mouseenter', () => {\r\n // Don't show icon while actively editing this or another element\r\n if (activeEditElement) return\r\n const key = htmlEl.dataset.textKey!\r\n if (isArrayTextKey(key)) {\r\n showArrayEditIcon(htmlEl)\r\n } else {\r\n showEditIcon(htmlEl)\r\n }\r\n })\r\n htmlEl.addEventListener('mouseleave', (e: MouseEvent) => {\r\n // Don't hide if moving to the edit icon or array edit icon\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && (\r\n related.classList?.contains('dcs-edit-icon') || related.closest?.('.dcs-edit-icon') ||\r\n related.classList?.contains('dcs-array-edit-icon') || related.closest?.('.dcs-array-edit-icon')\r\n )) return\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n })\r\n })\r\n\r\n // Set up section hover reporting (portal overlay handles the visual treatment)\r\n const sections = document.querySelectorAll<HTMLElement>('[data-section]')\r\n sections.forEach((el) => {\r\n const sectionId = el.dataset.section!\r\n\r\n el.addEventListener('mouseenter', () => {\r\n postToParent('dcs:section-hover', { sectionId })\r\n })\r\n el.addEventListener('mouseleave', () => {\r\n postToParent('dcs:section-hover', { sectionId: null })\r\n })\r\n el.addEventListener('click', (e) => {\r\n // Only fire section click if not clicking on a text-key element\r\n const target = e.target as HTMLElement\r\n if (!target.closest('[data-text-key]')) {\r\n postToParent('dcs:section-click', { sectionId })\r\n }\r\n })\r\n })\r\n}\r\n\r\n/**\r\n * Handle double-click on a text key element.\r\n * Notifies the parent portal to switch to edit mode and begins inline editing.\r\n */\r\nfunction handleTextKeyDblClick(e: Event) {\r\n if (!editingEnabled) return\r\n\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n const el = e.currentTarget as HTMLElement\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Always notify the parent to switch to edit mode when starting inline editing.\r\n // In explore mode this triggers the mode switch; in edit mode it's a no-op on the parent side.\r\n postToParent('dcs:text-key-dblclick', { key, sectionId })\r\n\r\n // Start inline editing\r\n startInlineEdit(el, key, sectionId)\r\n}\r\n\r\n/**\r\n * Start inline contenteditable editing on a text key element.\r\n * Called by double-click or when the parent sends dcs:select-text-key.\r\n */\r\nfunction startInlineEdit(el: HTMLElement, key: string, sectionId: string | null) {\r\n // Finish any active edit first\r\n if (activeEditElement && activeEditElement !== el) {\r\n finishEdit(activeEditElement)\r\n }\r\n\r\n // Store original text for change detection on finish\r\n originalTextValues.set(el, el.innerText.trim())\r\n\r\n // Enable contenteditable\r\n el.setAttribute('contenteditable', 'true')\r\n el.classList.add('dcs-editing')\r\n el.focus()\r\n activeEditElement = el\r\n\r\n // Select all text for easy replacement\r\n const selection = getSelection()\r\n const range = document.createRange()\r\n range.selectNodeContents(el)\r\n selection?.removeAllRanges()\r\n selection?.addRange(range)\r\n\r\n postToParent('dcs:text-key-click', { key, sectionId })\r\n}\r\n\r\nfunction handleTextKeyBlur(e: Event) {\r\n const el = e.currentTarget as HTMLElement\r\n finishEdit(el)\r\n}\r\n\r\nfunction handleTextKeyKeydown(e: KeyboardEvent) {\r\n if (e.key === 'Enter' && !e.shiftKey) {\r\n e.preventDefault()\r\n ;(e.currentTarget as HTMLElement).blur()\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n ;(e.currentTarget as HTMLElement).blur()\r\n }\r\n}\r\n\r\nfunction finishEdit(el: HTMLElement) {\r\n if (!el.hasAttribute('contenteditable')) return\r\n\r\n el.removeAttribute('contenteditable')\r\n el.classList.remove('dcs-editing')\r\n\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n const newValue = el.innerText.trim()\r\n const originalValue = originalTextValues.get(el) ?? ''\r\n\r\n if (activeEditElement === el) {\r\n activeEditElement = null\r\n }\r\n\r\n // Only report to the portal when text was actually modified\r\n if (newValue !== originalValue) {\r\n postToParent('dcs:text-key-changed', { key, value: newValue, sectionId })\r\n }\r\n\r\n originalTextValues.delete(el)\r\n}\r\n\r\nfunction focusTextKey(key: string) {\r\n const el = document.querySelector<HTMLElement>(`[data-text-key=\"${key}\"]`)\r\n if (!el) return\r\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\r\n\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Start inline editing directly\r\n startInlineEdit(el, key, sectionId)\r\n}\r\n\r\nfunction updateTextInPlace(key: string, value: string) {\r\n const el = document.querySelector<HTMLElement>(`[data-text-key=\"${key}\"]`)\r\n if (!el) return\r\n // Don't update if we're currently editing this element\r\n if (el === activeEditElement) return\r\n el.innerText = value\r\n}\r\n\r\n// ─── Edit Icon (Floating Text Cursor Button) ────────────────────\r\n\r\n/**\r\n * Create the floating edit icon element. This small button appears\r\n * in the top-left corner of a hovered [data-text-key] element,\r\n * providing a click target to start inline editing without triggering\r\n * navigation (important for text inside buttons/links).\r\n */\r\nfunction createEditIcon(): HTMLElement {\r\n const icon = document.createElement('button')\r\n icon.className = 'dcs-edit-icon'\r\n icon.setAttribute('type', 'button')\r\n icon.setAttribute('title', 'Edit text')\r\n icon.setAttribute('aria-label', 'Edit text inline')\r\n // SVG: Type icon (text cursor) — matches lucide \"Type\" icon\r\n icon.innerHTML = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"4 7 4 4 20 4 20 7\"/><line x1=\"9\" y1=\"20\" x2=\"15\" y2=\"20\"/><line x1=\"12\" y1=\"4\" x2=\"12\" y2=\"20\"/></svg>`\r\n\r\n icon.addEventListener('click', (e) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n if (!editIconTarget) return\r\n const el = editIconTarget\r\n const key = el.dataset.textKey!\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Signal parent to switch to edit mode\r\n postToParent('dcs:text-key-dblclick', { key, sectionId })\r\n // Start inline editing\r\n startInlineEdit(el, key, sectionId)\r\n // Hide the icon while editing\r\n removeEditIcon()\r\n }, true)\r\n\r\n // Hide icon when mouse leaves it (and isn't going to the target element)\r\n icon.addEventListener('mouseleave', (e: MouseEvent) => {\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && editIconTarget && (related === editIconTarget || editIconTarget.contains(related))) return\r\n removeEditIcon()\r\n })\r\n\r\n return icon\r\n}\r\n\r\n/**\r\n * Show the edit icon positioned at the top-left corner of the given element.\r\n */\r\nfunction showEditIcon(el: HTMLElement) {\r\n if (!editingEnabled) return\r\n\r\n if (!editIconElement) {\r\n editIconElement = createEditIcon()\r\n document.body.appendChild(editIconElement)\r\n }\r\n\r\n editIconTarget = el\r\n\r\n // Position relative to the element's bounding box\r\n const rect = el.getBoundingClientRect()\r\n editIconElement.style.top = `${rect.top + scrollY - 4}px`\r\n editIconElement.style.left = `${rect.left + scrollX - 4}px`\r\n editIconElement.style.display = 'flex'\r\n}\r\n\r\n/**\r\n * Hide and detach the edit icon.\r\n */\r\nfunction removeEditIcon() {\r\n if (editIconElement) {\r\n editIconElement.style.display = 'none'\r\n }\r\n editIconTarget = null\r\n}\r\n\r\n// ─── Array Key Detection ─────────────────────────────────────────\r\n\r\n/**\r\n * Check if a text key is part of an array pattern (e.g., \"conditions.0.title\").\r\n * Array text keys contain an indexed segment — either:\r\n * - Pure numeric: `baseKey.N.field` (e.g., `conditions.0.title`)\r\n * - Hyphenated index: `baseKey.prefix-N.field` (e.g., `steps.step-0.title`)\r\n */\r\nfunction isArrayTextKey(key: string): boolean {\r\n // Match pure numeric segment (.0.) or hyphenated index (.prefix-0.)\r\n return /\\.\\d+\\./.test(key) || /\\.\\w+-\\d+\\./.test(key)\r\n}\r\n\r\n/**\r\n * Extract the base array key from an array text key.\r\n * - `conditions.0.title` → `conditions`\r\n * - `steps.step-0.title` → `steps.step`\r\n */\r\nfunction getArrayBaseKey(key: string): string | null {\r\n // Try hyphenated pattern first (more specific)\r\n const hyphenMatch = key.match(/^(.+?)-\\d+\\./)\r\n if (hyphenMatch) return hyphenMatch[1]\r\n // Fall back to pure numeric pattern\r\n const numMatch = key.match(/^(.+?)\\.\\d+\\./)\r\n return numMatch ? numMatch[1] : null\r\n}\r\n\r\n// ─── Array Edit Icon (Floating List Button) ──────────────────────\r\n\r\n/**\r\n * Create the floating array edit icon element. This small button appears\r\n * in the top-left corner of a hovered [data-text-key] element whose key\r\n * matches an array pattern. Clicking it sends dcs:array-key-click to the\r\n * portal to open the array editor sheet.\r\n */\r\nfunction createArrayEditIcon(): HTMLElement {\r\n const icon = document.createElement('button')\r\n icon.className = 'dcs-array-edit-icon'\r\n icon.setAttribute('type', 'button')\r\n icon.setAttribute('title', 'Edit list items')\r\n icon.setAttribute('aria-label', 'Edit list items in panel')\r\n // SVG: List icon — matches lucide \"List\" icon\r\n icon.innerHTML = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"/><line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\"/></svg>`\r\n\r\n icon.addEventListener('click', (e) => {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n e.stopImmediatePropagation()\r\n\r\n if (!arrayEditIconTarget) return\r\n const el = arrayEditIconTarget\r\n const key = el.dataset.textKey!\r\n const arrayKey = getArrayBaseKey(key)\r\n if (!arrayKey) return\r\n\r\n const sectionParent = el.closest<HTMLElement>('[data-section]')\r\n const sectionId = sectionParent?.dataset.section ?? null\r\n\r\n // Signal parent to open array editor sheet\r\n postToParent('dcs:array-key-click', { arrayKey, sectionId })\r\n // Hide the icon\r\n removeArrayEditIcon()\r\n }, true)\r\n\r\n // Hide icon when mouse leaves it (and isn't going to the target element)\r\n icon.addEventListener('mouseleave', (e: MouseEvent) => {\r\n const related = e.relatedTarget as HTMLElement | null\r\n if (related && arrayEditIconTarget && (related === arrayEditIconTarget || arrayEditIconTarget.contains(related))) return\r\n removeArrayEditIcon()\r\n })\r\n\r\n return icon\r\n}\r\n\r\n/**\r\n * Show the array edit icon positioned at the top-left corner of the given element.\r\n */\r\nfunction showArrayEditIcon(el: HTMLElement) {\r\n if (!editingEnabled) return\r\n\r\n if (!arrayEditIconElement) {\r\n arrayEditIconElement = createArrayEditIcon()\r\n document.body.appendChild(arrayEditIconElement)\r\n }\r\n\r\n arrayEditIconTarget = el\r\n\r\n // Position relative to the element's bounding box\r\n const rect = el.getBoundingClientRect()\r\n arrayEditIconElement.style.top = `${rect.top + scrollY - 4}px`\r\n arrayEditIconElement.style.left = `${rect.left + scrollX - 4}px`\r\n arrayEditIconElement.style.display = 'flex'\r\n}\r\n\r\n/**\r\n * Hide and detach the array edit icon.\r\n */\r\nfunction removeArrayEditIcon() {\r\n if (arrayEditIconElement) {\r\n arrayEditIconElement.style.display = 'none'\r\n }\r\n arrayEditIconTarget = null\r\n}\r\n\r\nfunction injectEditorStyles() {\r\n if (document.getElementById('dcs-editor-styles')) return\r\n\r\n const style = document.createElement('style')\r\n style.id = 'dcs-editor-styles'\r\n style.textContent = `\r\n [data-text-key].dcs-editable {\r\n cursor: text !important;\r\n border-radius: 4px;\r\n position: relative;\r\n }\r\n\r\n [data-text-key].dcs-editable:hover {\r\n outline: 2px dashed hsl(221.2 83.2% 53.3% / 0.4) !important;\r\n outline-offset: 4px;\r\n background-color: hsl(221.2 83.2% 53.3% / 0.03);\r\n }\r\n\r\n [data-text-key].dcs-editing {\r\n outline: 2px solid hsl(221.2 83.2% 53.3%) !important;\r\n outline-offset: 4px;\r\n background-color: hsl(221.2 83.2% 53.3% / 0.06);\r\n min-height: 1em;\r\n }\r\n\r\n [data-text-key].dcs-editing:focus {\r\n outline: 2px solid hsl(221.2 83.2% 53.3%) !important;\r\n outline-offset: 4px;\r\n }\r\n\r\n .dcs-section-highlight {\r\n pointer-events: none;\r\n }\r\n\r\n /* Floating edit icon button */\r\n .dcs-edit-icon {\r\n position: absolute;\r\n z-index: 99999;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 0;\r\n margin: 0;\r\n border: 1.5px solid hsl(221.2 83.2% 53.3%);\r\n border-radius: 4px;\r\n background: hsl(221.2 83.2% 53.3% / 0.1);\r\n backdrop-filter: blur(4px);\r\n color: hsl(221.2 83.2% 53.3%);\r\n cursor: pointer;\r\n pointer-events: auto;\r\n transition: background 0.15s, transform 0.1s;\r\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\r\n }\r\n\r\n .dcs-edit-icon:hover {\r\n background: hsl(221.2 83.2% 53.3% / 0.25);\r\n transform: scale(1.1);\r\n }\r\n\r\n .dcs-edit-icon svg {\r\n display: block;\r\n }\r\n\r\n /* Floating array edit icon button */\r\n .dcs-array-edit-icon {\r\n position: absolute;\r\n z-index: 99999;\r\n display: none;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n padding: 0;\r\n margin: 0;\r\n border: 1.5px solid hsl(271 91% 65%);\r\n border-radius: 4px;\r\n background: hsl(271 91% 65% / 0.1);\r\n backdrop-filter: blur(4px);\r\n color: hsl(271 91% 65%);\r\n cursor: pointer;\r\n pointer-events: auto;\r\n transition: background 0.15s, transform 0.1s;\r\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\r\n }\r\n\r\n .dcs-array-edit-icon:hover {\r\n background: hsl(271 91% 65% / 0.25);\r\n transform: scale(1.1);\r\n }\r\n\r\n .dcs-array-edit-icon svg {\r\n display: block;\r\n }\r\n `\r\n document.head.appendChild(style)\r\n}\r\n\r\n// ─── Inbound Message Handler ─────────────────────────────────────\r\n\r\nfunction handleMessage(event: MessageEvent) {\r\n // Validate message structure (origin is not checked because\r\n // the parent portal origin varies between dev and production)\r\n const msg = event.data\r\n if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return\r\n if (!msg.type.startsWith('dcs:')) return\r\n\r\n const { type, data } = msg as { type: InboundMessageType; data: unknown }\r\n\r\n switch (type) {\r\n case 'dcs:init-editor':\r\n enableEditorMode()\r\n break\r\n\r\n case 'dcs:highlight-section':\r\n if (data && typeof data === 'object' && 'sectionId' in data) {\r\n showSectionHighlight((data as { sectionId: string }).sectionId)\r\n }\r\n break\r\n\r\n case 'dcs:clear-highlight':\r\n clearSectionHighlight()\r\n break\r\n\r\n case 'dcs:select-text-key':\r\n if (data && typeof data === 'object' && 'key' in data) {\r\n focusTextKey((data as { key: string }).key)\r\n }\r\n break\r\n\r\n case 'dcs:update-text':\r\n if (data && typeof data === 'object' && 'key' in data && 'value' in data) {\r\n const d = data as { key: string; value: string }\r\n updateTextInPlace(d.key, d.value)\r\n }\r\n break\r\n\r\n case 'dcs:set-mode':\r\n if (data && typeof data === 'object' && 'mode' in data) {\r\n setMode((data as { mode: 'edit' | 'explore' }).mode)\r\n }\r\n break\r\n\r\n case 'dcs:set-editing-enabled':\r\n if (data && typeof data === 'object' && 'enabled' in data) {\r\n editingEnabled = (data as { enabled: boolean }).enabled\r\n if (editingEnabled) {\r\n // Re-add dcs-editable class to text key elements\r\n document.querySelectorAll<HTMLElement>('[data-text-key]').forEach(el => {\r\n el.classList.add('dcs-editable')\r\n })\r\n } else {\r\n // Clean up: finish any active edit, remove edit icon, remove hover styles\r\n if (activeEditElement) {\r\n finishEdit(activeEditElement)\r\n }\r\n removeEditIcon()\r\n removeArrayEditIcon()\r\n // Remove dcs-editable class to disable hover outlines\r\n document.querySelectorAll<HTMLElement>('.dcs-editable').forEach(el => {\r\n el.classList.remove('dcs-editable')\r\n })\r\n }\r\n }\r\n break\r\n }\r\n}\r\n\r\n// ─── Bootstrap ───────────────────────────────────────────────────\r\n\r\n/**\r\n * Inject a stylesheet that neutralizes viewport-relative min-heights.\r\n * In the visual editor iframe, the portal controls height from body.scrollHeight.\r\n * CSS rules like `min-height: 100vh` create a feedback loop:\r\n * iframe height → vh grows → body.scrollHeight grows → iframe height grows → …\r\n * Neutralizing these rules lets the content determine its natural height.\r\n */\r\nfunction injectEditorHeightFix() {\r\n if (document.getElementById('dcs-editor-height-fix')) return\r\n const style = document.createElement('style')\r\n style.id = 'dcs-editor-height-fix'\r\n style.textContent = `\r\n /* DCS Editor Bridge: Neutralize viewport-relative min-heights */\r\n .min-h-screen, .min-h-dvh, .min-h-svh,\r\n .min-h-\\\\[100vh\\\\], .min-h-\\\\[100dvh\\\\], .min-h-\\\\[100svh\\\\] {\r\n min-height: 0 !important;\r\n }\r\n `\r\n document.head.appendChild(style)\r\n}\r\n\r\n/** Last reported content height — used to avoid re-sending when stable */\r\nlet lastReportedHeight = 0\r\n\r\n/**\r\n * Measure the true content height, avoiding inflation from viewport-relative\r\n * CSS rules. Uses section bounds as the primary signal (immune to vh feedback),\r\n * falling back to body.scrollHeight (which is reliable once the editor height\r\n * fix stylesheet has been injected).\r\n */\r\nfunction measureContentHeight(sections: SectionInfo[]): number {\r\n // Primary: compute from section bounds — these reflect actual element positions\r\n // after the height-fix stylesheet has neutralized viewport-relative min-heights.\r\n let maxBottom = 0\r\n for (const s of sections) {\r\n const bottom = s.bounds.y + s.bounds.height\r\n if (bottom > maxBottom) maxBottom = bottom\r\n }\r\n const sectionHeight = maxBottom > 0 ? Math.ceil(maxBottom) : 0\r\n\r\n // Fallback: body.scrollHeight (reliable now that min-height: 100vh is neutralized)\r\n const bodyHeight = document.body.scrollHeight\r\n\r\n // Use whichever is larger — sections might miss elements without data-section,\r\n // but bodyHeight should be accurate with the injected CSS fix.\r\n return Math.max(sectionHeight, bodyHeight)\r\n}\r\n\r\nexport function initEditorBridge() {\r\n if (!isInIframe()) return // Not in iframe\r\n\r\n // Inject height fix CSS before any measurement\r\n injectEditorHeightFix()\r\n\r\n // Prevent duplicate listener registration on HMR re-execution\r\n if (!bridgeInitialized) {\r\n bridgeInitialized = true\r\n\r\n // Monitor navigation within the iframe — notify the portal of page changes\r\n monitorNavigation()\r\n\r\n // Forward right-click events to the portal so the context menu\r\n // works when right-clicking inside the iframe (cross-origin boundary\r\n // prevents the parent from receiving native contextmenu events).\r\n document.addEventListener('contextmenu', (e: MouseEvent) => {\r\n e.preventDefault()\r\n const sectionEl = (e.target as HTMLElement).closest?.('[data-section]') as HTMLElement | null\r\n postToParent('dcs:contextmenu', {\r\n x: e.clientX,\r\n y: e.clientY,\r\n sectionId: sectionEl?.dataset.section ?? null,\r\n sectionLabel: sectionEl?.dataset.sectionLabel ?? null,\r\n })\r\n }, true)\r\n\r\n // Forward click events to the portal so it can dismiss context menus\r\n // when the user clicks inside the iframe.\r\n document.addEventListener('click', () => {\r\n postToParent('dcs:click', {})\r\n }, true)\r\n\r\n // Listen for portal commands. Origin is not validated because\r\n // the portal origin varies between environments and all inbound\r\n // messages are type-checked before processing.\r\n // Origin cannot be pre-validated because the portal URL varies by environment.\r\n // All inbound messages are type-checked via the dcs: prefix guard.\r\n globalThis.self.addEventListener('message', handleMessage) // NOSONAR\r\n\r\n // Watch for VitePress / Vite HMR page updates. When the framework\r\n // replaces DOM content, our event listeners and AI buttons are lost.\r\n // Re-discover sections after each update so the portal overlay stays in sync.\r\n if (import.meta.hot) {\r\n import.meta.hot.on('vite:afterUpdate', () => {\r\n // Small delay for the framework to finish DOM updates\r\n setTimeout(rediscoverAndNotify, 300)\r\n })\r\n }\r\n }\r\n\r\n // Report available sections, text keys, and content height\r\n const sections = discoverSections()\r\n const textKeys = discoverTextKeys()\r\n const contentHeight = measureContentHeight(sections)\r\n lastReportedHeight = contentHeight\r\n\r\n postToParent('dcs:ready', {\r\n sections,\r\n textKeys,\r\n contentHeight,\r\n } satisfies EditorReadyPayload)\r\n\r\n // Observe layout changes (lazy images, dynamic content) and re-report bounds\r\n observeSectionLayout()\r\n}\r\n\r\n// Auto-initialize when the script is loaded (e.g., injected by Vite plugin)\r\n// Delay slightly to ensure the page is fully rendered\r\nif (typeof globalThis.self !== 'undefined' && isInIframe()) {\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', () => {\r\n // Small delay for Vue/framework hydration\r\n setTimeout(initEditorBridge, 200)\r\n })\r\n } else {\r\n setTimeout(initEditorBridge, 200)\r\n }\r\n}\r\n"]}
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
+ import { Component, Plugin as Plugin$1 } from 'vue';
2
3
 
3
4
  /**
4
5
  * DCS Content Plugin for Vite
@@ -141,4 +142,52 @@ interface DcsEditorPluginOptions {
141
142
  */
142
143
  declare function dcsEditorPlugin(options?: DcsEditorPluginOptions): Plugin;
143
144
 
144
- export { type DcsContentPluginOptions, type DcsEditorPluginOptions, type DcsSeoPluginOptions, dcsContentPlugin, dcsEditorPlugin, dcsSeoPlugin };
145
+ /**
146
+ * DCS Preview Plugin for Vue
147
+ *
148
+ * Registers a supplied ribbon component as a global `DcsPreviewRibbon`
149
+ * component. The ribbon handles its own visibility — it only renders on
150
+ * `preview.duffcloudservices.com` and hides everywhere else (localhost,
151
+ * production domains, and inside the visual page editor iframe).
152
+ *
153
+ * Why does the caller pass the component in? Because this file is compiled
154
+ * by tsup (esbuild) which has no `.vue` SFC loader. Keeping the raw `.vue`
155
+ * import out of the compiled plugins bundle avoids the build error while
156
+ * still letting consumer code (which *does* run through Vite) resolve the
157
+ * SFC at dev/build time.
158
+ *
159
+ * @example VitePress theme/index.ts
160
+ * ```typescript
161
+ * import { dcsPreviewPlugin } from '@duffcloudservices/cms/plugins'
162
+ * import PreviewRibbon from '@duffcloudservices/cms/components'
163
+ *
164
+ * export default {
165
+ * Layout,
166
+ * enhanceApp({ app }) {
167
+ * app.use(dcsPreviewPlugin(PreviewRibbon))
168
+ * }
169
+ * }
170
+ * ```
171
+ */
172
+
173
+ interface DcsPreviewPluginOptions {
174
+ /**
175
+ * Override the version string displayed on the ribbon.
176
+ * If omitted, the ribbon auto-detects from VITE_SITE_VERSION or the API.
177
+ */
178
+ version?: string | null;
179
+ }
180
+ /**
181
+ * Creates and returns the DCS Preview plugin.
182
+ *
183
+ * When installed, it registers the supplied ribbon component as a global
184
+ * `DcsPreviewRibbon` component. Add `<DcsPreviewRibbon />` to your root
185
+ * Layout, or use the `dcsEditorPlugin` Vite plugin which injects it via
186
+ * `transformIndexHtml`.
187
+ *
188
+ * @param ribbonComponent - The PreviewRibbon SFC (imported by the consumer)
189
+ * @param options - Optional configuration
190
+ */
191
+ declare function dcsPreviewPlugin(ribbonComponent: Component, options?: DcsPreviewPluginOptions): Plugin$1;
192
+
193
+ export { type DcsContentPluginOptions, type DcsEditorPluginOptions, type DcsPreviewPluginOptions, type DcsSeoPluginOptions, dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin };
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import yaml from 'js-yaml';
4
+ import { defineComponent, h } from 'vue';
4
5
 
5
6
  // src/plugins/dcsContentPlugin.ts
6
7
  function dcsContentPlugin(options = {}) {
@@ -168,10 +169,11 @@ function dcsSeoPlugin(options = {}) {
168
169
  function dcsEditorPlugin(options = {}) {
169
170
  const { debug = false } = options;
170
171
  const VIRTUAL_PATH = "/__dcs-editor-bridge.js";
172
+ const RIBBON_VIRTUAL_PATH = "/__dcs-preview-ribbon.js";
171
173
  return {
172
174
  name: "dcs-editor-bridge",
173
175
  resolveId(id) {
174
- if (id === VIRTUAL_PATH) {
176
+ if (id === VIRTUAL_PATH || id === RIBBON_VIRTUAL_PATH) {
175
177
  return id;
176
178
  }
177
179
  },
@@ -187,20 +189,60 @@ if (window.parent !== window) {
187
189
  setTimeout(initEditorBridge, 200)
188
190
  }
189
191
  }
192
+ `;
193
+ }
194
+ if (id === RIBBON_VIRTUAL_PATH) {
195
+ return `
196
+ import { createApp } from 'vue'
197
+ import PreviewRibbon from '@duffcloudservices/cms/components'
198
+
199
+ function mountRibbon() {
200
+ // Create a detached mount point
201
+ const el = document.createElement('div')
202
+ el.id = 'dcs-preview-ribbon-root'
203
+ document.body.appendChild(el)
204
+
205
+ const app = createApp(PreviewRibbon)
206
+ app.mount(el)
207
+ }
208
+
209
+ if (document.readyState === 'loading') {
210
+ document.addEventListener('DOMContentLoaded', mountRibbon)
211
+ } else {
212
+ mountRibbon()
213
+ }
190
214
  `;
191
215
  }
192
216
  },
193
217
  transformIndexHtml(html) {
194
218
  if (debug) {
195
- console.log("[dcs-editor] Injecting editor bridge script tag");
219
+ console.log("[dcs-editor] Injecting editor bridge + preview ribbon script tags");
196
220
  }
197
- const scriptTag = `<script type="module" src="${VIRTUAL_PATH}"></script>`;
198
- return html.replace("</body>", `${scriptTag}
221
+ const editorTag = `<script type="module" src="${VIRTUAL_PATH}"></script>`;
222
+ const ribbonTag = `<script type="module" src="${RIBBON_VIRTUAL_PATH}"></script>`;
223
+ return html.replace("</body>", `${editorTag}
224
+ ${ribbonTag}
199
225
  </body>`);
200
226
  }
201
227
  };
202
228
  }
229
+ function dcsPreviewPlugin(ribbonComponent, options = {}) {
230
+ const Wrapper = defineComponent({
231
+ name: "DcsPreviewRibbon",
232
+ setup() {
233
+ return () => h(ribbonComponent, { version: options.version ?? null });
234
+ }
235
+ });
236
+ return {
237
+ install(app) {
238
+ app.component("DcsPreviewRibbon", Wrapper);
239
+ if (options.version !== void 0) {
240
+ app.provide("__dcs_preview_version__", options.version);
241
+ }
242
+ }
243
+ };
244
+ }
203
245
 
204
- export { dcsContentPlugin, dcsEditorPlugin, dcsSeoPlugin };
246
+ export { dcsContentPlugin, dcsEditorPlugin, dcsPreviewPlugin, dcsSeoPlugin };
205
247
  //# sourceMappingURL=index.js.map
206
248
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/dcsContentPlugin.ts","../../src/plugins/dcsSeoPlugin.ts","../../src/plugins/dcsEditorPlugin.ts"],"names":["path","fs","yaml"],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * DCS Content Plugin for Vite\r\n *\r\n * Reads `.dcs/content.yaml` at build time and injects content\r\n * as `__DCS_CONTENT__` global variable for use by useTextContent.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsContentPlugin({ debug: true })\r\n * ]\r\n * })\r\n * ```\r\n *\r\n * For VitePress:\r\n * ```typescript\r\n * // .vitepress/config.ts\r\n * import { defineConfig } from 'vitepress'\r\n * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * vite: {\r\n * plugins: [\r\n * dcsContentPlugin()\r\n * ]\r\n * }\r\n * })\r\n * ```\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type { Plugin, ResolvedConfig } from 'vite'\r\nimport type { DcsContentFile } from '../types/content'\r\n\r\nexport interface DcsContentPluginOptions {\r\n /** Path to content.yaml relative to project root (default: '.dcs/content.yaml') */\r\n contentPath?: string\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects .dcs/content.yaml at build time.\r\n *\r\n * @param options - Plugin configuration\r\n * @returns Vite plugin\r\n */\r\nexport function dcsContentPlugin(options: DcsContentPluginOptions = {}): Plugin {\r\n const { contentPath = '.dcs/content.yaml', debug = false } = options\r\n\r\n let resolvedConfig: ResolvedConfig\r\n\r\n return {\r\n name: 'dcs-content',\r\n\r\n configResolved(config) {\r\n resolvedConfig = config\r\n },\r\n\r\n config(config) {\r\n const projectRoot = config.root || process.cwd()\r\n\r\n // Try to find content.yaml in multiple locations\r\n // VitePress projects have the docs folder as root, so check parent too\r\n const possiblePaths = [\r\n path.resolve(projectRoot, contentPath),\r\n path.resolve(projectRoot, '..', contentPath),\r\n path.resolve(process.cwd(), contentPath),\r\n ]\r\n\r\n let foundPath: string | undefined\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n foundPath = testPath\r\n break\r\n }\r\n }\r\n\r\n if (!foundPath) {\r\n if (debug) {\r\n console.log('[dcs-content] No content.yaml found at:')\r\n possiblePaths.forEach((p) => console.log(` - ${p}`))\r\n console.log('[dcs-content] Using defaults only')\r\n }\r\n return {\r\n define: {\r\n __DCS_CONTENT__: 'undefined',\r\n },\r\n }\r\n }\r\n\r\n try {\r\n const fileContent = fs.readFileSync(foundPath, 'utf8')\r\n const content = yaml.load(fileContent) as DcsContentFile\r\n\r\n if (debug) {\r\n console.log(`[dcs-content] Loaded ${foundPath}`)\r\n console.log(`[dcs-content] Version: ${content.version}`)\r\n console.log(`[dcs-content] Pages: ${Object.keys(content.pages ?? {}).join(', ') || '(none)'}`)\r\n console.log(`[dcs-content] Global keys: ${Object.keys(content.global ?? {}).length}`)\r\n }\r\n\r\n return {\r\n define: {\r\n __DCS_CONTENT__: JSON.stringify(content),\r\n },\r\n }\r\n } catch (error) {\r\n console.warn('[dcs-content] Failed to parse content.yaml:', error)\r\n return {\r\n define: {\r\n __DCS_CONTENT__: 'undefined',\r\n },\r\n }\r\n }\r\n },\r\n\r\n // Watch for changes in development\r\n configureServer(server) {\r\n const projectRoot = resolvedConfig?.root || process.cwd()\r\n\r\n const watchPaths = [\r\n path.resolve(projectRoot, contentPath),\r\n path.resolve(projectRoot, '..', contentPath),\r\n ]\r\n\r\n watchPaths.forEach((watchPath) => {\r\n if (fs.existsSync(watchPath)) {\r\n server.watcher.add(watchPath)\r\n\r\n server.watcher.on('change', (changedPath) => {\r\n if (changedPath === watchPath) {\r\n if (debug) {\r\n console.log('[dcs-content] content.yaml changed, triggering reload')\r\n }\r\n server.restart()\r\n }\r\n })\r\n }\r\n })\r\n },\r\n }\r\n}\r\n","/**\r\n * DCS SEO Plugin for Vite\r\n *\r\n * Reads `.dcs/seo.yaml` at build time and injects content\r\n * as `__DCS_SEO__` global variable for use by useSEO.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsSeoPlugin({ debug: true })\r\n * ]\r\n * })\r\n * ```\r\n *\r\n * For VitePress:\r\n * ```typescript\r\n * // .vitepress/config.ts\r\n * import { defineConfig } from 'vitepress'\r\n * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * vite: {\r\n * plugins: [\r\n * dcsSeoPlugin()\r\n * ]\r\n * }\r\n * })\r\n * ```\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type { Plugin, ResolvedConfig } from 'vite'\r\nimport type { SeoConfiguration } from '../types/seo'\r\n\r\nexport interface DcsSeoPluginOptions {\r\n /** Path to seo.yaml relative to project root (default: '.dcs/seo.yaml') */\r\n seoPath?: string\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects .dcs/seo.yaml at build time.\r\n *\r\n * @param options - Plugin configuration\r\n * @returns Vite plugin\r\n */\r\nexport function dcsSeoPlugin(options: DcsSeoPluginOptions = {}): Plugin {\r\n const { seoPath = '.dcs/seo.yaml', debug = false } = options\r\n\r\n let resolvedConfig: ResolvedConfig\r\n\r\n return {\r\n name: 'dcs-seo',\r\n\r\n configResolved(config) {\r\n resolvedConfig = config\r\n },\r\n\r\n config(config) {\r\n const projectRoot = config.root || process.cwd()\r\n\r\n // Try to find seo.yaml in multiple locations\r\n // VitePress projects have the docs folder as root, so check parent too\r\n const possiblePaths = [\r\n path.resolve(projectRoot, seoPath),\r\n path.resolve(projectRoot, '..', seoPath),\r\n path.resolve(process.cwd(), seoPath),\r\n ]\r\n\r\n let foundPath: string | undefined\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n foundPath = testPath\r\n break\r\n }\r\n }\r\n\r\n if (!foundPath) {\r\n if (debug) {\r\n console.log('[dcs-seo] No seo.yaml found at:')\r\n possiblePaths.forEach((p) => console.log(` - ${p}`))\r\n console.log('[dcs-seo] Using defaults only')\r\n }\r\n return {\r\n define: {\r\n __DCS_SEO__: 'undefined',\r\n },\r\n }\r\n }\r\n\r\n try {\r\n const fileContent = fs.readFileSync(foundPath, 'utf8')\r\n const seoConfig = yaml.load(fileContent) as SeoConfiguration\r\n\r\n if (debug) {\r\n console.log(`[dcs-seo] Loaded ${foundPath}`)\r\n console.log(`[dcs-seo] Version: ${seoConfig.version}`)\r\n console.log(`[dcs-seo] Site Name: ${seoConfig.global?.siteName || '(not set)'}`)\r\n console.log(`[dcs-seo] Pages: ${Object.keys(seoConfig.pages ?? {}).join(', ') || '(none)'}`)\r\n }\r\n\r\n return {\r\n define: {\r\n __DCS_SEO__: JSON.stringify(seoConfig),\r\n },\r\n }\r\n } catch (error) {\r\n console.warn('[dcs-seo] Failed to parse seo.yaml:', error)\r\n return {\r\n define: {\r\n __DCS_SEO__: 'undefined',\r\n },\r\n }\r\n }\r\n },\r\n\r\n // Watch for changes in development\r\n configureServer(server) {\r\n const projectRoot = resolvedConfig?.root || process.cwd()\r\n\r\n const watchPaths = [\r\n path.resolve(projectRoot, seoPath),\r\n path.resolve(projectRoot, '..', seoPath),\r\n ]\r\n\r\n watchPaths.forEach((watchPath) => {\r\n if (fs.existsSync(watchPath)) {\r\n server.watcher.add(watchPath)\r\n\r\n server.watcher.on('change', (changedPath) => {\r\n if (changedPath === watchPath) {\r\n if (debug) {\r\n console.log('[dcs-seo] seo.yaml changed, triggering reload')\r\n }\r\n server.restart()\r\n }\r\n })\r\n }\r\n })\r\n },\r\n }\r\n}\r\n","/**\r\n * DCS Editor Plugin for Vite\r\n *\r\n * Injects the editor bridge script into customer sites when running\r\n * in portal preview mode (inside the visual editor iframe).\r\n *\r\n * The bridge enables:\r\n * - Inline text editing via contenteditable\r\n * - Section hover highlights and AI ✨ buttons\r\n * - postMessage communication with the portal\r\n *\r\n * This plugin should only be active in development/preview mode.\r\n * It's safe to include in production builds — it does nothing unless\r\n * the page detects it's inside an iframe.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsEditorPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsEditorPlugin()\r\n * ]\r\n * })\r\n * ```\r\n */\r\n\r\nimport type { Plugin } from 'vite'\r\n\r\nexport interface DcsEditorPluginOptions {\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects the editor bridge script for portal preview integration.\r\n *\r\n * The bridge auto-initializes only when the page detects it's running inside an iframe,\r\n * so it's safe to include in all builds — it's a no-op in standalone browsing.\r\n *\r\n * Uses a virtual module (`/__dcs-editor-bridge.js`) served through Vite's dev server\r\n * so that bare module specifiers (like `@duffcloudservices/cms/editor`) are properly\r\n * resolved through Vite's module graph rather than hitting the browser's native ESM\r\n * resolver, which can't handle bare specifiers.\r\n */\r\nexport function dcsEditorPlugin(options: DcsEditorPluginOptions = {}): Plugin {\r\n const { debug = false } = options\r\n const VIRTUAL_PATH = '/__dcs-editor-bridge.js'\r\n\r\n return {\r\n name: 'dcs-editor-bridge',\r\n\r\n resolveId(id) {\r\n if (id === VIRTUAL_PATH) {\r\n return id\r\n }\r\n },\r\n\r\n load(id) {\r\n if (id === VIRTUAL_PATH) {\r\n return `\r\nimport { initEditorBridge } from '@duffcloudservices/cms/editor'\r\n// The bridge only activates inside an iframe (portal preview)\r\nif (window.parent !== window) {\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', () => setTimeout(initEditorBridge, 200))\r\n } else {\r\n setTimeout(initEditorBridge, 200)\r\n }\r\n}\r\n`\r\n }\r\n },\r\n\r\n transformIndexHtml(html) {\r\n if (debug) {\r\n console.log('[dcs-editor] Injecting editor bridge script tag')\r\n }\r\n\r\n // Inject a <script> tag referencing our virtual module.\r\n // Vite's dev server will resolve the bare imports in the virtual module code.\r\n const scriptTag = `<script type=\"module\" src=\"${VIRTUAL_PATH}\"></script>`\r\n\r\n // Insert before closing </body> tag\r\n return html.replace('</body>', `${scriptTag}\\n</body>`)\r\n },\r\n }\r\n}\r\n"]}
1
+ {"version":3,"sources":["../../src/plugins/dcsContentPlugin.ts","../../src/plugins/dcsSeoPlugin.ts","../../src/plugins/dcsEditorPlugin.ts","../../src/plugins/dcsPreviewPlugin.ts"],"names":["path","fs","yaml"],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * DCS Content Plugin for Vite\r\n *\r\n * Reads `.dcs/content.yaml` at build time and injects content\r\n * as `__DCS_CONTENT__` global variable for use by useTextContent.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsContentPlugin({ debug: true })\r\n * ]\r\n * })\r\n * ```\r\n *\r\n * For VitePress:\r\n * ```typescript\r\n * // .vitepress/config.ts\r\n * import { defineConfig } from 'vitepress'\r\n * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * vite: {\r\n * plugins: [\r\n * dcsContentPlugin()\r\n * ]\r\n * }\r\n * })\r\n * ```\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type { Plugin, ResolvedConfig } from 'vite'\r\nimport type { DcsContentFile } from '../types/content'\r\n\r\nexport interface DcsContentPluginOptions {\r\n /** Path to content.yaml relative to project root (default: '.dcs/content.yaml') */\r\n contentPath?: string\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects .dcs/content.yaml at build time.\r\n *\r\n * @param options - Plugin configuration\r\n * @returns Vite plugin\r\n */\r\nexport function dcsContentPlugin(options: DcsContentPluginOptions = {}): Plugin {\r\n const { contentPath = '.dcs/content.yaml', debug = false } = options\r\n\r\n let resolvedConfig: ResolvedConfig\r\n\r\n return {\r\n name: 'dcs-content',\r\n\r\n configResolved(config) {\r\n resolvedConfig = config\r\n },\r\n\r\n config(config) {\r\n const projectRoot = config.root || process.cwd()\r\n\r\n // Try to find content.yaml in multiple locations\r\n // VitePress projects have the docs folder as root, so check parent too\r\n const possiblePaths = [\r\n path.resolve(projectRoot, contentPath),\r\n path.resolve(projectRoot, '..', contentPath),\r\n path.resolve(process.cwd(), contentPath),\r\n ]\r\n\r\n let foundPath: string | undefined\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n foundPath = testPath\r\n break\r\n }\r\n }\r\n\r\n if (!foundPath) {\r\n if (debug) {\r\n console.log('[dcs-content] No content.yaml found at:')\r\n possiblePaths.forEach((p) => console.log(` - ${p}`))\r\n console.log('[dcs-content] Using defaults only')\r\n }\r\n return {\r\n define: {\r\n __DCS_CONTENT__: 'undefined',\r\n },\r\n }\r\n }\r\n\r\n try {\r\n const fileContent = fs.readFileSync(foundPath, 'utf8')\r\n const content = yaml.load(fileContent) as DcsContentFile\r\n\r\n if (debug) {\r\n console.log(`[dcs-content] Loaded ${foundPath}`)\r\n console.log(`[dcs-content] Version: ${content.version}`)\r\n console.log(`[dcs-content] Pages: ${Object.keys(content.pages ?? {}).join(', ') || '(none)'}`)\r\n console.log(`[dcs-content] Global keys: ${Object.keys(content.global ?? {}).length}`)\r\n }\r\n\r\n return {\r\n define: {\r\n __DCS_CONTENT__: JSON.stringify(content),\r\n },\r\n }\r\n } catch (error) {\r\n console.warn('[dcs-content] Failed to parse content.yaml:', error)\r\n return {\r\n define: {\r\n __DCS_CONTENT__: 'undefined',\r\n },\r\n }\r\n }\r\n },\r\n\r\n // Watch for changes in development\r\n configureServer(server) {\r\n const projectRoot = resolvedConfig?.root || process.cwd()\r\n\r\n const watchPaths = [\r\n path.resolve(projectRoot, contentPath),\r\n path.resolve(projectRoot, '..', contentPath),\r\n ]\r\n\r\n watchPaths.forEach((watchPath) => {\r\n if (fs.existsSync(watchPath)) {\r\n server.watcher.add(watchPath)\r\n\r\n server.watcher.on('change', (changedPath) => {\r\n if (changedPath === watchPath) {\r\n if (debug) {\r\n console.log('[dcs-content] content.yaml changed, triggering reload')\r\n }\r\n server.restart()\r\n }\r\n })\r\n }\r\n })\r\n },\r\n }\r\n}\r\n","/**\r\n * DCS SEO Plugin for Vite\r\n *\r\n * Reads `.dcs/seo.yaml` at build time and injects content\r\n * as `__DCS_SEO__` global variable for use by useSEO.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsSeoPlugin({ debug: true })\r\n * ]\r\n * })\r\n * ```\r\n *\r\n * For VitePress:\r\n * ```typescript\r\n * // .vitepress/config.ts\r\n * import { defineConfig } from 'vitepress'\r\n * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * vite: {\r\n * plugins: [\r\n * dcsSeoPlugin()\r\n * ]\r\n * }\r\n * })\r\n * ```\r\n */\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport yaml from 'js-yaml'\r\nimport type { Plugin, ResolvedConfig } from 'vite'\r\nimport type { SeoConfiguration } from '../types/seo'\r\n\r\nexport interface DcsSeoPluginOptions {\r\n /** Path to seo.yaml relative to project root (default: '.dcs/seo.yaml') */\r\n seoPath?: string\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects .dcs/seo.yaml at build time.\r\n *\r\n * @param options - Plugin configuration\r\n * @returns Vite plugin\r\n */\r\nexport function dcsSeoPlugin(options: DcsSeoPluginOptions = {}): Plugin {\r\n const { seoPath = '.dcs/seo.yaml', debug = false } = options\r\n\r\n let resolvedConfig: ResolvedConfig\r\n\r\n return {\r\n name: 'dcs-seo',\r\n\r\n configResolved(config) {\r\n resolvedConfig = config\r\n },\r\n\r\n config(config) {\r\n const projectRoot = config.root || process.cwd()\r\n\r\n // Try to find seo.yaml in multiple locations\r\n // VitePress projects have the docs folder as root, so check parent too\r\n const possiblePaths = [\r\n path.resolve(projectRoot, seoPath),\r\n path.resolve(projectRoot, '..', seoPath),\r\n path.resolve(process.cwd(), seoPath),\r\n ]\r\n\r\n let foundPath: string | undefined\r\n for (const testPath of possiblePaths) {\r\n if (fs.existsSync(testPath)) {\r\n foundPath = testPath\r\n break\r\n }\r\n }\r\n\r\n if (!foundPath) {\r\n if (debug) {\r\n console.log('[dcs-seo] No seo.yaml found at:')\r\n possiblePaths.forEach((p) => console.log(` - ${p}`))\r\n console.log('[dcs-seo] Using defaults only')\r\n }\r\n return {\r\n define: {\r\n __DCS_SEO__: 'undefined',\r\n },\r\n }\r\n }\r\n\r\n try {\r\n const fileContent = fs.readFileSync(foundPath, 'utf8')\r\n const seoConfig = yaml.load(fileContent) as SeoConfiguration\r\n\r\n if (debug) {\r\n console.log(`[dcs-seo] Loaded ${foundPath}`)\r\n console.log(`[dcs-seo] Version: ${seoConfig.version}`)\r\n console.log(`[dcs-seo] Site Name: ${seoConfig.global?.siteName || '(not set)'}`)\r\n console.log(`[dcs-seo] Pages: ${Object.keys(seoConfig.pages ?? {}).join(', ') || '(none)'}`)\r\n }\r\n\r\n return {\r\n define: {\r\n __DCS_SEO__: JSON.stringify(seoConfig),\r\n },\r\n }\r\n } catch (error) {\r\n console.warn('[dcs-seo] Failed to parse seo.yaml:', error)\r\n return {\r\n define: {\r\n __DCS_SEO__: 'undefined',\r\n },\r\n }\r\n }\r\n },\r\n\r\n // Watch for changes in development\r\n configureServer(server) {\r\n const projectRoot = resolvedConfig?.root || process.cwd()\r\n\r\n const watchPaths = [\r\n path.resolve(projectRoot, seoPath),\r\n path.resolve(projectRoot, '..', seoPath),\r\n ]\r\n\r\n watchPaths.forEach((watchPath) => {\r\n if (fs.existsSync(watchPath)) {\r\n server.watcher.add(watchPath)\r\n\r\n server.watcher.on('change', (changedPath) => {\r\n if (changedPath === watchPath) {\r\n if (debug) {\r\n console.log('[dcs-seo] seo.yaml changed, triggering reload')\r\n }\r\n server.restart()\r\n }\r\n })\r\n }\r\n })\r\n },\r\n }\r\n}\r\n","/**\r\n * DCS Editor Plugin for Vite\r\n *\r\n * Injects the editor bridge script into customer sites when running\r\n * in portal preview mode (inside the visual editor iframe).\r\n *\r\n * The bridge enables:\r\n * - Inline text editing via contenteditable\r\n * - Section hover highlights and AI ✨ buttons\r\n * - postMessage communication with the portal\r\n *\r\n * This plugin should only be active in development/preview mode.\r\n * It's safe to include in production builds — it does nothing unless\r\n * the page detects it's inside an iframe.\r\n *\r\n * @example\r\n * ```typescript\r\n * // vite.config.ts\r\n * import { dcsEditorPlugin } from '@duffcloudservices/cms/plugins'\r\n *\r\n * export default defineConfig({\r\n * plugins: [\r\n * dcsEditorPlugin()\r\n * ]\r\n * })\r\n * ```\r\n */\r\n\r\nimport type { Plugin } from 'vite'\r\n\r\nexport interface DcsEditorPluginOptions {\r\n /** Enable debug logging */\r\n debug?: boolean\r\n}\r\n\r\n/**\r\n * Vite plugin that injects the editor bridge script for portal preview integration.\r\n *\r\n * The bridge auto-initializes only when the page detects it's running inside an iframe,\r\n * so it's safe to include in all builds — it's a no-op in standalone browsing.\r\n *\r\n * Uses a virtual module (`/__dcs-editor-bridge.js`) served through Vite's dev server\r\n * so that bare module specifiers (like `@duffcloudservices/cms/editor`) are properly\r\n * resolved through Vite's module graph rather than hitting the browser's native ESM\r\n * resolver, which can't handle bare specifiers.\r\n */\r\nexport function dcsEditorPlugin(options: DcsEditorPluginOptions = {}): Plugin {\r\n const { debug = false } = options\r\n const VIRTUAL_PATH = '/__dcs-editor-bridge.js'\r\n const RIBBON_VIRTUAL_PATH = '/__dcs-preview-ribbon.js'\r\n\r\n return {\r\n name: 'dcs-editor-bridge',\r\n\r\n resolveId(id) {\r\n if (id === VIRTUAL_PATH || id === RIBBON_VIRTUAL_PATH) {\r\n return id\r\n }\r\n },\r\n\r\n load(id) {\r\n if (id === VIRTUAL_PATH) {\r\n return `\r\nimport { initEditorBridge } from '@duffcloudservices/cms/editor'\r\n// The bridge only activates inside an iframe (portal preview)\r\nif (window.parent !== window) {\r\n if (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', () => setTimeout(initEditorBridge, 200))\r\n } else {\r\n setTimeout(initEditorBridge, 200)\r\n }\r\n}\r\n`\r\n }\r\n\r\n if (id === RIBBON_VIRTUAL_PATH) {\r\n return `\r\nimport { createApp } from 'vue'\r\nimport PreviewRibbon from '@duffcloudservices/cms/components'\r\n\r\nfunction mountRibbon() {\r\n // Create a detached mount point\r\n const el = document.createElement('div')\r\n el.id = 'dcs-preview-ribbon-root'\r\n document.body.appendChild(el)\r\n\r\n const app = createApp(PreviewRibbon)\r\n app.mount(el)\r\n}\r\n\r\nif (document.readyState === 'loading') {\r\n document.addEventListener('DOMContentLoaded', mountRibbon)\r\n} else {\r\n mountRibbon()\r\n}\r\n`\r\n }\r\n },\r\n\r\n transformIndexHtml(html) {\r\n if (debug) {\r\n console.log('[dcs-editor] Injecting editor bridge + preview ribbon script tags')\r\n }\r\n\r\n // Inject both the editor bridge and preview ribbon scripts\r\n const editorTag = `<script type=\"module\" src=\"${VIRTUAL_PATH}\"></script>`\r\n const ribbonTag = `<script type=\"module\" src=\"${RIBBON_VIRTUAL_PATH}\"></script>`\r\n\r\n // Insert before closing </body> tag\r\n return html.replace('</body>', `${editorTag}\\n${ribbonTag}\\n</body>`)\r\n },\r\n }\r\n}\r\n","/**\r\n * DCS Preview Plugin for Vue\r\n *\r\n * Registers a supplied ribbon component as a global `DcsPreviewRibbon`\r\n * component. The ribbon handles its own visibility — it only renders on\r\n * `preview.duffcloudservices.com` and hides everywhere else (localhost,\r\n * production domains, and inside the visual page editor iframe).\r\n *\r\n * Why does the caller pass the component in? Because this file is compiled\r\n * by tsup (esbuild) which has no `.vue` SFC loader. Keeping the raw `.vue`\r\n * import out of the compiled plugins bundle avoids the build error while\r\n * still letting consumer code (which *does* run through Vite) resolve the\r\n * SFC at dev/build time.\r\n *\r\n * @example VitePress theme/index.ts\r\n * ```typescript\r\n * import { dcsPreviewPlugin } from '@duffcloudservices/cms/plugins'\r\n * import PreviewRibbon from '@duffcloudservices/cms/components'\r\n *\r\n * export default {\r\n * Layout,\r\n * enhanceApp({ app }) {\r\n * app.use(dcsPreviewPlugin(PreviewRibbon))\r\n * }\r\n * }\r\n * ```\r\n */\r\n\r\nimport { defineComponent, h, type App, type Component, type Plugin } from 'vue'\r\n\r\nexport interface DcsPreviewPluginOptions {\r\n /**\r\n * Override the version string displayed on the ribbon.\r\n * If omitted, the ribbon auto-detects from VITE_SITE_VERSION or the API.\r\n */\r\n version?: string | null\r\n}\r\n\r\n/**\r\n * Creates and returns the DCS Preview plugin.\r\n *\r\n * When installed, it registers the supplied ribbon component as a global\r\n * `DcsPreviewRibbon` component. Add `<DcsPreviewRibbon />` to your root\r\n * Layout, or use the `dcsEditorPlugin` Vite plugin which injects it via\r\n * `transformIndexHtml`.\r\n *\r\n * @param ribbonComponent - The PreviewRibbon SFC (imported by the consumer)\r\n * @param options - Optional configuration\r\n */\r\nexport function dcsPreviewPlugin(\r\n ribbonComponent: Component,\r\n options: DcsPreviewPluginOptions = {},\r\n): Plugin {\r\n const Wrapper = defineComponent({\r\n name: 'DcsPreviewRibbon',\r\n setup() {\r\n return () => h(ribbonComponent, { version: options.version ?? null })\r\n },\r\n })\r\n\r\n return {\r\n install(app: App) {\r\n app.component('DcsPreviewRibbon', Wrapper)\r\n\r\n if (options.version !== undefined) {\r\n app.provide('__dcs_preview_version__', options.version)\r\n }\r\n },\r\n }\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,72 +1,77 @@
1
- {
2
- "name": "@duffcloudservices/cms",
3
- "version": "0.1.3",
4
- "description": "Vue 3 composables and Vite plugins for DCS CMS integration",
5
- "type": "module",
6
- "exports": {
7
- ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
10
- },
11
- "./plugins": {
12
- "types": "./dist/plugins/index.d.ts",
13
- "import": "./dist/plugins/index.js"
14
- },
15
- "./editor": {
16
- "types": "./dist/editor/editorBridge.d.ts",
17
- "import": "./dist/editor/editorBridge.js"
18
- }
19
- },
20
- "main": "./dist/index.js",
21
- "types": "./dist/index.d.ts",
22
- "files": [
23
- "dist"
24
- ],
25
- "peerDependencies": {
26
- "vue": "^3.4.0",
27
- "@unhead/vue": "^1.9.0"
28
- },
29
- "dependencies": {
30
- "js-yaml": "^4.1.0"
31
- },
32
- "devDependencies": {
33
- "@types/js-yaml": "^4.0.9",
34
- "@types/node": "^20.11.0",
35
- "@vue/test-utils": "^2.4.0",
36
- "tsup": "^8.0.0",
37
- "typescript": "~5.6.3",
38
- "vite": "^6.3.5",
39
- "vitest": "^3.2.3",
40
- "vue": "^3.5.16",
41
- "@unhead/vue": "^2.0.5"
42
- },
43
- "keywords": [
44
- "vue",
45
- "vitepress",
46
- "cms",
47
- "dcs",
48
- "composables",
49
- "duff-cloud-services"
50
- ],
51
- "author": "Duff Cloud Services",
52
- "license": "MIT",
53
- "repository": {
54
- "type": "git",
55
- "url": "https://github.com/duffn/dcs"
56
- },
57
- "homepage": "https://portal.duffcloudservices.com",
58
- "bugs": {
59
- "url": "https://github.com/duffn/dcs/issues"
60
- },
61
- "engines": {
62
- "node": ">=18.0.0"
63
- },
64
- "scripts": {
65
- "build": "tsup",
66
- "dev": "tsup --watch",
67
- "test": "vitest run",
68
- "test:watch": "vitest",
69
- "type-check": "tsc --noEmit",
70
- "lint": "eslint src --ext .ts"
71
- }
72
- }
1
+ {
2
+ "name": "@duffcloudservices/cms",
3
+ "version": "0.1.5",
4
+ "description": "Vue 3 composables and Vite plugins for DCS CMS integration",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./plugins": {
12
+ "types": "./dist/plugins/index.d.ts",
13
+ "import": "./dist/plugins/index.js"
14
+ },
15
+ "./editor": {
16
+ "types": "./dist/editor/editorBridge.d.ts",
17
+ "import": "./dist/editor/editorBridge.js"
18
+ },
19
+ "./components": {
20
+ "import": "./src/components/PreviewRibbon.vue"
21
+ }
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist",
27
+ "src/components"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "dev": "tsup --watch",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "type-check": "tsc --noEmit",
35
+ "lint": "eslint src --ext .ts",
36
+ "prepublishOnly": "pnpm run build"
37
+ },
38
+ "peerDependencies": {
39
+ "vue": "^3.4.0",
40
+ "@unhead/vue": "^1.9.0"
41
+ },
42
+ "dependencies": {
43
+ "js-yaml": "^4.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/js-yaml": "^4.0.9",
47
+ "@types/node": "^20.11.0",
48
+ "@vue/test-utils": "^2.4.0",
49
+ "tsup": "^8.0.0",
50
+ "typescript": "~5.6.3",
51
+ "vite": "^6.3.5",
52
+ "vitest": "^3.2.3",
53
+ "vue": "^3.5.16",
54
+ "@unhead/vue": "^2.0.5"
55
+ },
56
+ "keywords": [
57
+ "vue",
58
+ "vitepress",
59
+ "cms",
60
+ "dcs",
61
+ "composables",
62
+ "duff-cloud-services"
63
+ ],
64
+ "author": "Duff Cloud Services",
65
+ "license": "MIT",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "https://github.com/duffn/dcs"
69
+ },
70
+ "homepage": "https://portal.duffcloudservices.com",
71
+ "bugs": {
72
+ "url": "https://github.com/duffn/dcs/issues"
73
+ },
74
+ "engines": {
75
+ "node": ">=18.0.0"
76
+ }
77
+ }
@@ -0,0 +1,437 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div v-if="isVisible" class="dcs-ribbon-container">
4
+ <svg
5
+ ref="ribbonSvgRef"
6
+ :viewBox="`0 0 ${SVG_SIZE} ${SVG_SIZE}`"
7
+ class="dcs-ribbon-svg"
8
+ >
9
+ <defs>
10
+ <path id="dcs-ribbon-text-path" :d="centerPath" />
11
+ <linearGradient id="dcs-ribbon-grad" x1="0%" y1="100%" x2="100%" y2="0%">
12
+ <stop offset="0%" stop-color="#1a1a2e" />
13
+ <stop offset="40%" stop-color="#16213e" />
14
+ <stop offset="100%" stop-color="#0f3460" />
15
+ </linearGradient>
16
+ <filter id="dcs-ribbon-ts">
17
+ <feDropShadow dx="0" dy="1" stdDeviation="0.8" flood-color="rgba(0,0,0,0.5)" />
18
+ </filter>
19
+ </defs>
20
+
21
+ <!-- Drop shadow (offset copy of band) -->
22
+ <path :d="bandPath" fill="rgba(0,0,0,0.2)" transform="translate(2,3)" />
23
+
24
+ <!-- Main ribbon band -->
25
+ <path
26
+ :d="bandPath"
27
+ fill="url(#dcs-ribbon-grad)"
28
+ stroke="rgba(255,255,255,0.08)"
29
+ stroke-width="1"
30
+ :class="['dcs-ribbon-band', { 'dcs-ribbon-band--dragging': isDragging }]"
31
+ @pointerdown.stop.prevent="onRibbonPointerDown"
32
+ />
33
+
34
+ <!-- Decorative stitched borders -->
35
+ <path :d="outerStitchPath" class="dcs-ribbon-stitch" />
36
+ <path :d="innerStitchPath" class="dcs-ribbon-stitch" />
37
+
38
+ <!-- Text: full (wide screens) -->
39
+ <text
40
+ class="dcs-ribbon-text dcs-ribbon-text--full"
41
+ dominant-baseline="central"
42
+ filter="url(#dcs-ribbon-ts)"
43
+ >
44
+ <textPath href="#dcs-ribbon-text-path" startOffset="50%" text-anchor="middle">
45
+ Duff Cloud Services Preview Site{{ displayVersion }}
46
+ </textPath>
47
+ </text>
48
+
49
+ <!-- Text: compact (narrow screens) -->
50
+ <text
51
+ class="dcs-ribbon-text dcs-ribbon-text--compact"
52
+ dominant-baseline="central"
53
+ filter="url(#dcs-ribbon-ts)"
54
+ >
55
+ <textPath href="#dcs-ribbon-text-path" startOffset="50%" text-anchor="middle">
56
+ DCS Preview{{ displayVersion }}
57
+ </textPath>
58
+ </text>
59
+ </svg>
60
+ </div>
61
+ </Teleport>
62
+ </template>
63
+
64
+ <script setup lang="ts">
65
+ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
66
+
67
+ /* ------------------------------------------------------------------ */
68
+ /* Props */
69
+ /* ------------------------------------------------------------------ */
70
+ const props = withDefaults(
71
+ defineProps<{
72
+ /** Override the auto-detected version string (e.g. "1.2.3"). */
73
+ version?: string | null
74
+ }>(),
75
+ {
76
+ version: null,
77
+ },
78
+ )
79
+
80
+ /* ------------------------------------------------------------------ */
81
+ /* Geometry constants */
82
+ /* ------------------------------------------------------------------ */
83
+
84
+ /** SVG viewBox size (square, covers the ribbon corner area) */
85
+ const SVG_SIZE = 260
86
+
87
+ /** Where the ribbon center-line meets each viewport edge */
88
+ const EP = 220
89
+
90
+ /** Half the ribbon band width (perpendicular to curve) */
91
+ const HW = 20
92
+
93
+ /** Default bezier control-point position (x = y → symmetric 45° curve) */
94
+ const CP0 = 90
95
+
96
+ /**
97
+ * Perpendicular offset for band edges relative to center control point.
98
+ * HW / √2 because the perpendicular to a 45° diagonal has (1/√2, 1/√2)
99
+ * components.
100
+ */
101
+ const CPO = Math.round(HW / Math.SQRT2) // ≈ 14
102
+
103
+ /** Stitch-line inset from band edges (px in SVG space) */
104
+ const STI = 4
105
+
106
+ /* ------------------------------------------------------------------ */
107
+ /* Constants */
108
+ /* ------------------------------------------------------------------ */
109
+
110
+ /** The preview domain where the ribbon should be displayed */
111
+ const PREVIEW_DOMAIN = 'preview.duffcloudservices.com'
112
+
113
+ /* ------------------------------------------------------------------ */
114
+ /* Visibility gate */
115
+ /* ------------------------------------------------------------------ */
116
+ const isVisible = ref(false)
117
+
118
+ function shouldShow(): boolean {
119
+ if (globalThis.window === undefined) return false
120
+
121
+ const host = globalThis.location.hostname
122
+
123
+ // Only show on the preview domain
124
+ if (host !== PREVIEW_DOMAIN) return false
125
+
126
+ // Never show in the visual page editor (iframe inside portal)
127
+ try {
128
+ if (globalThis.self !== globalThis.top) return false
129
+ } catch {
130
+ // cross-origin – inside an iframe → editor context
131
+ return false
132
+ }
133
+
134
+ return true
135
+ }
136
+
137
+ onMounted(() => {
138
+ isVisible.value = shouldShow()
139
+ })
140
+
141
+ /* ------------------------------------------------------------------ */
142
+ /* Version display */
143
+ /* ------------------------------------------------------------------ */
144
+ const resolvedVersion = ref<string | null>(props.version ?? null)
145
+
146
+ const displayVersion = computed(() =>
147
+ resolvedVersion.value ? ` v${resolvedVersion.value}` : '',
148
+ )
149
+
150
+ // Try to read the version from VITE_SITE_VERSION env var at build time
151
+ onMounted(async () => {
152
+ if (resolvedVersion.value) return
153
+
154
+ // Check Vite env first
155
+ try {
156
+ const envVersion = (import.meta.env as Record<string, string | undefined>)
157
+ .VITE_SITE_VERSION
158
+ if (envVersion) {
159
+ resolvedVersion.value = envVersion
160
+ return
161
+ }
162
+ } catch {
163
+ /* noop */
164
+ }
165
+
166
+ // Fallback: fetch from API (reuse useSiteVersion logic inline to avoid
167
+ // coupling lifecycle hooks – the ribbon may mount before Pinia is ready)
168
+ try {
169
+ const slug =
170
+ (import.meta.env as Record<string, string | undefined>).VITE_SITE_SLUG ??
171
+ ''
172
+ if (!slug) return
173
+ const base =
174
+ (import.meta.env as Record<string, string | undefined>)
175
+ .VITE_API_BASE_URL ?? 'https://portal.duffcloudservices.com'
176
+ const res = await fetch(
177
+ `${base}/api/v1/sites/${slug}/release-notes/latest`,
178
+ { headers: { Accept: 'application/json' } },
179
+ )
180
+ if (res.ok) {
181
+ const data = await res.json()
182
+ if (data.version) resolvedVersion.value = data.version
183
+ }
184
+ } catch {
185
+ /* non-critical */
186
+ }
187
+ })
188
+
189
+ // Sync prop changes
190
+ watch(
191
+ () => props.version,
192
+ (v) => {
193
+ if (v !== null && v !== undefined) resolvedVersion.value = v
194
+ },
195
+ )
196
+
197
+ /* ------------------------------------------------------------------ */
198
+ /* Elastic stretch state (control-point displacement) */
199
+ /* ------------------------------------------------------------------ */
200
+ const stretchX = ref(0)
201
+ const stretchY = ref(0)
202
+ const isDragging = ref(false)
203
+
204
+ /** Effective control-point position (default + stretch offset) */
205
+ const cpX = computed(() => CP0 + stretchX.value)
206
+ const cpY = computed(() => CP0 + stretchY.value)
207
+
208
+ /* ------------------------------------------------------------------ */
209
+ /* SVG path computation */
210
+ /* ------------------------------------------------------------------ */
211
+
212
+ /** Quadratic bezier from left-edge (0, y0) to top-edge (x1, 0) */
213
+ function q(y0: number, cx: number, cy: number, x1: number): string {
214
+ return `M 0,${y0} Q ${cx},${cy} ${x1},0`
215
+ }
216
+
217
+ const centerPath = computed(() => q(EP, cpX.value, cpY.value, EP))
218
+
219
+ const outerStitchPath = computed(() =>
220
+ q(
221
+ EP + HW - STI,
222
+ cpX.value + CPO - 3,
223
+ cpY.value + CPO - 3,
224
+ EP + HW - STI,
225
+ ),
226
+ )
227
+
228
+ const innerStitchPath = computed(() =>
229
+ q(
230
+ EP - HW + STI,
231
+ cpX.value - CPO + 3,
232
+ cpY.value - CPO + 3,
233
+ EP - HW + STI,
234
+ ),
235
+ )
236
+
237
+ const bandPath = computed(() => {
238
+ const oY = EP + HW
239
+ const oX = EP + HW
240
+ const oCx = cpX.value + CPO
241
+ const oCy = cpY.value + CPO
242
+ const iY = EP - HW
243
+ const iX = EP - HW
244
+ const iCx = cpX.value - CPO
245
+ const iCy = cpY.value - CPO
246
+
247
+ return [
248
+ // Outer curve (left-edge → top-edge)
249
+ `M 0,${oY} Q ${oCx},${oCy} ${oX},0`,
250
+ // Cap at top-edge → inner curve start
251
+ `L ${iX},0`,
252
+ // Inner curve (top-edge → left-edge, reversed direction)
253
+ `Q ${iCx},${iCy} 0,${iY}`,
254
+ `Z`,
255
+ ].join(' ')
256
+ })
257
+
258
+ /* ------------------------------------------------------------------ */
259
+ /* Drag: elastic ribbon stretch */
260
+ /* ------------------------------------------------------------------ */
261
+ const ribbonSvgRef = ref<SVGSVGElement | null>(null)
262
+ let dragState: { startX: number; startY: number } | null = null
263
+ let springRaf = 0
264
+ let capturedTarget: Element | null = null
265
+
266
+ function onRibbonPointerDown(e: PointerEvent) {
267
+ // Cancel any in-progress spring animation
268
+ if (springRaf) {
269
+ cancelAnimationFrame(springRaf)
270
+ springRaf = 0
271
+ }
272
+
273
+ // Capture the pointer on the element that received the event (the band path).
274
+ // This ensures all subsequent pointer events are delivered to this element,
275
+ // preventing the browser from cancelling the pointer sequence for scrolling.
276
+ const target = e.currentTarget as Element
277
+ target.setPointerCapture(e.pointerId)
278
+ capturedTarget = target
279
+
280
+ isDragging.value = true
281
+ dragState = { startX: e.clientX, startY: e.clientY }
282
+ globalThis.addEventListener('pointermove', onPointerMove)
283
+ globalThis.addEventListener('pointerup', onPointerUp)
284
+ globalThis.addEventListener('pointercancel', onPointerCancel)
285
+ }
286
+
287
+ function onPointerMove(e: PointerEvent) {
288
+ if (!dragState) return
289
+ stretchX.value = e.clientX - dragState.startX
290
+ stretchY.value = e.clientY - dragState.startY
291
+ }
292
+
293
+ function onPointerUp(e?: PointerEvent) {
294
+ if (capturedTarget && e) {
295
+ try { capturedTarget.releasePointerCapture(e.pointerId) } catch { /* already released */ }
296
+ }
297
+ capturedTarget = null
298
+ isDragging.value = false
299
+ dragState = null
300
+ globalThis.removeEventListener('pointermove', onPointerMove)
301
+ globalThis.removeEventListener('pointerup', onPointerUp)
302
+ globalThis.removeEventListener('pointercancel', onPointerCancel)
303
+ animateSnapBack()
304
+ }
305
+
306
+ /** Handle browser cancelling the pointer (e.g. when native scroll takes over) */
307
+ function onPointerCancel(e: PointerEvent) {
308
+ onPointerUp(e)
309
+ }
310
+
311
+ /** Spring-physics animation to snap the control point back to default */
312
+ function animateSnapBack() {
313
+ const stiffness = 0.12
314
+ const damping = 0.72
315
+ let vx = 0
316
+ let vy = 0
317
+
318
+ function step() {
319
+ vx = (vx - stiffness * stretchX.value) * damping
320
+ vy = (vy - stiffness * stretchY.value) * damping
321
+ stretchX.value += vx
322
+ stretchY.value += vy
323
+
324
+ if (
325
+ Math.abs(stretchX.value) < 0.3 &&
326
+ Math.abs(stretchY.value) < 0.3 &&
327
+ Math.abs(vx) < 0.3 &&
328
+ Math.abs(vy) < 0.3
329
+ ) {
330
+ stretchX.value = 0
331
+ stretchY.value = 0
332
+ springRaf = 0
333
+ return
334
+ }
335
+
336
+ springRaf = requestAnimationFrame(step)
337
+ }
338
+
339
+ springRaf = requestAnimationFrame(step)
340
+ }
341
+
342
+ /* ------------------------------------------------------------------ */
343
+ /* Cleanup */
344
+ /* ------------------------------------------------------------------ */
345
+ onBeforeUnmount(() => {
346
+ if (springRaf) cancelAnimationFrame(springRaf)
347
+ globalThis.removeEventListener('pointermove', onPointerMove)
348
+ globalThis.removeEventListener('pointerup', onPointerUp)
349
+ globalThis.removeEventListener('pointercancel', onPointerCancel)
350
+ })
351
+ </script>
352
+
353
+ <style scoped>
354
+ /* ------------------------------------------------------------------ */
355
+ /* Container — fixed top-left, non-blocking */
356
+ /* ------------------------------------------------------------------ */
357
+ .dcs-ribbon-container {
358
+ position: fixed;
359
+ top: 0;
360
+ left: 0;
361
+ z-index: 2147483647;
362
+ pointer-events: none;
363
+ user-select: none;
364
+ -webkit-user-select: none;
365
+ }
366
+
367
+ /* ------------------------------------------------------------------ */
368
+ /* SVG canvas */
369
+ /* ------------------------------------------------------------------ */
370
+ .dcs-ribbon-svg {
371
+ display: block;
372
+ width: 260px;
373
+ height: 260px;
374
+ overflow: visible;
375
+ pointer-events: none;
376
+ }
377
+
378
+ /* ------------------------------------------------------------------ */
379
+ /* Band (interactive) */
380
+ /* ------------------------------------------------------------------ */
381
+ .dcs-ribbon-band {
382
+ pointer-events: auto;
383
+ cursor: grab;
384
+ touch-action: none;
385
+ }
386
+
387
+ .dcs-ribbon-band--dragging {
388
+ cursor: grabbing;
389
+ }
390
+
391
+ /* ------------------------------------------------------------------ */
392
+ /* Stitched borders */
393
+ /* ------------------------------------------------------------------ */
394
+ .dcs-ribbon-stitch {
395
+ fill: none;
396
+ stroke: rgba(255, 255, 255, 0.15);
397
+ stroke-width: 1;
398
+ stroke-dasharray: 4 3;
399
+ pointer-events: none;
400
+ }
401
+
402
+ /* ------------------------------------------------------------------ */
403
+ /* Text */
404
+ /* ------------------------------------------------------------------ */
405
+ .dcs-ribbon-text {
406
+ fill: #e0e7ff;
407
+ font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
408
+ font-weight: 600;
409
+ font-size: 12px;
410
+ letter-spacing: 0.06em;
411
+ text-transform: uppercase;
412
+ pointer-events: none;
413
+ }
414
+
415
+ /* Full label for wide screens */
416
+ .dcs-ribbon-text--full {
417
+ display: block;
418
+ }
419
+ .dcs-ribbon-text--compact {
420
+ display: none;
421
+ }
422
+
423
+ /* Compact label for narrow screens */
424
+ @media (max-width: 640px) {
425
+ .dcs-ribbon-svg {
426
+ width: 200px;
427
+ height: 200px;
428
+ }
429
+ .dcs-ribbon-text--full {
430
+ display: none;
431
+ }
432
+ .dcs-ribbon-text--compact {
433
+ display: block;
434
+ font-size: 11px;
435
+ }
436
+ }
437
+ </style>