@accelerated-agency/visual-editor 0.3.2 → 0.3.4

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.
package/dist/index.js CHANGED
@@ -4807,6 +4807,7 @@ function PlatformVisualEditorV2({
4807
4807
  // channel kept for API compatibility; VvvebJs uses its own internal channel
4808
4808
  embeddedGlobalKey = "__CONVERSION_EMBEDDED__",
4809
4809
  proxyBaseUrl = "",
4810
+ strictObserverFreeze = false,
4810
4811
  className = "fixed inset-0 z-[9999] flex flex-col bg-white",
4811
4812
  editorClassName = "flex-1 min-h-0",
4812
4813
  showHeader = true,
@@ -4867,9 +4868,10 @@ function PlatformVisualEditorV2({
4867
4868
  status: experiment?.status,
4868
4869
  pageUrl: experiment?.pageUrl,
4869
4870
  editorPassword: experiment?.editorPassword,
4871
+ strictObserverFreeze: !!strictObserverFreeze,
4870
4872
  variations: experiment?.variations ?? []
4871
4873
  }),
4872
- [experiment]
4874
+ [experiment, strictObserverFreeze]
4873
4875
  );
4874
4876
  console.log("loadPayload", loadPayload);
4875
4877
  const editorSrc = useMemo(() => {
@@ -4960,7 +4962,7 @@ function PlatformVisualEditorV2({
4960
4962
  window.addEventListener("message", listener);
4961
4963
  return () => window.removeEventListener("message", listener);
4962
4964
  }, [handleMessage]);
4963
- const onTabClick = useCallback(
4965
+ useCallback(
4964
4966
  (tab) => onTabChange?.(tab),
4965
4967
  [onTabChange]
4966
4968
  );
@@ -4972,59 +4974,16 @@ function PlatformVisualEditorV2({
4972
4974
  /* @__PURE__ */ jsx("p", { className: "text-xs text-red-400", children: error })
4973
4975
  ] }) });
4974
4976
  }
4975
- return /* @__PURE__ */ jsxs("div", { className, children: [
4976
- showHeader ? renderHeader?.({
4977
- title: title ?? experiment?.name,
4978
- status: status ?? experiment?.status,
4979
- tabs,
4980
- activeTab,
4981
- onTabClick,
4982
- onClose
4983
- }) ?? /* ── Figma-style top bar ── */
4984
- /* @__PURE__ */ jsxs("div", { className: "h-12 border-b border-slate-200 bg-white flex items-center justify-between px-4 shadow-[0_1px_3px_rgba(0,0,0,0.04)] shrink-0", children: [
4985
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [
4986
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-bold tracking-wide px-1.5 py-0.5 rounded bg-indigo-50 text-indigo-600 border border-indigo-100 shrink-0", children: "V2" }),
4987
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-sm text-slate-800 truncate", children: title ?? experiment?.name ?? "Visual Editor" }),
4988
- status ? /* @__PURE__ */ jsx("span", { className: "hidden sm:inline-flex text-[10px] px-2 py-0.5 rounded-full border border-slate-200 text-slate-500 font-medium bg-slate-50 capitalize", children: status }) : null,
4989
- dirty ? /* @__PURE__ */ jsx(
4990
- "span",
4991
- {
4992
- className: "w-1.5 h-1.5 rounded-full bg-amber-400 shrink-0",
4993
- title: "Unsaved changes"
4994
- }
4995
- ) : null
4996
- ] }),
4997
- tabs.length > 0 ? /* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2", children: tabs.map((tab) => /* @__PURE__ */ jsx(
4998
- "button",
4999
- {
5000
- type: "button",
5001
- onClick: () => onTabClick(tab),
5002
- className: `text-[11px] font-semibold px-3 py-1 rounded-full border transition-all ${tab.label === activeTab ? "bg-indigo-600 text-white border-indigo-600 shadow-sm" : "bg-white text-slate-500 border-slate-200 hover:border-slate-300 hover:text-slate-700"}`,
5003
- children: tab.label
5004
- },
5005
- tab.hash
5006
- )) }) : null,
5007
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: showCloseButton ? /* @__PURE__ */ jsx(
5008
- "button",
5009
- {
5010
- type: "button",
5011
- className: "text-[11px] font-medium px-3 py-1.5 rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300 transition-all",
5012
- onClick: () => sendToVvveb("close-editor", {}),
5013
- children: closeLabel
5014
- }
5015
- ) : null })
5016
- ] }) : null,
5017
- /* @__PURE__ */ jsx("div", { className: editorClassName, children: /* @__PURE__ */ jsx(
5018
- "iframe",
5019
- {
5020
- ref: iframeRef,
5021
- src: editorSrc,
5022
- className: "w-full h-full border-0",
5023
- title: "Vvveb Visual Editor",
5024
- allow: "same-origin"
5025
- }
5026
- ) })
5027
- ] });
4977
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx("div", { className: editorClassName, children: /* @__PURE__ */ jsx(
4978
+ "iframe",
4979
+ {
4980
+ ref: iframeRef,
4981
+ src: editorSrc,
4982
+ className: "w-full h-full border-0",
4983
+ title: "Vvveb Visual Editor",
4984
+ allow: "same-origin"
4985
+ }
4986
+ ) }) });
5028
4987
  }
5029
4988
  var AI_EDITOR_CHANNEL = "ve-ai-editor";
5030
4989
  var AI_VIEWPORT_WIDTH = {
package/dist/vite.cjs CHANGED
@@ -11,8 +11,20 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
11
11
  var path__default = /*#__PURE__*/_interopDefault(path);
12
12
 
13
13
  // src/visualEditorProxyPlugin.ts
14
- var popupHideCss = `<style id="__ce_popup_hide">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"],.klaviyo-form,.klaviyo-modal,.klaviyo-popup,[class*="klaviyo"],.privy-popup,.privy-flyout,[id*="privy"],#PopupSignupForm,.popup-signup,.newsletter-popup,.newsletter-modal,[class*="newsletter-popup"],[class*="newsletter-modal"],[id*="newsletter-popup"],.email-popup,.email-modal,[class*="email-popup"],[class*="email-modal"],.signup-popup,.signup-modal,[class*="signup-popup"],[class*="signup-modal"],.subscribe-popup,.subscribe-modal,[class*="subscribe-popup"],#mc_embed_signup,.mc-modal,.mc-banner,.mc-closeModal,.omniconvert-popup,[class*="omniconvert"],.optinmonster-popup,[id*="om-"][class*="campaign"],.sumo-overlay,.sumome-overlay,[class*="sumome"],.hustle-modal,.hustle-popup,[class*="hustle-"],.popup-overlay,.popup-modal,.modal-overlay,[class*="exit-intent"],[class*="exitintent"],.wheelio-popup,[class*="wheelio"],.spin-wheel-popup,.justuno-popup,[class*="justuno"],.wisepops,.wisepops-overlay,[class*="wisepops"],.elegantmodal,.elegant-popup,#zipify-popup,[class*="zipify"],.age-gate,.age-verification,.age-popup,[class*="age-gate"],[class*="age-verif"],[class*="popup-overlay"],[class*="modal-overlay"],[class*="popup-backdrop"]{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important;height:0!important;overflow:hidden!important;}body.klaviyo-open,body.modal-open,body.popup-open,body.no-scroll,body.noscroll{overflow:auto!important;position:static!important;}</style>`;
15
- var consentAllowCss = `<style id="__ce_consent_allow">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"]{display:revert!important;visibility:visible!important;opacity:1!important;pointer-events:auto!important;height:auto!important;overflow:visible!important;}</style>`;
14
+ var iframeAlwaysShowCss = `<style id="__ce_force_show">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"]{display:revert!important;visibility:visible!important;opacity:1!important;pointer-events:auto!important;height:auto!important;max-height:none!important;overflow:visible!important;}</style>`;
15
+ var iframeAlwaysShowCssGuardScript = `<script id="__ce_force_show_guard">(function(){try{
16
+ function ensureForceShowStyleLast(){
17
+ var style=document.getElementById("__ce_force_show");
18
+ if(!style||!document.head)return;
19
+ if(document.head.lastElementChild!==style){
20
+ document.head.appendChild(style);
21
+ }
22
+ }
23
+ ensureForceShowStyleLast();
24
+ var mo=new MutationObserver(function(){ensureForceShowStyleLast();});
25
+ if(document.head){mo.observe(document.head,{childList:true});}
26
+ window.addEventListener("beforeunload",function(){try{mo.disconnect();}catch(_){}});}
27
+ catch(_){}})();</script>`;
16
28
  var AI_SYSTEM_PROMPT = `You are a CRO expert. Return JSON only with: hypothesis, mutations, summary. Keep 2-6 precise mutations and only valid selectors from snapshot.`;
17
29
  var BRIDGE_SCRIPT = `(function(){if(window.__CONVERSION_BRIDGE_LOADED__)return;window.__CONVERSION_BRIDGE_LOADED__=true;var CHANNEL='conversion-editor';var highlightEl=null;function send(payload){window.parent.postMessage({channel:CHANNEL,payload:payload},'*');}function qs(selector){try{return document.querySelector(selector);}catch(_){return null;}}function getSelector(el){if(!el||el.nodeType!==1)return'';if(el.id)return'#'+CSS.escape(el.id);var parts=[];var current=el;var depth=0;while(current&&current.nodeType===1&&depth<5){var part=current.tagName.toLowerCase();if(current.classList&&current.classList.length){part+='.'+Array.from(current.classList).slice(0,2).map(function(c){return CSS.escape(c);}).join('.');}var parent=current.parentElement;if(parent){var siblings=Array.from(parent.children).filter(function(s){return s.tagName===current.tagName;});if(siblings.length>1)part+=':nth-of-type('+(siblings.indexOf(current)+1)+')';}parts.unshift(part);current=parent;depth+=1;}return parts.join(' > ');}function payloadOf(el){var rect=el.getBoundingClientRect();var style=window.getComputedStyle(el);return{selector:getSelector(el),tagName:el.tagName.toLowerCase(),textContent:(el.textContent||'').trim().slice(0,500),computedStyles:{color:style.color,backgroundColor:style.backgroundColor,fontSize:style.fontSize,fontWeight:style.fontWeight,lineHeight:style.lineHeight,display:style.display},rect:{top:rect.top,left:rect.left,width:rect.width,height:rect.height}};}function applyMutation(m){var el=qs(m.selector);if(!el)return;switch(m.action){case'setStyle':if(m.property)el.style[m.property]=m.value;break;case'setText':el.textContent=m.value;break;case'setHTML':el.innerHTML=m.value;break;case'setAttribute':if(m.property)el.setAttribute(m.property,m.value);break;case'hide':el.style.display='none';break;case'show':el.style.display='';break;case'insertHTML':if(m.position)el.insertAdjacentHTML(m.position,m.value||'');break;case'insertSection':if(m.position)el.insertAdjacentHTML(m.position,m.sectionHtml||m.value||'');break;case'reorderElement':if(!m.targetSelector)break;var target=qs(m.targetSelector);if(!target||!target.parentNode||!el.parentNode)break;if(m.insertPosition==='before')target.parentNode.insertBefore(el,target);else target.parentNode.insertBefore(el,target.nextSibling);break;default:break;}}function revertMutation(m){var el=qs(m.selector);if(!el)return;switch(m.action){case'setStyle':if(m.property)el.style[m.property]=m.previous||'';break;case'setText':el.textContent=m.previous||'';break;case'setHTML':el.innerHTML=m.previous||'';break;case'setAttribute':if(m.property){if(m.previous!=null)el.setAttribute(m.property,m.previous);else el.removeAttribute(m.property);}break;case'hide':case'show':el.style.display=m.previous||'';break;default:break;}}function captureSnapshot(){var elements=Array.from(document.querySelectorAll('h1,h2,h3,p,button,a,input,select,img,section,div')).slice(0,700).map(function(el){var rect=el.getBoundingClientRect();var cs=window.getComputedStyle(el);return{selector:getSelector(el),parentSelector:el.parentElement?getSelector(el.parentElement):null,tagName:el.tagName.toLowerCase(),textContent:(el.textContent||'').trim().slice(0,300),attributes:Array.from(el.attributes||[]).reduce(function(acc,a){acc[a.name]=a.value;return acc;},{}),computedStyles:{color:cs.color,backgroundColor:cs.backgroundColor,fontSize:cs.fontSize,fontWeight:cs.fontWeight,display:cs.display},childrenCount:el.children?el.children.length:0,aboveFold:rect.top<window.innerHeight,depth:(function(){var d=0,p=el.parentElement;while(p&&d<25){d++;p=p.parentElement;}return d;})()};});send({type:'snapshotCaptured',snapshot:{url:window.location.href,title:document.title,viewport:{width:window.innerWidth,height:window.innerHeight},elements:elements}});}function captureTree(){function toNode(el,depth){var rect=el.getBoundingClientRect();var tag=el.tagName.toLowerCase();var type='generic';if(tag==='section')type='section';else if(tag==='img')type='image';else if(tag==='a')type='link';else if(tag==='button')type='button';else if(tag==='form')type='form';else if(['p','h1','h2','h3','h4','h5','h6','span'].indexOf(tag)>=0)type='text';else if(['div','main','article','aside','header','footer','nav'].indexOf(tag)>=0)type='container';var children=Array.from(el.children||[]).slice(0,50).map(function(c){return toNode(c,depth+1);});return{id:getSelector(el),selector:getSelector(el),tagName:tag,label:(el.getAttribute('aria-label')||el.getAttribute('id')||el.className||tag).toString().slice(0,80),type:type,children:children,depth:depth,isAboveFold:rect.top<window.innerHeight,rect:{top:rect.top,left:rect.left,width:rect.width,height:rect.height}};}send({type:'pageTreeCaptured',tree:[toNode(document.body,0)]});}window.addEventListener('message',function(e){var msg=e.data;if(!msg||msg.channel!==CHANNEL||!msg.payload)return;var p=msg.payload;switch(p.type){case'ping':send({type:'pong'});break;case'applyMutation':applyMutation(p.mutation);break;case'applyMutationBatch':(p.mutations||[]).forEach(applyMutation);break;case'revert':revertMutation(p.mutation);break;case'clearAllMutations':window.location.reload();break;case'captureSnapshot':captureSnapshot();break;case'validateSelectors':send({type:'selectorsValidated',results:(p.selectors||[]).map(function(s){var el=qs(s);return{selector:s,found:!!el,tagName:el?el.tagName.toLowerCase():null};})});break;case'scrollToElement':case'selectElement':var target=qs(p.selector);if(target){target.scrollIntoView({behavior:'smooth',block:'center'});send({type:'elementSelected',element:payloadOf(target)});}break;case'hoverElement':var h=qs(p.selector);if(h){if(highlightEl)highlightEl.style.outline='';h.style.outline='2px solid #3b82f6';highlightEl=h;}break;case'capturePageTree':captureTree();break;default:break;}});document.addEventListener('click',function(e){var el=e.target;if(!(el instanceof Element))return;send({type:'elementSelected',element:payloadOf(el)});},true);send({type:'bridgeReady'});})();`;
18
30
  function buildVvvebEditorHtml() {
@@ -254,7 +266,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
254
266
  /* \u2500\u2500 DOM tree (Elements tab) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
255
267
  .dt-tree{font-size:11px;padding:0px 0 0px 20px;user-select:none}
256
268
  .dt-row{
257
- display:flex;align-items:center;gap:2px;min-height:26px;padding:2px 8px 2px 4px;
269
+ width:fit-content;display:flex;align-items:center;gap:2px;min-height:26px;padding:2px 8px 2px 4px;
258
270
  cursor:pointer;color:var(--text-2);border-radius:4px;margin:0 4px
259
271
  }
260
272
  .dt-row:hover{background:var(--bg-hover);color:var(--text)}
@@ -680,10 +692,10 @@ select.pr-inp{cursor:pointer;background:#fff}
680
692
  <button class="tb-dk-btn" id="btn-mode-nav" onclick="setMode('navigate')" title="Navigate mode \u2014 interact with page normally"><i class="bi bi-magic"></i></button>
681
693
  <button class="tb-dk-btn" title="Comments"><i class="bi bi-chat-dots"></i></button>
682
694
  </div>
683
- <!-- btn-close: hidden visually, kept for JS event listener -->
684
- <button id="btn-close" style="display:none" title="Close editor"></button>
685
695
  <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
686
- <button class="tb-fin-btn" id="btn-save">Finalize</button>
696
+ <button class="tb-fin-btn" id="btn-save">Save Changes</button>
697
+ <!-- btn-close: kept for JS event listener -->
698
+ <button class="tb-fin-btn" id="btn-close">Close</button>
687
699
  </div>
688
700
 
689
701
  <!-- url-bar: hidden, kept for JS compatibility -->
@@ -1102,6 +1114,8 @@ var iframeContentNavGen = 0;
1102
1114
  var iframeContentApplyTimer = null;
1103
1115
  var iframeEarlyGranularPrimedForGen = null;
1104
1116
  var iframeEarlySyncPrimedForGen = null;
1117
+ var iframeEarlyDomSignature = '';
1118
+ var iframeEarlyDomSignatureNavGen = 0;
1105
1119
  /** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
1106
1120
  var appliedStructuralChangesetKeys = {};
1107
1121
  var isDirty = false;
@@ -1113,6 +1127,7 @@ var selectedEl = null;
1113
1127
  var selectedElFingerprint = '';
1114
1128
  var selectedElRecoverMisses = 0;
1115
1129
  var MAX_SELECTED_RECOVER_MISSES = 12;
1130
+ var hoveredTreeEl = null;
1116
1131
  var isDeselectingSelection = false;
1117
1132
  var suppressClickUntil = 0;
1118
1133
  var dragAttachDoc = null;
@@ -1126,6 +1141,7 @@ var iframeSyncAttempts = 0;
1126
1141
  var selectionScrollWin = null;
1127
1142
  var selectionResizeBound = false;
1128
1143
  var clickAttachDoc = null;
1144
+ var hoverAttachDoc = null;
1129
1145
  var changeObserver = null;
1130
1146
  var changeObserverDoc = null;
1131
1147
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
@@ -1338,6 +1354,7 @@ function setMode(mode) {
1338
1354
  document.getElementById('btn-mode-editor').classList.toggle('active', mode === 'editor');
1339
1355
  document.getElementById('btn-mode-nav').classList.toggle('active', mode === 'navigate');
1340
1356
  if (mode === 'navigate') {
1357
+ clearTreeHoverHighlight();
1341
1358
  setDragHandleActive(false);
1342
1359
  deselectElement();
1343
1360
  } else if (mode === 'editor') {
@@ -1721,7 +1738,7 @@ function softReloadEditorIframe() {
1721
1738
  resetIframeBindings();
1722
1739
  setIframePageLoadingUi(true);
1723
1740
  iframe.src = appendIframeReloadBust(src);
1724
- startIframeContentApplyWatcher(navGen);
1741
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
1725
1742
  scheduleDomTreeRefresh();
1726
1743
  }
1727
1744
 
@@ -2049,7 +2066,8 @@ function handleLoadExperiment(data) {
2049
2066
  return;
2050
2067
  }
2051
2068
  var proxyUrl = '/api/conversion-proxy?password=' + encodeURIComponent(data.editorPassword || '') +
2052
- '&url=' + encodeURIComponent(pageUrl);
2069
+ '&url=' + encodeURIComponent(pageUrl) +
2070
+ '&strictObserverFreeze=' + encodeURIComponent(data && data.strictObserverFreeze ? '1' : '0');
2053
2071
 
2054
2072
  // Parent often re-posts load-experiment when React re-renders (new object identity) or
2055
2073
  // after mutations-changed. Reloading the iframe again wipes variant changesets mid-session.
@@ -2200,6 +2218,27 @@ function bodyHasFirstPaintChild(body) {
2200
2218
  return false;
2201
2219
  }
2202
2220
 
2221
+ function computeIframeDomSignature(doc) {
2222
+ if (!doc || !doc.body) return '';
2223
+ var body = doc.body;
2224
+ var tags = [];
2225
+ var walker = null;
2226
+ try {
2227
+ walker = doc.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, null);
2228
+ } catch(_) {
2229
+ walker = null;
2230
+ }
2231
+ var count = 0;
2232
+ if (walker) {
2233
+ while (walker.nextNode() && count < 400) {
2234
+ var node = walker.currentNode;
2235
+ tags.push((node && node.tagName) ? String(node.tagName).toLowerCase() : '');
2236
+ count += 1;
2237
+ }
2238
+ }
2239
+ return String(body.children ? body.children.length : 0) + '|' + String(count) + '|' + tags.join(',');
2240
+ }
2241
+
2203
2242
  /** True when at least one granular changeset selector already matches (nested content painted). */
2204
2243
  function granularAnySelectorMatches(doc, cs) {
2205
2244
  if (!doc || !cs || !cs.length) return false;
@@ -2354,6 +2393,7 @@ function resetIframeBindings() {
2354
2393
  appliedStructuralChangesetKeys = {};
2355
2394
  clickAttachDoc = null;
2356
2395
  dragAttachDoc = null;
2396
+ hoverAttachDoc = null;
2357
2397
  changeObserverDoc = null;
2358
2398
  clearPendingGranularChangesets();
2359
2399
  if (changeObserver) {
@@ -2382,7 +2422,7 @@ function loadPage(proxyUrl) {
2382
2422
  iframe.style.display = 'block';
2383
2423
  setIframePageLoadingUi(true);
2384
2424
  iframe.src = appendIframeReloadBust(proxyUrl);
2385
- startIframeContentApplyWatcher(navGen);
2425
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2386
2426
  scheduleDomTreeRefresh();
2387
2427
  }
2388
2428
 
@@ -2454,7 +2494,7 @@ function switchVariation(varId) {
2454
2494
  iframe.src = appendIframeReloadBust(src);
2455
2495
  // Do not sync here: the document is still the previous navigation until the
2456
2496
  // iframe load event; an eager sync attached observers / DOM tree to the wrong document.
2457
- startIframeContentApplyWatcher(navGen);
2497
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2458
2498
  scheduleDomTreeRefresh();
2459
2499
  }
2460
2500
  } catch(_) {}
@@ -2971,10 +3011,12 @@ function reapplyActiveVariationGranular(iframeDoc) {
2971
3011
  }
2972
3012
 
2973
3013
  /** Poll iframe document during navigation; apply granular edits as soon as DOM can match selectors. */
2974
- function startIframeContentApplyWatcher(navGen) {
3014
+ function startIframeContentApplyWatcher(navGen, prevDocRef) {
2975
3015
  stopIframeContentApplyWatcher();
2976
3016
  iframeEarlyGranularPrimedForGen = null;
2977
3017
  iframeEarlySyncPrimedForGen = null;
3018
+ iframeEarlyDomSignature = '';
3019
+ iframeEarlyDomSignatureNavGen = navGen;
2978
3020
  var iframe = document.getElementById('iframeId');
2979
3021
  iframeContentApplyTimer = setInterval(function() {
2980
3022
  if (navGen !== iframeContentNavGen) {
@@ -2984,6 +3026,7 @@ function startIframeContentApplyWatcher(navGen) {
2984
3026
  try {
2985
3027
  var doc = iframe.contentDocument;
2986
3028
  if (!doc || !doc.body) return;
3029
+ if (prevDocRef && doc === prevDocRef) return;
2987
3030
  var docUrl = '';
2988
3031
  try { docUrl = String(doc.URL || ''); } catch(_) {}
2989
3032
  if (docUrl === 'about:blank') return;
@@ -3018,6 +3061,7 @@ function startIframeContentApplyWatcher(navGen) {
3018
3061
  }
3019
3062
  if (iframeEarlySyncPrimedForGen !== navGen) {
3020
3063
  iframeEarlySyncPrimedForGen = navGen;
3064
+ iframeEarlyDomSignature = computeIframeDomSignature(doc);
3021
3065
  syncIframeInteractions('iframe-early-paint');
3022
3066
  }
3023
3067
  }
@@ -3087,6 +3131,8 @@ function injectIframeSelectionStyles(doc) {
3087
3131
  st.textContent =
3088
3132
  '.vve-selected{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3089
3133
  'box-shadow:0 0 0 2px rgba(99,102,241,.28),inset 0 0 0 1px rgba(99,102,241,.18)!important;}' +
3134
+ '.vve-tree-hover{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3135
+ 'box-shadow:0 0 0 2px rgba(99,102,241,.22),inset 0 0 0 1px rgba(99,102,241,.12)!important;}' +
3090
3136
  'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
3091
3137
  '.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
3092
3138
  'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
@@ -3098,6 +3144,47 @@ function injectIframeSelectionStyles(doc) {
3098
3144
  }
3099
3145
  }
3100
3146
 
3147
+ function clearTreeHoverHighlight() {
3148
+ if (!hoveredTreeEl) return;
3149
+ beginSuppressIframeMutationDirty();
3150
+ try {
3151
+ if (hoveredTreeEl.classList) hoveredTreeEl.classList.remove('vve-tree-hover');
3152
+ } catch(_) {
3153
+ } finally {
3154
+ endSuppressIframeMutationDirty();
3155
+ hoveredTreeEl = null;
3156
+ }
3157
+ }
3158
+
3159
+ function setTreeHoverHighlight(el) {
3160
+ if (!el || el.nodeType !== 1) {
3161
+ clearTreeHoverHighlight();
3162
+ return;
3163
+ }
3164
+ if (hoveredTreeEl === el) return;
3165
+ clearTreeHoverHighlight();
3166
+ beginSuppressIframeMutationDirty();
3167
+ try {
3168
+ if (el.classList) el.classList.add('vve-tree-hover');
3169
+ hoveredTreeEl = el;
3170
+ } catch(_) {
3171
+ hoveredTreeEl = null;
3172
+ } finally {
3173
+ endSuppressIframeMutationDirty();
3174
+ }
3175
+ }
3176
+
3177
+ function isTreeHoverOnlyClassMutation(mutation) {
3178
+ if (!mutation || mutation.type !== 'attributes' || mutation.attributeName !== 'class') return false;
3179
+ var oldClass = String(mutation.oldValue || '');
3180
+ var target = mutation.target;
3181
+ var nextClass = '';
3182
+ try {
3183
+ nextClass = target && typeof target.className === 'string' ? target.className : '';
3184
+ } catch(_) {}
3185
+ return oldClass.indexOf('vve-tree-hover') >= 0 || String(nextClass).indexOf('vve-tree-hover') >= 0;
3186
+ }
3187
+
3101
3188
  function setDragHandleActive(on) {
3102
3189
  dragHandleActive = !!on;
3103
3190
  var b = document.getElementById('sf-drag');
@@ -3418,6 +3505,9 @@ function renderDomTree(filterRaw) {
3418
3505
  if (e.target.closest && e.target.closest('.dt-chev')) return;
3419
3506
  selectElementFromTree(el);
3420
3507
  };
3508
+ row.onmouseenter = function() {
3509
+ setTreeHoverHighlight(el);
3510
+ };
3421
3511
  root.appendChild(row);
3422
3512
 
3423
3513
  if (!hasKids || collapsed) return;
@@ -3436,6 +3526,9 @@ function renderDomTree(filterRaw) {
3436
3526
  ? '<div class="dt-muted">No elements match your search.</div>'
3437
3527
  : '<div class="dt-muted">No visible elements yet.</div>';
3438
3528
  }
3529
+ root.onmouseleave = function() {
3530
+ clearTreeHoverHighlight();
3531
+ };
3439
3532
  }
3440
3533
 
3441
3534
  // \u2500\u2500 Utility helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -3855,12 +3948,25 @@ function renderRightPanel(el) {
3855
3948
  var sel = buildSelector(el);
3856
3949
  bindings.forEach(function(b){
3857
3950
  var inp = document.getElementById(b[0]);
3858
- if (inp) inp.addEventListener('input', function(){
3951
+ if (inp) {
3952
+ var onValueChange = function() {
3859
3953
  // Read the original value BEFORE applying the change so we can revert later
3860
3954
  var orig = getOriginalValue(b[0], el);
3861
3955
  b[1](inp.value);
3862
- logChange(sel, b[0], inp.value, el, orig);
3863
- });
3956
+ var valueToLog = inp.value;
3957
+ try {
3958
+ console.log('[V2] input changed', {
3959
+ inputId: b[0],
3960
+ rawValue: inp.value,
3961
+ appliedValue: valueToLog,
3962
+ selector: sel,
3963
+ });
3964
+ } catch(_) {}
3965
+ logChange(sel, b[0], valueToLog, el, orig);
3966
+ };
3967
+ inp.addEventListener('input', onValueChange);
3968
+ inp.addEventListener('change', onValueChange);
3969
+ }
3864
3970
  });
3865
3971
  }
3866
3972
 
@@ -4123,6 +4229,32 @@ function attachClickHandler() {
4123
4229
  } catch(_) {}
4124
4230
  }
4125
4231
 
4232
+ function attachIframeHoverHandler() {
4233
+ try {
4234
+ var iframe = document.getElementById('iframeId');
4235
+ var doc = iframe && iframe.contentDocument;
4236
+ if (!doc || !doc.body) return;
4237
+ if (hoverAttachDoc === doc) return;
4238
+ hoverAttachDoc = doc;
4239
+ doc.addEventListener('mousemove', function(e) {
4240
+ if (currentMode !== 'editor') {
4241
+ clearTreeHoverHighlight();
4242
+ return;
4243
+ }
4244
+ var target = e.target;
4245
+ if (!target || target === doc.body || target === doc.documentElement) {
4246
+ clearTreeHoverHighlight();
4247
+ return;
4248
+ }
4249
+ setTreeHoverHighlight(target);
4250
+ }, true);
4251
+ doc.addEventListener('mouseout', function(e) {
4252
+ if (e.relatedTarget) return;
4253
+ clearTreeHoverHighlight();
4254
+ }, true);
4255
+ } catch(_) {}
4256
+ }
4257
+
4126
4258
  function attachChangeObserver() {
4127
4259
  try {
4128
4260
  var iframe = document.getElementById('iframeId');
@@ -4135,6 +4267,14 @@ function attachChangeObserver() {
4135
4267
  changeObserverDoc = null;
4136
4268
  }
4137
4269
  changeObserver = new MutationObserver(function(mutations) {
4270
+ var hasMeaningfulMutation = false;
4271
+ for (var mi = 0; mi < mutations.length; mi++) {
4272
+ if (!isTreeHoverOnlyClassMutation(mutations[mi])) {
4273
+ hasMeaningfulMutation = true;
4274
+ break;
4275
+ }
4276
+ }
4277
+ if (!hasMeaningfulMutation) return;
4138
4278
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
4139
4279
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
4140
4280
  // Keep selection sticky by re-resolving from fingerprint.
@@ -4145,7 +4285,7 @@ function attachChangeObserver() {
4145
4285
  updateSelectionToolbar();
4146
4286
  });
4147
4287
  changeObserver.observe(doc.body, {
4148
- childList: true, subtree: true, attributes: true, characterData: true
4288
+ childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true
4149
4289
  });
4150
4290
  changeObserverDoc = doc;
4151
4291
  } catch(_) {}
@@ -4171,6 +4311,7 @@ function syncIframeInteractions(reason) {
4171
4311
  injectIframeSelectionStyles(doc);
4172
4312
  refreshPersistentChangesetStyleTagForActiveVariation();
4173
4313
  attachClickHandler();
4314
+ attachIframeHoverHandler();
4174
4315
  attachDragReposition();
4175
4316
  attachChangeObserver();
4176
4317
  startConsistencyWatchdog(doc);
@@ -4529,10 +4670,9 @@ window.addEventListener('load', function() {
4529
4670
  var iframe = document.getElementById('iframeId');
4530
4671
  iframe.addEventListener('load', function() {
4531
4672
  if (!iframe.src || iframe.src === 'about:blank' || iframe.src === window.location.href) return;
4532
- // New iframe navigation: always drop bindings tied to the previous document.
4533
- resetIframeBindings();
4534
4673
  var doc = iframe.contentDocument;
4535
4674
  if (!doc) {
4675
+ resetIframeBindings();
4536
4676
  syncIframeInteractions('iframe-load-no-doc');
4537
4677
  return;
4538
4678
  }
@@ -4541,14 +4681,32 @@ window.addEventListener('load', function() {
4541
4681
  // Stale events: src may already be the proxy URL while the document is still
4542
4682
  // about:blank (e.g. src cleared then reset to force reload). Ask sync path to retry.
4543
4683
  if (docUrl === 'about:blank') {
4684
+ resetIframeBindings();
4544
4685
  syncIframeInteractions('iframe-load-about-blank');
4545
4686
  return;
4546
4687
  }
4688
+ // If early-paint and final load DOM signatures match, avoid a second full apply/reset
4689
+ // that steals focus from sidebar controls while the page is still stabilizing.
4690
+ var shouldRefreshOnFinalLoad = true;
4691
+ if (
4692
+ iframeEarlyDomSignatureNavGen === iframeContentNavGen &&
4693
+ iframeEarlySyncPrimedForGen === iframeContentNavGen &&
4694
+ iframeEarlyDomSignature
4695
+ ) {
4696
+ var finalDomSignature = computeIframeDomSignature(doc);
4697
+ if (finalDomSignature && finalDomSignature === iframeEarlyDomSignature) {
4698
+ shouldRefreshOnFinalLoad = false;
4699
+ }
4700
+ }
4701
+ if (clickAttachDoc !== doc || dragAttachDoc !== doc || changeObserverDoc !== doc || hoverAttachDoc !== doc) {
4702
+ resetIframeBindings();
4703
+ }
4547
4704
  attachIframeLoadingUntilComplete(iframe);
4548
4705
  if (doc.body && iframeDocMatchesNavigatedSrc(iframe, doc)) {
4549
4706
  stopIframeContentApplyWatcher();
4550
- deselectElement();
4551
- applyActiveVariationHtml();
4707
+ if (shouldRefreshOnFinalLoad) {
4708
+ applyActiveVariationHtml();
4709
+ }
4552
4710
  }
4553
4711
  // Always attempt sync; it has its own readiness checks + retry loop.
4554
4712
  syncIframeInteractions('iframe-load');
@@ -4605,6 +4763,7 @@ var getDefaultAnthropicApiKey = () => {
4605
4763
  function createVisualEditorMiddleware(options) {
4606
4764
  const anthropicApiKey = options?.anthropicApiKey || getDefaultAnthropicApiKey();
4607
4765
  const enableGenerateTestApi = options?.enableGenerateTestApi ?? true;
4766
+ const strictObserverFreeze = options?.strictObserverFreeze === true;
4608
4767
  const allowedFrameOrigins = options?.allowedFrameOrigins ?? ["*"];
4609
4768
  function setFrameHeaders(req, res) {
4610
4769
  res.removeHeader("X-Frame-Options");
@@ -4772,6 +4931,8 @@ function createVisualEditorMiddleware(options) {
4772
4931
  const url = new URL(req.url || "", "http://localhost");
4773
4932
  const targetUrl = url.searchParams.get("url");
4774
4933
  const password = url.searchParams.get("password") || "";
4934
+ const strictFreezeParam = (url.searchParams.get("strictObserverFreeze") || "").toLowerCase();
4935
+ const strictObserverFreezeForRequest = strictFreezeParam === "1" || strictFreezeParam === "true" || strictFreezeParam === "yes" ? true : strictFreezeParam === "0" || strictFreezeParam === "false" || strictFreezeParam === "no" ? false : strictObserverFreeze;
4775
4936
  if (!targetUrl) {
4776
4937
  res.statusCode = 400;
4777
4938
  res.end(JSON.stringify({ error: "Missing url parameter" }));
@@ -4890,9 +5051,12 @@ function createVisualEditorMiddleware(options) {
4890
5051
  }
4891
5052
  );
4892
5053
  if (html.includes("</head>")) {
4893
- html = html.replace("</head>", `${popupHideCss}
4894
- ${consentAllowCss}
4895
- </head>`);
5054
+ html = html.replace(
5055
+ "</head>",
5056
+ `${iframeAlwaysShowCss}
5057
+ ${iframeAlwaysShowCssGuardScript}
5058
+ </head>`
5059
+ );
4896
5060
  }
4897
5061
  html = html.replace(
4898
5062
  /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
@@ -4906,6 +5070,7 @@ ${consentAllowCss}
4906
5070
  var TARGET_ORIGIN=${JSON.stringify(origin)};
4907
5071
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4908
5072
  var PROXY_PASSWORD=${JSON.stringify(password)};
5073
+ var STRICT_OBSERVER_FREEZE=${JSON.stringify(strictObserverFreezeForRequest)};
4909
5074
  window.__CONVERSION_EDITOR_ACTIVE__=true;
4910
5075
  function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4911
5076
  function toAbsolute(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")||raw.startsWith("//")?TARGET_ORIGIN:TARGET_PAGE_URL;return new URL(raw,base).toString();}catch(_){return raw;}}
@@ -4925,6 +5090,9 @@ try{
4925
5090
  var wrapped=function(list,obs){
4926
5091
  try{
4927
5092
  if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
5093
+ if(STRICT_OBSERVER_FREEZE){
5094
+ return;
5095
+ }
4928
5096
  var now=Date.now();
4929
5097
  if(now-last<120)return;
4930
5098
  last=now;
@@ -4934,6 +5102,11 @@ try{
4934
5102
  return new NativeMO(wrapped);
4935
5103
  };
4936
5104
  window.MutationObserver.prototype=NativeMO.prototype;
5105
+ try{
5106
+ if(STRICT_OBSERVER_FREEZE){
5107
+ console.info("[conversion-proxy] strict MutationObserver freeze active");
5108
+ }
5109
+ }catch(_){}
4937
5110
  }
4938
5111
  }catch(_){}
4939
5112
  }catch(_){}})();</script>`;
package/dist/vite.js CHANGED
@@ -3,8 +3,20 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
 
5
5
  // src/visualEditorProxyPlugin.ts
6
- var popupHideCss = `<style id="__ce_popup_hide">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"],.klaviyo-form,.klaviyo-modal,.klaviyo-popup,[class*="klaviyo"],.privy-popup,.privy-flyout,[id*="privy"],#PopupSignupForm,.popup-signup,.newsletter-popup,.newsletter-modal,[class*="newsletter-popup"],[class*="newsletter-modal"],[id*="newsletter-popup"],.email-popup,.email-modal,[class*="email-popup"],[class*="email-modal"],.signup-popup,.signup-modal,[class*="signup-popup"],[class*="signup-modal"],.subscribe-popup,.subscribe-modal,[class*="subscribe-popup"],#mc_embed_signup,.mc-modal,.mc-banner,.mc-closeModal,.omniconvert-popup,[class*="omniconvert"],.optinmonster-popup,[id*="om-"][class*="campaign"],.sumo-overlay,.sumome-overlay,[class*="sumome"],.hustle-modal,.hustle-popup,[class*="hustle-"],.popup-overlay,.popup-modal,.modal-overlay,[class*="exit-intent"],[class*="exitintent"],.wheelio-popup,[class*="wheelio"],.spin-wheel-popup,.justuno-popup,[class*="justuno"],.wisepops,.wisepops-overlay,[class*="wisepops"],.elegantmodal,.elegant-popup,#zipify-popup,[class*="zipify"],.age-gate,.age-verification,.age-popup,[class*="age-gate"],[class*="age-verif"],[class*="popup-overlay"],[class*="modal-overlay"],[class*="popup-backdrop"]{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important;height:0!important;overflow:hidden!important;}body.klaviyo-open,body.modal-open,body.popup-open,body.no-scroll,body.noscroll{overflow:auto!important;position:static!important;}</style>`;
7
- var consentAllowCss = `<style id="__ce_consent_allow">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"]{display:revert!important;visibility:visible!important;opacity:1!important;pointer-events:auto!important;height:auto!important;overflow:visible!important;}</style>`;
6
+ var iframeAlwaysShowCss = `<style id="__ce_force_show">#CybotCookiebotDialog,#CybotCookiebotDialogBodyUnderlay,#onetrust-consent-sdk,#onetrust-banner-sdk,.cc-window,.cc-banner,.cc-overlay,#cookie-notice,#cookie-banner,#cookie-consent,.cookie-notice,.cookie-banner,.cookie-consent,.cookie-popup,.cookie-bar,.cookie-message,.cookie-alert,.gdpr-banner,.gdpr-consent,.gdpr-popup,.gdpr-overlay,#gdpr-consent,#gdpr-banner,.consent-banner,.consent-popup,.consent-overlay,#consent-banner,#consent-popup,[class*="cookie-consent"],[class*="cookie-banner"],[class*="cookie-notice"],[class*="CookieConsent"],[class*="CookieBanner"],[id*="cookie-consent"],[id*="cookie-banner"],[id*="cookie-notice"],[aria-label*="cookie" i],[aria-label*="consent" i],.klaro,.klaro .cookie-modal,#usercentrics-root,.trustarc-banner,#truste-consent-track,#hs-eu-cookie-confirmation,.osano-cm-window,.osano-cm-dialog,.evidon-banner,#_evidon_banner,.js-cookie-consent,.cookie-disclaimer,.shopify-section-cookies,#shopify-section-cookies,#shopify-pc__banner,#shopify-pc__modal,.privacy-banner,.privacy-popup,[data-testid="cookie-banner"],[data-testid="consent-banner"],.amgdprcookie-bar-container,[data-amcookie-js="bar"],.amgdprjs-bar-template,.amgdprcookie-modal-container,.amgdprcookie-modal-overlay,#cmplz-cookiebanner-container,.cmplz-cookiebanner,#iubenda-cs-banner,.iubenda-cs-container,#qc-cmp2-container,.qc-cmp2-consent-info,#didomi-host,.didomi-popup-container,.didomi-notice,#termly-code-snippet-support,[class*="termly"],[class*="gdprcookie"],[class*="amgdpr"],[id*="gdpr-cookie"],[class*="cookie-modal"],[id*="cookie-modal"],[class*="cookieConsent"],[id*="cookieConsent"]{display:revert!important;visibility:visible!important;opacity:1!important;pointer-events:auto!important;height:auto!important;max-height:none!important;overflow:visible!important;}</style>`;
7
+ var iframeAlwaysShowCssGuardScript = `<script id="__ce_force_show_guard">(function(){try{
8
+ function ensureForceShowStyleLast(){
9
+ var style=document.getElementById("__ce_force_show");
10
+ if(!style||!document.head)return;
11
+ if(document.head.lastElementChild!==style){
12
+ document.head.appendChild(style);
13
+ }
14
+ }
15
+ ensureForceShowStyleLast();
16
+ var mo=new MutationObserver(function(){ensureForceShowStyleLast();});
17
+ if(document.head){mo.observe(document.head,{childList:true});}
18
+ window.addEventListener("beforeunload",function(){try{mo.disconnect();}catch(_){}});}
19
+ catch(_){}})();</script>`;
8
20
  var AI_SYSTEM_PROMPT = `You are a CRO expert. Return JSON only with: hypothesis, mutations, summary. Keep 2-6 precise mutations and only valid selectors from snapshot.`;
9
21
  var BRIDGE_SCRIPT = `(function(){if(window.__CONVERSION_BRIDGE_LOADED__)return;window.__CONVERSION_BRIDGE_LOADED__=true;var CHANNEL='conversion-editor';var highlightEl=null;function send(payload){window.parent.postMessage({channel:CHANNEL,payload:payload},'*');}function qs(selector){try{return document.querySelector(selector);}catch(_){return null;}}function getSelector(el){if(!el||el.nodeType!==1)return'';if(el.id)return'#'+CSS.escape(el.id);var parts=[];var current=el;var depth=0;while(current&&current.nodeType===1&&depth<5){var part=current.tagName.toLowerCase();if(current.classList&&current.classList.length){part+='.'+Array.from(current.classList).slice(0,2).map(function(c){return CSS.escape(c);}).join('.');}var parent=current.parentElement;if(parent){var siblings=Array.from(parent.children).filter(function(s){return s.tagName===current.tagName;});if(siblings.length>1)part+=':nth-of-type('+(siblings.indexOf(current)+1)+')';}parts.unshift(part);current=parent;depth+=1;}return parts.join(' > ');}function payloadOf(el){var rect=el.getBoundingClientRect();var style=window.getComputedStyle(el);return{selector:getSelector(el),tagName:el.tagName.toLowerCase(),textContent:(el.textContent||'').trim().slice(0,500),computedStyles:{color:style.color,backgroundColor:style.backgroundColor,fontSize:style.fontSize,fontWeight:style.fontWeight,lineHeight:style.lineHeight,display:style.display},rect:{top:rect.top,left:rect.left,width:rect.width,height:rect.height}};}function applyMutation(m){var el=qs(m.selector);if(!el)return;switch(m.action){case'setStyle':if(m.property)el.style[m.property]=m.value;break;case'setText':el.textContent=m.value;break;case'setHTML':el.innerHTML=m.value;break;case'setAttribute':if(m.property)el.setAttribute(m.property,m.value);break;case'hide':el.style.display='none';break;case'show':el.style.display='';break;case'insertHTML':if(m.position)el.insertAdjacentHTML(m.position,m.value||'');break;case'insertSection':if(m.position)el.insertAdjacentHTML(m.position,m.sectionHtml||m.value||'');break;case'reorderElement':if(!m.targetSelector)break;var target=qs(m.targetSelector);if(!target||!target.parentNode||!el.parentNode)break;if(m.insertPosition==='before')target.parentNode.insertBefore(el,target);else target.parentNode.insertBefore(el,target.nextSibling);break;default:break;}}function revertMutation(m){var el=qs(m.selector);if(!el)return;switch(m.action){case'setStyle':if(m.property)el.style[m.property]=m.previous||'';break;case'setText':el.textContent=m.previous||'';break;case'setHTML':el.innerHTML=m.previous||'';break;case'setAttribute':if(m.property){if(m.previous!=null)el.setAttribute(m.property,m.previous);else el.removeAttribute(m.property);}break;case'hide':case'show':el.style.display=m.previous||'';break;default:break;}}function captureSnapshot(){var elements=Array.from(document.querySelectorAll('h1,h2,h3,p,button,a,input,select,img,section,div')).slice(0,700).map(function(el){var rect=el.getBoundingClientRect();var cs=window.getComputedStyle(el);return{selector:getSelector(el),parentSelector:el.parentElement?getSelector(el.parentElement):null,tagName:el.tagName.toLowerCase(),textContent:(el.textContent||'').trim().slice(0,300),attributes:Array.from(el.attributes||[]).reduce(function(acc,a){acc[a.name]=a.value;return acc;},{}),computedStyles:{color:cs.color,backgroundColor:cs.backgroundColor,fontSize:cs.fontSize,fontWeight:cs.fontWeight,display:cs.display},childrenCount:el.children?el.children.length:0,aboveFold:rect.top<window.innerHeight,depth:(function(){var d=0,p=el.parentElement;while(p&&d<25){d++;p=p.parentElement;}return d;})()};});send({type:'snapshotCaptured',snapshot:{url:window.location.href,title:document.title,viewport:{width:window.innerWidth,height:window.innerHeight},elements:elements}});}function captureTree(){function toNode(el,depth){var rect=el.getBoundingClientRect();var tag=el.tagName.toLowerCase();var type='generic';if(tag==='section')type='section';else if(tag==='img')type='image';else if(tag==='a')type='link';else if(tag==='button')type='button';else if(tag==='form')type='form';else if(['p','h1','h2','h3','h4','h5','h6','span'].indexOf(tag)>=0)type='text';else if(['div','main','article','aside','header','footer','nav'].indexOf(tag)>=0)type='container';var children=Array.from(el.children||[]).slice(0,50).map(function(c){return toNode(c,depth+1);});return{id:getSelector(el),selector:getSelector(el),tagName:tag,label:(el.getAttribute('aria-label')||el.getAttribute('id')||el.className||tag).toString().slice(0,80),type:type,children:children,depth:depth,isAboveFold:rect.top<window.innerHeight,rect:{top:rect.top,left:rect.left,width:rect.width,height:rect.height}};}send({type:'pageTreeCaptured',tree:[toNode(document.body,0)]});}window.addEventListener('message',function(e){var msg=e.data;if(!msg||msg.channel!==CHANNEL||!msg.payload)return;var p=msg.payload;switch(p.type){case'ping':send({type:'pong'});break;case'applyMutation':applyMutation(p.mutation);break;case'applyMutationBatch':(p.mutations||[]).forEach(applyMutation);break;case'revert':revertMutation(p.mutation);break;case'clearAllMutations':window.location.reload();break;case'captureSnapshot':captureSnapshot();break;case'validateSelectors':send({type:'selectorsValidated',results:(p.selectors||[]).map(function(s){var el=qs(s);return{selector:s,found:!!el,tagName:el?el.tagName.toLowerCase():null};})});break;case'scrollToElement':case'selectElement':var target=qs(p.selector);if(target){target.scrollIntoView({behavior:'smooth',block:'center'});send({type:'elementSelected',element:payloadOf(target)});}break;case'hoverElement':var h=qs(p.selector);if(h){if(highlightEl)highlightEl.style.outline='';h.style.outline='2px solid #3b82f6';highlightEl=h;}break;case'capturePageTree':captureTree();break;default:break;}});document.addEventListener('click',function(e){var el=e.target;if(!(el instanceof Element))return;send({type:'elementSelected',element:payloadOf(el)});},true);send({type:'bridgeReady'});})();`;
10
22
  function buildVvvebEditorHtml() {
@@ -246,7 +258,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
246
258
  /* \u2500\u2500 DOM tree (Elements tab) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
247
259
  .dt-tree{font-size:11px;padding:0px 0 0px 20px;user-select:none}
248
260
  .dt-row{
249
- display:flex;align-items:center;gap:2px;min-height:26px;padding:2px 8px 2px 4px;
261
+ width:fit-content;display:flex;align-items:center;gap:2px;min-height:26px;padding:2px 8px 2px 4px;
250
262
  cursor:pointer;color:var(--text-2);border-radius:4px;margin:0 4px
251
263
  }
252
264
  .dt-row:hover{background:var(--bg-hover);color:var(--text)}
@@ -672,10 +684,10 @@ select.pr-inp{cursor:pointer;background:#fff}
672
684
  <button class="tb-dk-btn" id="btn-mode-nav" onclick="setMode('navigate')" title="Navigate mode \u2014 interact with page normally"><i class="bi bi-magic"></i></button>
673
685
  <button class="tb-dk-btn" title="Comments"><i class="bi bi-chat-dots"></i></button>
674
686
  </div>
675
- <!-- btn-close: hidden visually, kept for JS event listener -->
676
- <button id="btn-close" style="display:none" title="Close editor"></button>
677
687
  <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
678
- <button class="tb-fin-btn" id="btn-save">Finalize</button>
688
+ <button class="tb-fin-btn" id="btn-save">Save Changes</button>
689
+ <!-- btn-close: kept for JS event listener -->
690
+ <button class="tb-fin-btn" id="btn-close">Close</button>
679
691
  </div>
680
692
 
681
693
  <!-- url-bar: hidden, kept for JS compatibility -->
@@ -1094,6 +1106,8 @@ var iframeContentNavGen = 0;
1094
1106
  var iframeContentApplyTimer = null;
1095
1107
  var iframeEarlyGranularPrimedForGen = null;
1096
1108
  var iframeEarlySyncPrimedForGen = null;
1109
+ var iframeEarlyDomSignature = '';
1110
+ var iframeEarlyDomSignatureNavGen = 0;
1097
1111
  /** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
1098
1112
  var appliedStructuralChangesetKeys = {};
1099
1113
  var isDirty = false;
@@ -1105,6 +1119,7 @@ var selectedEl = null;
1105
1119
  var selectedElFingerprint = '';
1106
1120
  var selectedElRecoverMisses = 0;
1107
1121
  var MAX_SELECTED_RECOVER_MISSES = 12;
1122
+ var hoveredTreeEl = null;
1108
1123
  var isDeselectingSelection = false;
1109
1124
  var suppressClickUntil = 0;
1110
1125
  var dragAttachDoc = null;
@@ -1118,6 +1133,7 @@ var iframeSyncAttempts = 0;
1118
1133
  var selectionScrollWin = null;
1119
1134
  var selectionResizeBound = false;
1120
1135
  var clickAttachDoc = null;
1136
+ var hoverAttachDoc = null;
1121
1137
  var changeObserver = null;
1122
1138
  var changeObserverDoc = null;
1123
1139
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
@@ -1330,6 +1346,7 @@ function setMode(mode) {
1330
1346
  document.getElementById('btn-mode-editor').classList.toggle('active', mode === 'editor');
1331
1347
  document.getElementById('btn-mode-nav').classList.toggle('active', mode === 'navigate');
1332
1348
  if (mode === 'navigate') {
1349
+ clearTreeHoverHighlight();
1333
1350
  setDragHandleActive(false);
1334
1351
  deselectElement();
1335
1352
  } else if (mode === 'editor') {
@@ -1713,7 +1730,7 @@ function softReloadEditorIframe() {
1713
1730
  resetIframeBindings();
1714
1731
  setIframePageLoadingUi(true);
1715
1732
  iframe.src = appendIframeReloadBust(src);
1716
- startIframeContentApplyWatcher(navGen);
1733
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
1717
1734
  scheduleDomTreeRefresh();
1718
1735
  }
1719
1736
 
@@ -2041,7 +2058,8 @@ function handleLoadExperiment(data) {
2041
2058
  return;
2042
2059
  }
2043
2060
  var proxyUrl = '/api/conversion-proxy?password=' + encodeURIComponent(data.editorPassword || '') +
2044
- '&url=' + encodeURIComponent(pageUrl);
2061
+ '&url=' + encodeURIComponent(pageUrl) +
2062
+ '&strictObserverFreeze=' + encodeURIComponent(data && data.strictObserverFreeze ? '1' : '0');
2045
2063
 
2046
2064
  // Parent often re-posts load-experiment when React re-renders (new object identity) or
2047
2065
  // after mutations-changed. Reloading the iframe again wipes variant changesets mid-session.
@@ -2192,6 +2210,27 @@ function bodyHasFirstPaintChild(body) {
2192
2210
  return false;
2193
2211
  }
2194
2212
 
2213
+ function computeIframeDomSignature(doc) {
2214
+ if (!doc || !doc.body) return '';
2215
+ var body = doc.body;
2216
+ var tags = [];
2217
+ var walker = null;
2218
+ try {
2219
+ walker = doc.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, null);
2220
+ } catch(_) {
2221
+ walker = null;
2222
+ }
2223
+ var count = 0;
2224
+ if (walker) {
2225
+ while (walker.nextNode() && count < 400) {
2226
+ var node = walker.currentNode;
2227
+ tags.push((node && node.tagName) ? String(node.tagName).toLowerCase() : '');
2228
+ count += 1;
2229
+ }
2230
+ }
2231
+ return String(body.children ? body.children.length : 0) + '|' + String(count) + '|' + tags.join(',');
2232
+ }
2233
+
2195
2234
  /** True when at least one granular changeset selector already matches (nested content painted). */
2196
2235
  function granularAnySelectorMatches(doc, cs) {
2197
2236
  if (!doc || !cs || !cs.length) return false;
@@ -2346,6 +2385,7 @@ function resetIframeBindings() {
2346
2385
  appliedStructuralChangesetKeys = {};
2347
2386
  clickAttachDoc = null;
2348
2387
  dragAttachDoc = null;
2388
+ hoverAttachDoc = null;
2349
2389
  changeObserverDoc = null;
2350
2390
  clearPendingGranularChangesets();
2351
2391
  if (changeObserver) {
@@ -2374,7 +2414,7 @@ function loadPage(proxyUrl) {
2374
2414
  iframe.style.display = 'block';
2375
2415
  setIframePageLoadingUi(true);
2376
2416
  iframe.src = appendIframeReloadBust(proxyUrl);
2377
- startIframeContentApplyWatcher(navGen);
2417
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2378
2418
  scheduleDomTreeRefresh();
2379
2419
  }
2380
2420
 
@@ -2446,7 +2486,7 @@ function switchVariation(varId) {
2446
2486
  iframe.src = appendIframeReloadBust(src);
2447
2487
  // Do not sync here: the document is still the previous navigation until the
2448
2488
  // iframe load event; an eager sync attached observers / DOM tree to the wrong document.
2449
- startIframeContentApplyWatcher(navGen);
2489
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2450
2490
  scheduleDomTreeRefresh();
2451
2491
  }
2452
2492
  } catch(_) {}
@@ -2963,10 +3003,12 @@ function reapplyActiveVariationGranular(iframeDoc) {
2963
3003
  }
2964
3004
 
2965
3005
  /** Poll iframe document during navigation; apply granular edits as soon as DOM can match selectors. */
2966
- function startIframeContentApplyWatcher(navGen) {
3006
+ function startIframeContentApplyWatcher(navGen, prevDocRef) {
2967
3007
  stopIframeContentApplyWatcher();
2968
3008
  iframeEarlyGranularPrimedForGen = null;
2969
3009
  iframeEarlySyncPrimedForGen = null;
3010
+ iframeEarlyDomSignature = '';
3011
+ iframeEarlyDomSignatureNavGen = navGen;
2970
3012
  var iframe = document.getElementById('iframeId');
2971
3013
  iframeContentApplyTimer = setInterval(function() {
2972
3014
  if (navGen !== iframeContentNavGen) {
@@ -2976,6 +3018,7 @@ function startIframeContentApplyWatcher(navGen) {
2976
3018
  try {
2977
3019
  var doc = iframe.contentDocument;
2978
3020
  if (!doc || !doc.body) return;
3021
+ if (prevDocRef && doc === prevDocRef) return;
2979
3022
  var docUrl = '';
2980
3023
  try { docUrl = String(doc.URL || ''); } catch(_) {}
2981
3024
  if (docUrl === 'about:blank') return;
@@ -3010,6 +3053,7 @@ function startIframeContentApplyWatcher(navGen) {
3010
3053
  }
3011
3054
  if (iframeEarlySyncPrimedForGen !== navGen) {
3012
3055
  iframeEarlySyncPrimedForGen = navGen;
3056
+ iframeEarlyDomSignature = computeIframeDomSignature(doc);
3013
3057
  syncIframeInteractions('iframe-early-paint');
3014
3058
  }
3015
3059
  }
@@ -3079,6 +3123,8 @@ function injectIframeSelectionStyles(doc) {
3079
3123
  st.textContent =
3080
3124
  '.vve-selected{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3081
3125
  'box-shadow:0 0 0 2px rgba(99,102,241,.28),inset 0 0 0 1px rgba(99,102,241,.18)!important;}' +
3126
+ '.vve-tree-hover{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3127
+ 'box-shadow:0 0 0 2px rgba(99,102,241,.22),inset 0 0 0 1px rgba(99,102,241,.12)!important;}' +
3082
3128
  'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
3083
3129
  '.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
3084
3130
  'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
@@ -3090,6 +3136,47 @@ function injectIframeSelectionStyles(doc) {
3090
3136
  }
3091
3137
  }
3092
3138
 
3139
+ function clearTreeHoverHighlight() {
3140
+ if (!hoveredTreeEl) return;
3141
+ beginSuppressIframeMutationDirty();
3142
+ try {
3143
+ if (hoveredTreeEl.classList) hoveredTreeEl.classList.remove('vve-tree-hover');
3144
+ } catch(_) {
3145
+ } finally {
3146
+ endSuppressIframeMutationDirty();
3147
+ hoveredTreeEl = null;
3148
+ }
3149
+ }
3150
+
3151
+ function setTreeHoverHighlight(el) {
3152
+ if (!el || el.nodeType !== 1) {
3153
+ clearTreeHoverHighlight();
3154
+ return;
3155
+ }
3156
+ if (hoveredTreeEl === el) return;
3157
+ clearTreeHoverHighlight();
3158
+ beginSuppressIframeMutationDirty();
3159
+ try {
3160
+ if (el.classList) el.classList.add('vve-tree-hover');
3161
+ hoveredTreeEl = el;
3162
+ } catch(_) {
3163
+ hoveredTreeEl = null;
3164
+ } finally {
3165
+ endSuppressIframeMutationDirty();
3166
+ }
3167
+ }
3168
+
3169
+ function isTreeHoverOnlyClassMutation(mutation) {
3170
+ if (!mutation || mutation.type !== 'attributes' || mutation.attributeName !== 'class') return false;
3171
+ var oldClass = String(mutation.oldValue || '');
3172
+ var target = mutation.target;
3173
+ var nextClass = '';
3174
+ try {
3175
+ nextClass = target && typeof target.className === 'string' ? target.className : '';
3176
+ } catch(_) {}
3177
+ return oldClass.indexOf('vve-tree-hover') >= 0 || String(nextClass).indexOf('vve-tree-hover') >= 0;
3178
+ }
3179
+
3093
3180
  function setDragHandleActive(on) {
3094
3181
  dragHandleActive = !!on;
3095
3182
  var b = document.getElementById('sf-drag');
@@ -3410,6 +3497,9 @@ function renderDomTree(filterRaw) {
3410
3497
  if (e.target.closest && e.target.closest('.dt-chev')) return;
3411
3498
  selectElementFromTree(el);
3412
3499
  };
3500
+ row.onmouseenter = function() {
3501
+ setTreeHoverHighlight(el);
3502
+ };
3413
3503
  root.appendChild(row);
3414
3504
 
3415
3505
  if (!hasKids || collapsed) return;
@@ -3428,6 +3518,9 @@ function renderDomTree(filterRaw) {
3428
3518
  ? '<div class="dt-muted">No elements match your search.</div>'
3429
3519
  : '<div class="dt-muted">No visible elements yet.</div>';
3430
3520
  }
3521
+ root.onmouseleave = function() {
3522
+ clearTreeHoverHighlight();
3523
+ };
3431
3524
  }
3432
3525
 
3433
3526
  // \u2500\u2500 Utility helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -3847,12 +3940,25 @@ function renderRightPanel(el) {
3847
3940
  var sel = buildSelector(el);
3848
3941
  bindings.forEach(function(b){
3849
3942
  var inp = document.getElementById(b[0]);
3850
- if (inp) inp.addEventListener('input', function(){
3943
+ if (inp) {
3944
+ var onValueChange = function() {
3851
3945
  // Read the original value BEFORE applying the change so we can revert later
3852
3946
  var orig = getOriginalValue(b[0], el);
3853
3947
  b[1](inp.value);
3854
- logChange(sel, b[0], inp.value, el, orig);
3855
- });
3948
+ var valueToLog = inp.value;
3949
+ try {
3950
+ console.log('[V2] input changed', {
3951
+ inputId: b[0],
3952
+ rawValue: inp.value,
3953
+ appliedValue: valueToLog,
3954
+ selector: sel,
3955
+ });
3956
+ } catch(_) {}
3957
+ logChange(sel, b[0], valueToLog, el, orig);
3958
+ };
3959
+ inp.addEventListener('input', onValueChange);
3960
+ inp.addEventListener('change', onValueChange);
3961
+ }
3856
3962
  });
3857
3963
  }
3858
3964
 
@@ -4115,6 +4221,32 @@ function attachClickHandler() {
4115
4221
  } catch(_) {}
4116
4222
  }
4117
4223
 
4224
+ function attachIframeHoverHandler() {
4225
+ try {
4226
+ var iframe = document.getElementById('iframeId');
4227
+ var doc = iframe && iframe.contentDocument;
4228
+ if (!doc || !doc.body) return;
4229
+ if (hoverAttachDoc === doc) return;
4230
+ hoverAttachDoc = doc;
4231
+ doc.addEventListener('mousemove', function(e) {
4232
+ if (currentMode !== 'editor') {
4233
+ clearTreeHoverHighlight();
4234
+ return;
4235
+ }
4236
+ var target = e.target;
4237
+ if (!target || target === doc.body || target === doc.documentElement) {
4238
+ clearTreeHoverHighlight();
4239
+ return;
4240
+ }
4241
+ setTreeHoverHighlight(target);
4242
+ }, true);
4243
+ doc.addEventListener('mouseout', function(e) {
4244
+ if (e.relatedTarget) return;
4245
+ clearTreeHoverHighlight();
4246
+ }, true);
4247
+ } catch(_) {}
4248
+ }
4249
+
4118
4250
  function attachChangeObserver() {
4119
4251
  try {
4120
4252
  var iframe = document.getElementById('iframeId');
@@ -4127,6 +4259,14 @@ function attachChangeObserver() {
4127
4259
  changeObserverDoc = null;
4128
4260
  }
4129
4261
  changeObserver = new MutationObserver(function(mutations) {
4262
+ var hasMeaningfulMutation = false;
4263
+ for (var mi = 0; mi < mutations.length; mi++) {
4264
+ if (!isTreeHoverOnlyClassMutation(mutations[mi])) {
4265
+ hasMeaningfulMutation = true;
4266
+ break;
4267
+ }
4268
+ }
4269
+ if (!hasMeaningfulMutation) return;
4130
4270
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
4131
4271
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
4132
4272
  // Keep selection sticky by re-resolving from fingerprint.
@@ -4137,7 +4277,7 @@ function attachChangeObserver() {
4137
4277
  updateSelectionToolbar();
4138
4278
  });
4139
4279
  changeObserver.observe(doc.body, {
4140
- childList: true, subtree: true, attributes: true, characterData: true
4280
+ childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true
4141
4281
  });
4142
4282
  changeObserverDoc = doc;
4143
4283
  } catch(_) {}
@@ -4163,6 +4303,7 @@ function syncIframeInteractions(reason) {
4163
4303
  injectIframeSelectionStyles(doc);
4164
4304
  refreshPersistentChangesetStyleTagForActiveVariation();
4165
4305
  attachClickHandler();
4306
+ attachIframeHoverHandler();
4166
4307
  attachDragReposition();
4167
4308
  attachChangeObserver();
4168
4309
  startConsistencyWatchdog(doc);
@@ -4521,10 +4662,9 @@ window.addEventListener('load', function() {
4521
4662
  var iframe = document.getElementById('iframeId');
4522
4663
  iframe.addEventListener('load', function() {
4523
4664
  if (!iframe.src || iframe.src === 'about:blank' || iframe.src === window.location.href) return;
4524
- // New iframe navigation: always drop bindings tied to the previous document.
4525
- resetIframeBindings();
4526
4665
  var doc = iframe.contentDocument;
4527
4666
  if (!doc) {
4667
+ resetIframeBindings();
4528
4668
  syncIframeInteractions('iframe-load-no-doc');
4529
4669
  return;
4530
4670
  }
@@ -4533,14 +4673,32 @@ window.addEventListener('load', function() {
4533
4673
  // Stale events: src may already be the proxy URL while the document is still
4534
4674
  // about:blank (e.g. src cleared then reset to force reload). Ask sync path to retry.
4535
4675
  if (docUrl === 'about:blank') {
4676
+ resetIframeBindings();
4536
4677
  syncIframeInteractions('iframe-load-about-blank');
4537
4678
  return;
4538
4679
  }
4680
+ // If early-paint and final load DOM signatures match, avoid a second full apply/reset
4681
+ // that steals focus from sidebar controls while the page is still stabilizing.
4682
+ var shouldRefreshOnFinalLoad = true;
4683
+ if (
4684
+ iframeEarlyDomSignatureNavGen === iframeContentNavGen &&
4685
+ iframeEarlySyncPrimedForGen === iframeContentNavGen &&
4686
+ iframeEarlyDomSignature
4687
+ ) {
4688
+ var finalDomSignature = computeIframeDomSignature(doc);
4689
+ if (finalDomSignature && finalDomSignature === iframeEarlyDomSignature) {
4690
+ shouldRefreshOnFinalLoad = false;
4691
+ }
4692
+ }
4693
+ if (clickAttachDoc !== doc || dragAttachDoc !== doc || changeObserverDoc !== doc || hoverAttachDoc !== doc) {
4694
+ resetIframeBindings();
4695
+ }
4539
4696
  attachIframeLoadingUntilComplete(iframe);
4540
4697
  if (doc.body && iframeDocMatchesNavigatedSrc(iframe, doc)) {
4541
4698
  stopIframeContentApplyWatcher();
4542
- deselectElement();
4543
- applyActiveVariationHtml();
4699
+ if (shouldRefreshOnFinalLoad) {
4700
+ applyActiveVariationHtml();
4701
+ }
4544
4702
  }
4545
4703
  // Always attempt sync; it has its own readiness checks + retry loop.
4546
4704
  syncIframeInteractions('iframe-load');
@@ -4597,6 +4755,7 @@ var getDefaultAnthropicApiKey = () => {
4597
4755
  function createVisualEditorMiddleware(options) {
4598
4756
  const anthropicApiKey = options?.anthropicApiKey || getDefaultAnthropicApiKey();
4599
4757
  const enableGenerateTestApi = options?.enableGenerateTestApi ?? true;
4758
+ const strictObserverFreeze = options?.strictObserverFreeze === true;
4600
4759
  const allowedFrameOrigins = options?.allowedFrameOrigins ?? ["*"];
4601
4760
  function setFrameHeaders(req, res) {
4602
4761
  res.removeHeader("X-Frame-Options");
@@ -4764,6 +4923,8 @@ function createVisualEditorMiddleware(options) {
4764
4923
  const url = new URL(req.url || "", "http://localhost");
4765
4924
  const targetUrl = url.searchParams.get("url");
4766
4925
  const password = url.searchParams.get("password") || "";
4926
+ const strictFreezeParam = (url.searchParams.get("strictObserverFreeze") || "").toLowerCase();
4927
+ const strictObserverFreezeForRequest = strictFreezeParam === "1" || strictFreezeParam === "true" || strictFreezeParam === "yes" ? true : strictFreezeParam === "0" || strictFreezeParam === "false" || strictFreezeParam === "no" ? false : strictObserverFreeze;
4767
4928
  if (!targetUrl) {
4768
4929
  res.statusCode = 400;
4769
4930
  res.end(JSON.stringify({ error: "Missing url parameter" }));
@@ -4882,9 +5043,12 @@ function createVisualEditorMiddleware(options) {
4882
5043
  }
4883
5044
  );
4884
5045
  if (html.includes("</head>")) {
4885
- html = html.replace("</head>", `${popupHideCss}
4886
- ${consentAllowCss}
4887
- </head>`);
5046
+ html = html.replace(
5047
+ "</head>",
5048
+ `${iframeAlwaysShowCss}
5049
+ ${iframeAlwaysShowCssGuardScript}
5050
+ </head>`
5051
+ );
4888
5052
  }
4889
5053
  html = html.replace(
4890
5054
  /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
@@ -4898,6 +5062,7 @@ ${consentAllowCss}
4898
5062
  var TARGET_ORIGIN=${JSON.stringify(origin)};
4899
5063
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4900
5064
  var PROXY_PASSWORD=${JSON.stringify(password)};
5065
+ var STRICT_OBSERVER_FREEZE=${JSON.stringify(strictObserverFreezeForRequest)};
4901
5066
  window.__CONVERSION_EDITOR_ACTIVE__=true;
4902
5067
  function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4903
5068
  function toAbsolute(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")||raw.startsWith("//")?TARGET_ORIGIN:TARGET_PAGE_URL;return new URL(raw,base).toString();}catch(_){return raw;}}
@@ -4917,6 +5082,9 @@ try{
4917
5082
  var wrapped=function(list,obs){
4918
5083
  try{
4919
5084
  if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
5085
+ if(STRICT_OBSERVER_FREEZE){
5086
+ return;
5087
+ }
4920
5088
  var now=Date.now();
4921
5089
  if(now-last<120)return;
4922
5090
  last=now;
@@ -4926,6 +5094,11 @@ try{
4926
5094
  return new NativeMO(wrapped);
4927
5095
  };
4928
5096
  window.MutationObserver.prototype=NativeMO.prototype;
5097
+ try{
5098
+ if(STRICT_OBSERVER_FREEZE){
5099
+ console.info("[conversion-proxy] strict MutationObserver freeze active");
5100
+ }
5101
+ }catch(_){}
4929
5102
  }
4930
5103
  }catch(_){}
4931
5104
  }catch(_){}})();</script>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "dev": "tsup --watch",
26
26
  "clean": "rm -rf dist",
27
27
  "build": "tsup",
28
+ "local:build": "tsup && yarn --cwd ../codebase-server clean && yarn --cwd ../codebase-server dev",
28
29
  "watch": "tsup --watch"
29
30
  },
30
31
  "peerDependencies": {