@accelerated-agency/visual-editor 0.3.1 → 0.3.3

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/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() {
@@ -148,6 +160,17 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
148
160
  .tb-save-txt{font-size:14px;color:#00C951;white-space:nowrap}
149
161
  .tb-save-txt::before{content:'Saved'}
150
162
  #dirty-dot.on~.tb-save-txt::before{content:'Unsaved'}
163
+
164
+ /* \u2500\u2500 In-editor toast notification \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 */
165
+ #ve-notification{
166
+ position:fixed;right:16px;top:64px;z-index:13000;max-width:360px;
167
+ padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.35;
168
+ border:1px solid var(--border);background:#fff;color:var(--text);
169
+ box-shadow:0 10px 24px rgba(2,6,23,.18);display:none
170
+ }
171
+ #ve-notification.show{display:block}
172
+ #ve-notification.error{border-color:#fecaca;background:#fef2f2;color:#991b1b}
173
+ #ve-notification.success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}
151
174
  .tb-save-time{font-size:12px;color:#52525b;white-space:nowrap}
152
175
  #dirty-dot.on~.tb-save-time{display:none}
153
176
  /* Simulate + Finalize buttons */
@@ -243,7 +266,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
243
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 */
244
267
  .dt-tree{font-size:11px;padding:0px 0 0px 20px;user-select:none}
245
268
  .dt-row{
246
- 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;
247
270
  cursor:pointer;color:var(--text-2);border-radius:4px;margin:0 4px
248
271
  }
249
272
  .dt-row:hover{background:var(--bg-hover);color:var(--text)}
@@ -455,6 +478,49 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
455
478
  /* \u2500\u2500 Subgroup label \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\u2500 */
456
479
  .sub-lbl{font-size:10px;text-transform:uppercase;color:var(--text-3);font-weight:700;letter-spacing:.05em;margin:8px 0 5px}
457
480
  .sub-lbl:first-child{margin-top:0}
481
+ .sub-lbl-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:8px 0 5px}
482
+ .sub-lbl-row .sub-lbl{margin:0}
483
+ .css-expand-btn{
484
+ width:22px;height:22px;border:1px solid var(--border);border-radius:5px;background:#fff;
485
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text-3);
486
+ transition:all .15s
487
+ }
488
+ .css-expand-btn:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
489
+
490
+ /* \u2500\u2500 Custom CSS modal \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 */
491
+ #custom-css-modal{
492
+ position:fixed;inset:0;z-index:12000;background:rgba(15,23,42,.5);
493
+ display:none;align-items:center;justify-content:center;padding:20px
494
+ }
495
+ #custom-css-modal.open{display:flex}
496
+ .custom-css-dialog{
497
+ width:min(860px,96vw);max-height:86vh;background:#fff;border:1px solid var(--border);
498
+ border-radius:10px;box-shadow:0 20px 40px rgba(2,6,23,.35);display:flex;flex-direction:column;overflow:hidden
499
+ }
500
+ .custom-css-head{
501
+ display:flex;align-items:center;justify-content:space-between;gap:10px;
502
+ padding:10px 12px;border-bottom:1px solid var(--border);background:#fff
503
+ }
504
+ .custom-css-title{font-size:12px;font-weight:700;color:var(--text)}
505
+ .custom-css-close{
506
+ border:none;background:transparent;color:var(--text-3);cursor:pointer;width:24px;height:24px;border-radius:5px
507
+ }
508
+ .custom-css-close:hover{background:var(--bg-hover);color:var(--text)}
509
+ #custom-css-modal-textarea{
510
+ width:100%;min-height:360px;max-height:58vh;resize:vertical;border:none;outline:none;
511
+ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
512
+ font-size:12px;line-height:1.5;padding:12px;background:#0b1220;color:#e2e8f0
513
+ }
514
+ .custom-css-actions{
515
+ display:flex;justify-content:flex-end;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:#fff
516
+ }
517
+ .custom-css-btn{
518
+ border:1px solid var(--border);background:#fff;color:var(--text-2);border-radius:6px;padding:6px 10px;cursor:pointer;
519
+ font-size:12px;font-weight:600;font-family:inherit
520
+ }
521
+ .custom-css-btn:hover{background:var(--bg-hover)}
522
+ .custom-css-btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
523
+ .custom-css-btn.primary:hover{filter:brightness(.97)}
458
524
 
459
525
  /* \u2500\u2500 Shadow presets \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\u2500 */
460
526
  .shadow-presets{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
@@ -626,10 +692,10 @@ select.pr-inp{cursor:pointer;background:#fff}
626
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>
627
693
  <button class="tb-dk-btn" title="Comments"><i class="bi bi-chat-dots"></i></button>
628
694
  </div>
629
- <!-- btn-close: hidden visually, kept for JS event listener -->
630
- <button id="btn-close" style="display:none" title="Close editor"></button>
631
695
  <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
632
- <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>
633
699
  </div>
634
700
 
635
701
  <!-- url-bar: hidden, kept for JS compatibility -->
@@ -844,6 +910,23 @@ select.pr-inp{cursor:pointer;background:#fff}
844
910
 
845
911
  </div><!-- #app -->
846
912
 
913
+ <!-- Custom CSS full-screen modal -->
914
+ <div id="custom-css-modal" aria-hidden="true">
915
+ <div class="custom-css-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-css-modal-title">
916
+ <div class="custom-css-head">
917
+ <div class="custom-css-title" id="custom-css-modal-title"><i class="bi bi-code-slash"></i> Custom CSS</div>
918
+ <button type="button" class="custom-css-close" onclick="closeCustomCssModal()" title="Close">
919
+ <i class="bi bi-x-lg"></i>
920
+ </button>
921
+ </div>
922
+ <textarea id="custom-css-modal-textarea" spellcheck="false" placeholder="Type your css here"></textarea>
923
+ <div class="custom-css-actions">
924
+ <button type="button" class="custom-css-btn" onclick="closeCustomCssModal()">Cancel</button>
925
+ <button type="button" class="custom-css-btn primary" onclick="applyCustomCssModal()">Add</button>
926
+ </div>
927
+ </div>
928
+ </div>
929
+
847
930
  <!-- CDN scripts -->
848
931
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
849
932
  <script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/builder.js"></script>
@@ -915,13 +998,37 @@ function send(type, payload) {
915
998
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
916
999
  }
917
1000
 
1001
+ var notificationTimer = null;
1002
+ function showEditorNotification(message, kind, durationMs) {
1003
+ var id = 've-notification';
1004
+ var el = document.getElementById(id);
1005
+ if (!el) {
1006
+ el = document.createElement('div');
1007
+ el.id = id;
1008
+ el.setAttribute('role', 'status');
1009
+ el.setAttribute('aria-live', 'polite');
1010
+ document.body.appendChild(el);
1011
+ }
1012
+ el.className = '';
1013
+ el.classList.add(kind === 'success' ? 'success' : 'error');
1014
+ el.classList.add('show');
1015
+ el.textContent = String(message || 'Something went wrong');
1016
+ if (notificationTimer) clearTimeout(notificationTimer);
1017
+ notificationTimer = setTimeout(function() {
1018
+ var cur = document.getElementById(id);
1019
+ if (!cur) return;
1020
+ cur.classList.remove('show');
1021
+ }, Math.max(1200, durationMs || 2600));
1022
+ }
1023
+
918
1024
  function generatePreviewUrlString(args) {
919
1025
  var baseUrl = (args && args.url) || '';
920
1026
  var test = (args && args.test) || {};
921
1027
  var variation = (args && args.variation) || {};
922
1028
  if (!baseUrl) return '';
923
- var testId = test.iid || test.experimentId || test._id || '';
924
- var variationId = variation.iid || variation._id || '';
1029
+ var testId = test.iid || '';
1030
+ var variationId = variation.iid || '';
1031
+ if (!testId || !variationId) return '';
925
1032
  var cId = String(testId || '') + '_' + String(variationId || '');
926
1033
  var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
927
1034
  return (
@@ -945,6 +1052,10 @@ function simulateExperiment() {
945
1052
  test.pageUrl ||
946
1053
  (test.metadata_1 && test.metadata_1.editor_url) ||
947
1054
  '';
1055
+ if (!test.iid || !activeVariation || !activeVariation.iid) {
1056
+ showEditorNotification('Cannot simulate: missing test.iid or variation.iid.', 'error', 3200);
1057
+ return;
1058
+ }
948
1059
  var url = generatePreviewUrlString({
949
1060
  url: targetUrl,
950
1061
  test: test,
@@ -952,6 +1063,7 @@ function simulateExperiment() {
952
1063
  });
953
1064
  if (!url) {
954
1065
  console.warn('[V2] simulateExperiment: missing target URL');
1066
+ showEditorNotification('Cannot simulate: missing target URL.', 'error', 3200);
955
1067
  return;
956
1068
  }
957
1069
  try {
@@ -969,6 +1081,19 @@ window.addEventListener('message', function(e) {
969
1081
  }
970
1082
  });
971
1083
 
1084
+ document.addEventListener('keydown', function(e) {
1085
+ if (e.key !== 'Escape') return;
1086
+ var modal = document.getElementById('custom-css-modal');
1087
+ if (!modal || !modal.classList.contains('open')) return;
1088
+ closeCustomCssModal();
1089
+ });
1090
+
1091
+ document.addEventListener('click', function(e) {
1092
+ var modal = document.getElementById('custom-css-modal');
1093
+ if (!modal || !modal.classList.contains('open')) return;
1094
+ if (e.target === modal) closeCustomCssModal();
1095
+ });
1096
+
972
1097
  // \u2500\u2500 State \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
973
1098
  var experimentData = null;
974
1099
  var variations = [];
@@ -989,6 +1114,8 @@ var iframeContentNavGen = 0;
989
1114
  var iframeContentApplyTimer = null;
990
1115
  var iframeEarlyGranularPrimedForGen = null;
991
1116
  var iframeEarlySyncPrimedForGen = null;
1117
+ var iframeEarlyDomSignature = '';
1118
+ var iframeEarlyDomSignatureNavGen = 0;
992
1119
  /** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
993
1120
  var appliedStructuralChangesetKeys = {};
994
1121
  var isDirty = false;
@@ -1000,6 +1127,7 @@ var selectedEl = null;
1000
1127
  var selectedElFingerprint = '';
1001
1128
  var selectedElRecoverMisses = 0;
1002
1129
  var MAX_SELECTED_RECOVER_MISSES = 12;
1130
+ var hoveredTreeEl = null;
1003
1131
  var isDeselectingSelection = false;
1004
1132
  var suppressClickUntil = 0;
1005
1133
  var dragAttachDoc = null;
@@ -1013,6 +1141,7 @@ var iframeSyncAttempts = 0;
1013
1141
  var selectionScrollWin = null;
1014
1142
  var selectionResizeBound = false;
1015
1143
  var clickAttachDoc = null;
1144
+ var hoverAttachDoc = null;
1016
1145
  var changeObserver = null;
1017
1146
  var changeObserverDoc = null;
1018
1147
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
@@ -1048,14 +1177,17 @@ function endSuppressIframeMutationDirty() {
1048
1177
  function commitStateChangesForActiveVariation() {
1049
1178
  if (!activeVarId) return;
1050
1179
  stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1180
+ refreshPersistentChangesetStyleTagForActiveVariation();
1051
1181
  }
1052
1182
 
1053
1183
  function loadStateChangesForActiveVariation() {
1054
1184
  if (!activeVarId) {
1055
1185
  stateChanges = [];
1186
+ refreshPersistentChangesetStyleTagForActiveVariation();
1056
1187
  return;
1057
1188
  }
1058
1189
  stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1190
+ refreshPersistentChangesetStyleTagForActiveVariation();
1059
1191
  }
1060
1192
 
1061
1193
  function recoverSelectedElement(forceDeselectOnMiss) {
@@ -1222,6 +1354,7 @@ function setMode(mode) {
1222
1354
  document.getElementById('btn-mode-editor').classList.toggle('active', mode === 'editor');
1223
1355
  document.getElementById('btn-mode-nav').classList.toggle('active', mode === 'navigate');
1224
1356
  if (mode === 'navigate') {
1357
+ clearTreeHoverHighlight();
1225
1358
  setDragHandleActive(false);
1226
1359
  deselectElement();
1227
1360
  } else if (mode === 'editor') {
@@ -1535,24 +1668,26 @@ function persistActiveVariationChangesets(arr) {
1535
1668
  }
1536
1669
  }
1537
1670
  }
1671
+ refreshPersistentChangesetStyleTagForActiveVariation();
1538
1672
  }
1539
1673
 
1540
1674
  function entrySnapshotKey(entry) {
1541
1675
  if (!entry || !entry.selector) return '';
1676
+ var SEP = '__vve_sep__';
1542
1677
  var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
1543
1678
  return (
1544
1679
  selKey +
1545
- '\0' +
1680
+ SEP +
1546
1681
  normalizeChangesetType(entry) +
1547
- '\0' +
1682
+ SEP +
1548
1683
  String(entry.property || '') +
1549
- '\0' +
1684
+ SEP +
1550
1685
  String(entry.attribute || '') +
1551
- '\0' +
1686
+ SEP +
1552
1687
  String(entry.action || '') +
1553
- '\0' +
1688
+ SEP +
1554
1689
  String(entry.html != null ? 'h' + String(entry.html).length : '') +
1555
- '\0' +
1690
+ SEP +
1556
1691
  String(entry.value != null ? 'v:' + entry.value : '')
1557
1692
  );
1558
1693
  }
@@ -1602,15 +1737,15 @@ function softReloadEditorIframe() {
1602
1737
  var navGen = nextIframeContentNavGen();
1603
1738
  resetIframeBindings();
1604
1739
  setIframePageLoadingUi(true);
1605
- iframe.src = '';
1606
1740
  iframe.src = appendIframeReloadBust(src);
1607
- startIframeContentApplyWatcher(navGen);
1741
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
1608
1742
  scheduleDomTreeRefresh();
1609
1743
  }
1610
1744
 
1611
1745
  /** @returns {boolean} true if a full iframe reload was started */
1612
1746
  function revertChangesetEntryOnDom(entry) {
1613
1747
  if (!entry) return false;
1748
+ var styleOnly = isStyleOnlyChangesetEntry(entry);
1614
1749
  if (entry.selector === '__vvveb_body__') {
1615
1750
  var iframeDoc0 = document.getElementById('iframeId').contentDocument;
1616
1751
  if (!iframeDoc0 || !iframeDoc0.body) return false;
@@ -1631,6 +1766,11 @@ function revertChangesetEntryOnDom(entry) {
1631
1766
  var snap = appliedChangesetSnapshots[k];
1632
1767
  var el = querySelectorResolved(iframeDoc, entry.selector);
1633
1768
  if (!snap || !el) {
1769
+ if (styleOnly) {
1770
+ refreshPersistentChangesetStyleTagForActiveVariation();
1771
+ delete appliedChangesetSnapshots[k];
1772
+ return false;
1773
+ }
1634
1774
  softReloadEditorIframe();
1635
1775
  delete appliedChangesetSnapshots[k];
1636
1776
  return true;
@@ -1645,6 +1785,11 @@ function revertChangesetEntryOnDom(entry) {
1645
1785
  else el.setAttribute(snap.name, snap.v);
1646
1786
  } else if (snap.kind === 'display') el.style.display = snap.v;
1647
1787
  else {
1788
+ if (styleOnly) {
1789
+ refreshPersistentChangesetStyleTagForActiveVariation();
1790
+ delete appliedChangesetSnapshots[k];
1791
+ return false;
1792
+ }
1648
1793
  softReloadEditorIframe();
1649
1794
  delete appliedChangesetSnapshots[k];
1650
1795
  return true;
@@ -1704,7 +1849,9 @@ function renderHistoryTab() {
1704
1849
  var lab = historyEntryTypeLabel(item.entry);
1705
1850
  var val = historyEntryValuePreview(item.entry);
1706
1851
  html +=
1707
- '<div class="state-item">' +
1852
+ '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1853
+ item.idx +
1854
+ ')">' +
1708
1855
  '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1709
1856
  item.idx +
1710
1857
  '</span>' +
@@ -1720,7 +1867,7 @@ function renderHistoryTab() {
1720
1867
  item.idx +
1721
1868
  ')" onclick="removeHistoryChangeset(' +
1722
1869
  item.idx +
1723
- ')">&#x2715;</button>' +
1870
+ ', event)">&#x2715;</button>' +
1724
1871
  '</div>';
1725
1872
  });
1726
1873
  html += '</div>';
@@ -1738,7 +1885,38 @@ function changesetListHasStructural(arr) {
1738
1885
  return false;
1739
1886
  }
1740
1887
 
1741
- function removeHistoryChangeset(idx) {
1888
+ function isStyleOnlyChangesetEntry(entry) {
1889
+ if (!entry) return false;
1890
+ var t = normalizeChangesetType(entry);
1891
+ if (t === 'style') return true;
1892
+ if (t === 'attribute' && String(entry.attribute || '').toLowerCase() === 'style') return true;
1893
+ return false;
1894
+ }
1895
+
1896
+ function focusHistoryChangeset(idx) {
1897
+ var v = getActiveVariationForHistory();
1898
+ if (!v) return;
1899
+ var arr = parseVariationChangesets(v);
1900
+ if (idx < 0 || idx >= arr.length) return;
1901
+ var entry = arr[idx];
1902
+ if (!entry || !entry.selector || entry.selector === '__vvveb_body__') return;
1903
+ try {
1904
+ var iframe = document.getElementById('iframeId');
1905
+ var iframeDoc = iframe && iframe.contentDocument;
1906
+ if (!iframeDoc) return;
1907
+ var el = querySelectorResolved(iframeDoc, entry.selector);
1908
+ if (!el) return;
1909
+ selectElement(el);
1910
+ try {
1911
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
1912
+ } catch(_) {
1913
+ el.scrollIntoView();
1914
+ }
1915
+ } catch(_) {}
1916
+ }
1917
+
1918
+ function removeHistoryChangeset(idx, evt) {
1919
+ if (evt && evt.stopPropagation) evt.stopPropagation();
1742
1920
  var v = getActiveVariationForHistory();
1743
1921
  if (!v) return;
1744
1922
  var arr = parseVariationChangesets(v);
@@ -1746,6 +1924,16 @@ function removeHistoryChangeset(idx) {
1746
1924
  var removed = arr[idx];
1747
1925
  arr.splice(idx, 1);
1748
1926
  persistActiveVariationChangesets(arr);
1927
+ if (isStyleOnlyChangesetEntry(removed)) {
1928
+ try {
1929
+ refreshPersistentChangesetStyleTagForActiveVariation();
1930
+ saveCurrentVariationHtml();
1931
+ } catch(_) {}
1932
+ if (currentMainTab === 'history') renderHistoryTab();
1933
+ recomputeEditorDirty();
1934
+ scheduleDomTreeRefresh();
1935
+ return;
1936
+ }
1749
1937
  var didReload = revertChangesetEntryOnDom(removed);
1750
1938
  try {
1751
1939
  delete varHtmlCache[activeVarId];
@@ -1753,15 +1941,19 @@ function removeHistoryChangeset(idx) {
1753
1941
  // Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
1754
1942
  // structural row remains or was removed (revert may already have started a reload for insert/body).
1755
1943
  var removedType = normalizeChangesetType(removed);
1756
- var needsStructuralReload =
1757
- !didReload &&
1758
- (removedType === 'insert' ||
1759
- removedType === 'reorder' ||
1760
- changesetListHasStructural(arr));
1944
+ var hasStructuralRemaining = changesetListHasStructural(arr);
1945
+ var removedIsStructural = removedType === 'insert' || removedType === 'reorder';
1761
1946
  if (didReload) {
1762
1947
  /* revertChangesetEntryOnDom already kicked off iframe reload */
1763
- } else if (needsStructuralReload) {
1948
+ } else if (removedIsStructural) {
1764
1949
  softReloadEditorIframe();
1950
+ } else if (hasStructuralRemaining) {
1951
+ // Keep current DOM state (already reverted for removed row) and only refresh style layer.
1952
+ // Avoid full reload and avoid re-applying all rows, which can duplicate structural insert/reorder entries.
1953
+ try {
1954
+ refreshPersistentChangesetStyleTagForActiveVariation();
1955
+ saveCurrentVariationHtml();
1956
+ } catch(_) {}
1765
1957
  } else {
1766
1958
  try {
1767
1959
  appliedStructuralChangesetKeys = {};
@@ -2025,6 +2217,27 @@ function bodyHasFirstPaintChild(body) {
2025
2217
  return false;
2026
2218
  }
2027
2219
 
2220
+ function computeIframeDomSignature(doc) {
2221
+ if (!doc || !doc.body) return '';
2222
+ var body = doc.body;
2223
+ var tags = [];
2224
+ var walker = null;
2225
+ try {
2226
+ walker = doc.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, null);
2227
+ } catch(_) {
2228
+ walker = null;
2229
+ }
2230
+ var count = 0;
2231
+ if (walker) {
2232
+ while (walker.nextNode() && count < 400) {
2233
+ var node = walker.currentNode;
2234
+ tags.push((node && node.tagName) ? String(node.tagName).toLowerCase() : '');
2235
+ count += 1;
2236
+ }
2237
+ }
2238
+ return String(body.children ? body.children.length : 0) + '|' + String(count) + '|' + tags.join(',');
2239
+ }
2240
+
2028
2241
  /** True when at least one granular changeset selector already matches (nested content painted). */
2029
2242
  function granularAnySelectorMatches(doc, cs) {
2030
2243
  if (!doc || !cs || !cs.length) return false;
@@ -2129,7 +2342,7 @@ function runConsistencyReconcile() {
2129
2342
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2130
2343
  var granular = filterGranularChangesetEntries(cs);
2131
2344
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
2132
- if (unresolved > 0 || changesetListHasStructural(cs)) {
2345
+ if (unresolved > 0 || hasUnappliedStructuralChangesets(cs)) {
2133
2346
  reapplyActiveVariationGranular(doc);
2134
2347
  registerPendingGranularChangesets(cs, doc);
2135
2348
  }
@@ -2179,6 +2392,7 @@ function resetIframeBindings() {
2179
2392
  appliedStructuralChangesetKeys = {};
2180
2393
  clickAttachDoc = null;
2181
2394
  dragAttachDoc = null;
2395
+ hoverAttachDoc = null;
2182
2396
  changeObserverDoc = null;
2183
2397
  clearPendingGranularChangesets();
2184
2398
  if (changeObserver) {
@@ -2207,7 +2421,7 @@ function loadPage(proxyUrl) {
2207
2421
  iframe.style.display = 'block';
2208
2422
  setIframePageLoadingUi(true);
2209
2423
  iframe.src = appendIframeReloadBust(proxyUrl);
2210
- startIframeContentApplyWatcher(navGen);
2424
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2211
2425
  scheduleDomTreeRefresh();
2212
2426
  }
2213
2427
 
@@ -2279,7 +2493,7 @@ function switchVariation(varId) {
2279
2493
  iframe.src = appendIframeReloadBust(src);
2280
2494
  // Do not sync here: the document is still the previous navigation until the
2281
2495
  // iframe load event; an eager sync attached observers / DOM tree to the wrong document.
2282
- startIframeContentApplyWatcher(navGen);
2496
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2283
2497
  scheduleDomTreeRefresh();
2284
2498
  }
2285
2499
  } catch(_) {}
@@ -2499,24 +2713,136 @@ function flushPendingGranularChangesets() {
2499
2713
  function structuralChangesetDedupKey(entry) {
2500
2714
  var nt = normalizeChangesetType(entry);
2501
2715
  if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
2716
+ var SEP = '__vve_sep__';
2502
2717
  var vid = activeVarId || '';
2503
2718
  try {
2504
2719
  return (
2505
2720
  vid +
2506
- '\0' +
2721
+ SEP +
2507
2722
  nt +
2508
- '\0' +
2723
+ SEP +
2509
2724
  entry.selector +
2510
- '\0' +
2725
+ SEP +
2511
2726
  String(entry.action || '') +
2512
- '\0' +
2727
+ SEP +
2513
2728
  String(entry.html != null ? entry.html : '').slice(0, 240) +
2514
- '\0' +
2729
+ SEP +
2515
2730
  String(entry.targetSelector || '')
2516
2731
  );
2517
2732
  } catch(_) {
2518
- return vid + '\0' + nt + '\0' + entry.selector;
2733
+ return vid + SEP + nt + SEP + entry.selector;
2734
+ }
2735
+ }
2736
+
2737
+ function hasUnappliedStructuralChangesets(cs) {
2738
+ if (!cs || !cs.length) return false;
2739
+ for (var i = 0; i < cs.length; i++) {
2740
+ var e = cs[i];
2741
+ var t = normalizeChangesetType(e);
2742
+ if (t !== 'insert' && t !== 'reorder') continue;
2743
+ var k = structuralChangesetDedupKey(e);
2744
+ if (!k) return true;
2745
+ if (!appliedStructuralChangesetKeys[k]) return true;
2746
+ }
2747
+ return false;
2748
+ }
2749
+
2750
+ function parseInlineStyleDeclarations(styleText) {
2751
+ var out = [];
2752
+ if (styleText == null) return out;
2753
+ var s = String(styleText);
2754
+ if (!s.trim()) return out;
2755
+ var parts = s.split(';');
2756
+ for (var i = 0; i < parts.length; i++) {
2757
+ var seg = parts[i];
2758
+ if (!seg) continue;
2759
+ var idx = seg.indexOf(':');
2760
+ if (idx <= 0) continue;
2761
+ var prop = seg.slice(0, idx).trim();
2762
+ var value = seg.slice(idx + 1).trim();
2763
+ if (!prop || !value) continue;
2764
+ out.push({ prop: prop, value: value });
2765
+ }
2766
+ return out;
2767
+ }
2768
+
2769
+ function buildPersistentStyleRulesForActiveVariation() {
2770
+ if (!activeVarId) return '';
2771
+ var v = variations.find(function(x) { return x && x._id === activeVarId; });
2772
+ var parsed = parseVariationChangesets(v);
2773
+ var map = {};
2774
+ var order = [];
2775
+ function put(selector, prop, value) {
2776
+ if (!selector || !prop) return;
2777
+ if (value == null || value === '') return;
2778
+ var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
2779
+ var pr = String(prop).trim();
2780
+ var val = String(value).trim();
2781
+ if (!sel || !pr || !val) return;
2782
+ var k = sel + '__vve_sep__' + pr;
2783
+ if (!map[k]) order.push(k);
2784
+ map[k] = { selector: sel, property: pr, value: val };
2785
+ }
2786
+ for (var i = 0; i < parsed.length; i++) {
2787
+ var e = parsed[i];
2788
+ if (!e) continue;
2789
+ var t = normalizeChangesetType(e);
2790
+ if (t === 'style') {
2791
+ put(e.selector, e.property || e.cssProp, e.value);
2792
+ continue;
2793
+ }
2794
+ if (t === 'attribute' && String(e.attribute || '').toLowerCase() === 'style') {
2795
+ var decls = parseInlineStyleDeclarations(e.value);
2796
+ for (var di = 0; di < decls.length; di++) {
2797
+ put(e.selector, decls[di].prop, decls[di].value);
2798
+ }
2799
+ }
2519
2800
  }
2801
+ for (var j = 0; j < stateChanges.length; j++) {
2802
+ var c = stateChanges[j];
2803
+ if (!c) continue;
2804
+ if (c.cssProp) {
2805
+ put(c.selector, c.cssProp, c.value);
2806
+ continue;
2807
+ }
2808
+ if (c.inputId === 'pp-css') {
2809
+ var liveDecls = parseInlineStyleDeclarations(c.value);
2810
+ for (var ldi = 0; ldi < liveDecls.length; ldi++) {
2811
+ put(c.selector, liveDecls[ldi].prop, liveDecls[ldi].value);
2812
+ }
2813
+ }
2814
+ }
2815
+ var lines = [];
2816
+ for (var oi = 0; oi < order.length; oi++) {
2817
+ var row = map[order[oi]];
2818
+ lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
2819
+ }
2820
+ return lines.join('\\n');
2821
+ }
2822
+
2823
+ function upsertPersistentChangesetStyleTag(iframeDoc, rulesText) {
2824
+ if (!iframeDoc) return;
2825
+ var STYLE_ID = '__vve_persist_changesets_style__';
2826
+ var prev = iframeDoc.getElementById(STYLE_ID);
2827
+ if (!rulesText) {
2828
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
2829
+ return;
2830
+ }
2831
+ var head = iframeDoc.head || iframeDoc.getElementsByTagName('head')[0];
2832
+ if (!head) return;
2833
+ var styleEl = prev || iframeDoc.createElement('style');
2834
+ styleEl.id = STYLE_ID;
2835
+ if (styleEl.textContent !== rulesText) styleEl.textContent = rulesText;
2836
+ if (!styleEl.parentNode) head.appendChild(styleEl);
2837
+ }
2838
+
2839
+ function refreshPersistentChangesetStyleTagForActiveVariation() {
2840
+ try {
2841
+ var iframe = document.getElementById('iframeId');
2842
+ var iframeDoc = iframe && iframe.contentDocument;
2843
+ if (!iframeDoc) return;
2844
+ upsertPersistentChangesetStyleTag(iframeDoc, buildPersistentStyleRulesForActiveVariation());
2845
+ } catch(_) {}
2520
2846
  }
2521
2847
 
2522
2848
  /**
@@ -2553,23 +2879,11 @@ function applyChangesetEntry(entry, iframeDoc) {
2553
2879
  else if (entry.value != null) el.textContent = entry.value;
2554
2880
  break;
2555
2881
  case 'style':
2556
- if (entry.property) {
2557
- var propKebab = entry.property;
2558
- var cam = camelize(propKebab);
2559
- if (entry.value == null || entry.value === '') {
2560
- try { el.style.removeProperty(propKebab); } catch(_) {}
2561
- try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
2562
- } else {
2563
- try {
2564
- el.style.setProperty(propKebab, entry.value, 'important');
2565
- } catch(_) {
2566
- el.style[cam] = entry.value;
2567
- }
2568
- }
2569
- }
2882
+ // Style changes are applied via persistent stylesheet injection.
2570
2883
  break;
2571
2884
  case 'attribute':
2572
2885
  if (entry.attribute && entry.value != null) {
2886
+ if (String(entry.attribute).toLowerCase() === 'style') break;
2573
2887
  el.setAttribute(entry.attribute, entry.value);
2574
2888
  }
2575
2889
  break;
@@ -2606,6 +2920,7 @@ function applyActiveVariationHtml() {
2606
2920
 
2607
2921
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2608
2922
  var cs = parseVariationChangesets(variation);
2923
+ refreshPersistentChangesetStyleTagForActiveVariation();
2609
2924
 
2610
2925
  beginSuppressIframeMutationDirty();
2611
2926
  try {
@@ -2620,6 +2935,7 @@ function applyActiveVariationHtml() {
2620
2935
  for (var i = 0; i < cs.length; i++) {
2621
2936
  applyChangesetEntry(cs[i], iframeDoc);
2622
2937
  }
2938
+ refreshPersistentChangesetStyleTagForActiveVariation();
2623
2939
  // Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
2624
2940
  registerPendingGranularChangesets(cs, iframeDoc);
2625
2941
  } finally {
@@ -2668,6 +2984,7 @@ function applyVariationGranularOnly(iframeDoc) {
2668
2984
  for (var i = 0; i < cs.length; i++) {
2669
2985
  applyChangesetEntry(cs[i], iframeDoc);
2670
2986
  }
2987
+ refreshPersistentChangesetStyleTagForActiveVariation();
2671
2988
  registerPendingGranularChangesets(cs, iframeDoc);
2672
2989
  } finally {
2673
2990
  endSuppressIframeMutationDirty();
@@ -2686,16 +3003,19 @@ function reapplyActiveVariationGranular(iframeDoc) {
2686
3003
  for (var i = 0; i < cs.length; i++) {
2687
3004
  applyChangesetEntry(cs[i], iframeDoc);
2688
3005
  }
3006
+ refreshPersistentChangesetStyleTagForActiveVariation();
2689
3007
  } finally {
2690
3008
  endSuppressIframeMutationDirty();
2691
3009
  }
2692
3010
  }
2693
3011
 
2694
3012
  /** Poll iframe document during navigation; apply granular edits as soon as DOM can match selectors. */
2695
- function startIframeContentApplyWatcher(navGen) {
3013
+ function startIframeContentApplyWatcher(navGen, prevDocRef) {
2696
3014
  stopIframeContentApplyWatcher();
2697
3015
  iframeEarlyGranularPrimedForGen = null;
2698
3016
  iframeEarlySyncPrimedForGen = null;
3017
+ iframeEarlyDomSignature = '';
3018
+ iframeEarlyDomSignatureNavGen = navGen;
2699
3019
  var iframe = document.getElementById('iframeId');
2700
3020
  iframeContentApplyTimer = setInterval(function() {
2701
3021
  if (navGen !== iframeContentNavGen) {
@@ -2705,6 +3025,7 @@ function startIframeContentApplyWatcher(navGen) {
2705
3025
  try {
2706
3026
  var doc = iframe.contentDocument;
2707
3027
  if (!doc || !doc.body) return;
3028
+ if (prevDocRef && doc === prevDocRef) return;
2708
3029
  var docUrl = '';
2709
3030
  try { docUrl = String(doc.URL || ''); } catch(_) {}
2710
3031
  if (docUrl === 'about:blank') return;
@@ -2739,6 +3060,7 @@ function startIframeContentApplyWatcher(navGen) {
2739
3060
  }
2740
3061
  if (iframeEarlySyncPrimedForGen !== navGen) {
2741
3062
  iframeEarlySyncPrimedForGen = navGen;
3063
+ iframeEarlyDomSignature = computeIframeDomSignature(doc);
2742
3064
  syncIframeInteractions('iframe-early-paint');
2743
3065
  }
2744
3066
  }
@@ -2808,6 +3130,8 @@ function injectIframeSelectionStyles(doc) {
2808
3130
  st.textContent =
2809
3131
  '.vve-selected{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
2810
3132
  'box-shadow:0 0 0 2px rgba(99,102,241,.28),inset 0 0 0 1px rgba(99,102,241,.18)!important;}' +
3133
+ '.vve-tree-hover{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3134
+ 'box-shadow:0 0 0 2px rgba(99,102,241,.22),inset 0 0 0 1px rgba(99,102,241,.12)!important;}' +
2811
3135
  'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
2812
3136
  '.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
2813
3137
  'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
@@ -2819,6 +3143,47 @@ function injectIframeSelectionStyles(doc) {
2819
3143
  }
2820
3144
  }
2821
3145
 
3146
+ function clearTreeHoverHighlight() {
3147
+ if (!hoveredTreeEl) return;
3148
+ beginSuppressIframeMutationDirty();
3149
+ try {
3150
+ if (hoveredTreeEl.classList) hoveredTreeEl.classList.remove('vve-tree-hover');
3151
+ } catch(_) {
3152
+ } finally {
3153
+ endSuppressIframeMutationDirty();
3154
+ hoveredTreeEl = null;
3155
+ }
3156
+ }
3157
+
3158
+ function setTreeHoverHighlight(el) {
3159
+ if (!el || el.nodeType !== 1) {
3160
+ clearTreeHoverHighlight();
3161
+ return;
3162
+ }
3163
+ if (hoveredTreeEl === el) return;
3164
+ clearTreeHoverHighlight();
3165
+ beginSuppressIframeMutationDirty();
3166
+ try {
3167
+ if (el.classList) el.classList.add('vve-tree-hover');
3168
+ hoveredTreeEl = el;
3169
+ } catch(_) {
3170
+ hoveredTreeEl = null;
3171
+ } finally {
3172
+ endSuppressIframeMutationDirty();
3173
+ }
3174
+ }
3175
+
3176
+ function isTreeHoverOnlyClassMutation(mutation) {
3177
+ if (!mutation || mutation.type !== 'attributes' || mutation.attributeName !== 'class') return false;
3178
+ var oldClass = String(mutation.oldValue || '');
3179
+ var target = mutation.target;
3180
+ var nextClass = '';
3181
+ try {
3182
+ nextClass = target && typeof target.className === 'string' ? target.className : '';
3183
+ } catch(_) {}
3184
+ return oldClass.indexOf('vve-tree-hover') >= 0 || String(nextClass).indexOf('vve-tree-hover') >= 0;
3185
+ }
3186
+
2822
3187
  function setDragHandleActive(on) {
2823
3188
  dragHandleActive = !!on;
2824
3189
  var b = document.getElementById('sf-drag');
@@ -3139,6 +3504,9 @@ function renderDomTree(filterRaw) {
3139
3504
  if (e.target.closest && e.target.closest('.dt-chev')) return;
3140
3505
  selectElementFromTree(el);
3141
3506
  };
3507
+ row.onmouseenter = function() {
3508
+ setTreeHoverHighlight(el);
3509
+ };
3142
3510
  root.appendChild(row);
3143
3511
 
3144
3512
  if (!hasKids || collapsed) return;
@@ -3157,6 +3525,9 @@ function renderDomTree(filterRaw) {
3157
3525
  ? '<div class="dt-muted">No elements match your search.</div>'
3158
3526
  : '<div class="dt-muted">No visible elements yet.</div>';
3159
3527
  }
3528
+ root.onmouseleave = function() {
3529
+ clearTreeHoverHighlight();
3530
+ };
3160
3531
  }
3161
3532
 
3162
3533
  // \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
@@ -3174,6 +3545,40 @@ function pr2(l1, i1, l2, i2) {
3174
3545
  '<div class="pr2-item"><div class="pr2-lbl">'+l2+'</div>'+i2+'</div></div>';
3175
3546
  }
3176
3547
  function subLbl(text) { return '<div class="sub-lbl">'+text+'</div>'; }
3548
+ function openCustomCssModal() {
3549
+ var modal = document.getElementById('custom-css-modal');
3550
+ var ta = document.getElementById('custom-css-modal-textarea');
3551
+ if (!modal || !ta) return;
3552
+ var inp = document.getElementById('pp-css');
3553
+ var v = inp ? inp.value : (selectedEl && selectedEl.getAttribute ? (selectedEl.getAttribute('style') || '') : '');
3554
+ ta.value = v || '';
3555
+ modal.classList.add('open');
3556
+ modal.setAttribute('aria-hidden', 'false');
3557
+ setTimeout(function() {
3558
+ try { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } catch(_) {}
3559
+ }, 0);
3560
+ }
3561
+ function closeCustomCssModal() {
3562
+ var modal = document.getElementById('custom-css-modal');
3563
+ if (!modal) return;
3564
+ modal.classList.remove('open');
3565
+ modal.setAttribute('aria-hidden', 'true');
3566
+ }
3567
+ function applyCustomCssModal() {
3568
+ var ta = document.getElementById('custom-css-modal-textarea');
3569
+ var inp = document.getElementById('pp-css');
3570
+ if (!ta || !inp) {
3571
+ closeCustomCssModal();
3572
+ return;
3573
+ }
3574
+ inp.value = ta.value || '';
3575
+ try {
3576
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
3577
+ } catch(_) {
3578
+ if (typeof inp.oninput === 'function') inp.oninput();
3579
+ }
3580
+ closeCustomCssModal();
3581
+ }
3177
3582
  function weightOpts(cur) {
3178
3583
  return [['100','Thin'],['200','Extra Light'],['300','Light'],['400','Normal'],['500','Medium'],
3179
3584
  ['600','Semi Bold'],['700','Bold'],['800','Extra Bold'],['900','Black']]
@@ -3454,7 +3859,12 @@ function renderRightPanel(el) {
3454
3859
  // \u2500\u2500 CSS and Classes \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
3455
3860
  document.getElementById('acc-body-css').innerHTML =
3456
3861
  pr('Classes', '<input class="pr-inp" id="pp-cls" type="text" value="'+esc(el.className||'')+'" placeholder="class1 class2">') +
3457
- subLbl('Custom CSS') +
3862
+ '<div class="sub-lbl-row">' +
3863
+ '<div class="sub-lbl">Custom CSS</div>' +
3864
+ '<button type="button" class="css-expand-btn" title="Open full-screen editor" onclick="openCustomCssModal()">' +
3865
+ '<i class="bi bi-fullscreen"></i>' +
3866
+ '</button>' +
3867
+ '</div>' +
3458
3868
  '<textarea class="pr-inp" id="pp-css" style="width:100%;min-height:80px;font-family:monospace;font-size:11px" placeholder="color: red; font-size: 16px;">'+esc(el.getAttribute('style')||'')+'</textarea>';
3459
3869
 
3460
3870
  // \u2500\u2500 Attributes \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\u2500
@@ -3537,12 +3947,25 @@ function renderRightPanel(el) {
3537
3947
  var sel = buildSelector(el);
3538
3948
  bindings.forEach(function(b){
3539
3949
  var inp = document.getElementById(b[0]);
3540
- if (inp) inp.addEventListener('input', function(){
3950
+ if (inp) {
3951
+ var onValueChange = function() {
3541
3952
  // Read the original value BEFORE applying the change so we can revert later
3542
3953
  var orig = getOriginalValue(b[0], el);
3543
3954
  b[1](inp.value);
3544
- logChange(sel, b[0], inp.value, el, orig);
3545
- });
3955
+ var valueToLog = inp.value;
3956
+ try {
3957
+ console.log('[V2] input changed', {
3958
+ inputId: b[0],
3959
+ rawValue: inp.value,
3960
+ appliedValue: valueToLog,
3961
+ selector: sel,
3962
+ });
3963
+ } catch(_) {}
3964
+ logChange(sel, b[0], valueToLog, el, orig);
3965
+ };
3966
+ inp.addEventListener('input', onValueChange);
3967
+ inp.addEventListener('change', onValueChange);
3968
+ }
3546
3969
  });
3547
3970
  }
3548
3971
 
@@ -3805,6 +4228,32 @@ function attachClickHandler() {
3805
4228
  } catch(_) {}
3806
4229
  }
3807
4230
 
4231
+ function attachIframeHoverHandler() {
4232
+ try {
4233
+ var iframe = document.getElementById('iframeId');
4234
+ var doc = iframe && iframe.contentDocument;
4235
+ if (!doc || !doc.body) return;
4236
+ if (hoverAttachDoc === doc) return;
4237
+ hoverAttachDoc = doc;
4238
+ doc.addEventListener('mousemove', function(e) {
4239
+ if (currentMode !== 'editor') {
4240
+ clearTreeHoverHighlight();
4241
+ return;
4242
+ }
4243
+ var target = e.target;
4244
+ if (!target || target === doc.body || target === doc.documentElement) {
4245
+ clearTreeHoverHighlight();
4246
+ return;
4247
+ }
4248
+ setTreeHoverHighlight(target);
4249
+ }, true);
4250
+ doc.addEventListener('mouseout', function(e) {
4251
+ if (e.relatedTarget) return;
4252
+ clearTreeHoverHighlight();
4253
+ }, true);
4254
+ } catch(_) {}
4255
+ }
4256
+
3808
4257
  function attachChangeObserver() {
3809
4258
  try {
3810
4259
  var iframe = document.getElementById('iframeId');
@@ -3817,27 +4266,15 @@ function attachChangeObserver() {
3817
4266
  changeObserverDoc = null;
3818
4267
  }
3819
4268
  changeObserver = new MutationObserver(function(mutations) {
3820
- // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3821
- var bodyReplaced = false;
4269
+ var hasMeaningfulMutation = false;
3822
4270
  for (var mi = 0; mi < mutations.length; mi++) {
3823
- var m = mutations[mi];
3824
- if (
3825
- m &&
3826
- m.type === 'childList' &&
3827
- m.target === doc.body &&
3828
- m.addedNodes &&
3829
- m.removedNodes &&
3830
- m.addedNodes.length > 0 &&
3831
- m.removedNodes.length > 0
3832
- ) {
3833
- bodyReplaced = true;
4271
+ if (!isTreeHoverOnlyClassMutation(mutations[mi])) {
4272
+ hasMeaningfulMutation = true;
3834
4273
  break;
3835
4274
  }
3836
4275
  }
3837
- if (bodyReplaced) {
3838
- // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3839
- appliedStructuralChangesetKeys = {};
3840
- }
4276
+ if (!hasMeaningfulMutation) return;
4277
+ // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3841
4278
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3842
4279
  // Keep selection sticky by re-resolving from fingerprint.
3843
4280
  recoverSelectedElement(false);
@@ -3847,7 +4284,7 @@ function attachChangeObserver() {
3847
4284
  updateSelectionToolbar();
3848
4285
  });
3849
4286
  changeObserver.observe(doc.body, {
3850
- childList: true, subtree: true, attributes: true, characterData: true
4287
+ childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true
3851
4288
  });
3852
4289
  changeObserverDoc = doc;
3853
4290
  } catch(_) {}
@@ -3871,7 +4308,9 @@ function syncIframeInteractions(reason) {
3871
4308
  }
3872
4309
  showNoUrl(false);
3873
4310
  injectIframeSelectionStyles(doc);
4311
+ refreshPersistentChangesetStyleTagForActiveVariation();
3874
4312
  attachClickHandler();
4313
+ attachIframeHoverHandler();
3875
4314
  attachDragReposition();
3876
4315
  attachChangeObserver();
3877
4316
  startConsistencyWatchdog(doc);
@@ -4230,10 +4669,9 @@ window.addEventListener('load', function() {
4230
4669
  var iframe = document.getElementById('iframeId');
4231
4670
  iframe.addEventListener('load', function() {
4232
4671
  if (!iframe.src || iframe.src === 'about:blank' || iframe.src === window.location.href) return;
4233
- // New iframe navigation: always drop bindings tied to the previous document.
4234
- resetIframeBindings();
4235
4672
  var doc = iframe.contentDocument;
4236
4673
  if (!doc) {
4674
+ resetIframeBindings();
4237
4675
  syncIframeInteractions('iframe-load-no-doc');
4238
4676
  return;
4239
4677
  }
@@ -4242,14 +4680,32 @@ window.addEventListener('load', function() {
4242
4680
  // Stale events: src may already be the proxy URL while the document is still
4243
4681
  // about:blank (e.g. src cleared then reset to force reload). Ask sync path to retry.
4244
4682
  if (docUrl === 'about:blank') {
4683
+ resetIframeBindings();
4245
4684
  syncIframeInteractions('iframe-load-about-blank');
4246
4685
  return;
4247
4686
  }
4687
+ // If early-paint and final load DOM signatures match, avoid a second full apply/reset
4688
+ // that steals focus from sidebar controls while the page is still stabilizing.
4689
+ var shouldRefreshOnFinalLoad = true;
4690
+ if (
4691
+ iframeEarlyDomSignatureNavGen === iframeContentNavGen &&
4692
+ iframeEarlySyncPrimedForGen === iframeContentNavGen &&
4693
+ iframeEarlyDomSignature
4694
+ ) {
4695
+ var finalDomSignature = computeIframeDomSignature(doc);
4696
+ if (finalDomSignature && finalDomSignature === iframeEarlyDomSignature) {
4697
+ shouldRefreshOnFinalLoad = false;
4698
+ }
4699
+ }
4700
+ if (clickAttachDoc !== doc || dragAttachDoc !== doc || changeObserverDoc !== doc || hoverAttachDoc !== doc) {
4701
+ resetIframeBindings();
4702
+ }
4248
4703
  attachIframeLoadingUntilComplete(iframe);
4249
4704
  if (doc.body && iframeDocMatchesNavigatedSrc(iframe, doc)) {
4250
4705
  stopIframeContentApplyWatcher();
4251
- deselectElement();
4252
- applyActiveVariationHtml();
4706
+ if (shouldRefreshOnFinalLoad) {
4707
+ applyActiveVariationHtml();
4708
+ }
4253
4709
  }
4254
4710
  // Always attempt sync; it has its own readiness checks + retry loop.
4255
4711
  syncIframeInteractions('iframe-load');
@@ -4591,9 +5047,12 @@ function createVisualEditorMiddleware(options) {
4591
5047
  }
4592
5048
  );
4593
5049
  if (html.includes("</head>")) {
4594
- html = html.replace("</head>", `${popupHideCss}
4595
- ${consentAllowCss}
4596
- </head>`);
5050
+ html = html.replace(
5051
+ "</head>",
5052
+ `${iframeAlwaysShowCss}
5053
+ ${iframeAlwaysShowCssGuardScript}
5054
+ </head>`
5055
+ );
4597
5056
  }
4598
5057
  html = html.replace(
4599
5058
  /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,