@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.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() {
@@ -140,6 +152,17 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
140
152
  .tb-save-txt{font-size:14px;color:#00C951;white-space:nowrap}
141
153
  .tb-save-txt::before{content:'Saved'}
142
154
  #dirty-dot.on~.tb-save-txt::before{content:'Unsaved'}
155
+
156
+ /* \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 */
157
+ #ve-notification{
158
+ position:fixed;right:16px;top:64px;z-index:13000;max-width:360px;
159
+ padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.35;
160
+ border:1px solid var(--border);background:#fff;color:var(--text);
161
+ box-shadow:0 10px 24px rgba(2,6,23,.18);display:none
162
+ }
163
+ #ve-notification.show{display:block}
164
+ #ve-notification.error{border-color:#fecaca;background:#fef2f2;color:#991b1b}
165
+ #ve-notification.success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}
143
166
  .tb-save-time{font-size:12px;color:#52525b;white-space:nowrap}
144
167
  #dirty-dot.on~.tb-save-time{display:none}
145
168
  /* Simulate + Finalize buttons */
@@ -235,7 +258,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
235
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 */
236
259
  .dt-tree{font-size:11px;padding:0px 0 0px 20px;user-select:none}
237
260
  .dt-row{
238
- 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;
239
262
  cursor:pointer;color:var(--text-2);border-radius:4px;margin:0 4px
240
263
  }
241
264
  .dt-row:hover{background:var(--bg-hover);color:var(--text)}
@@ -447,6 +470,49 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
447
470
  /* \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 */
448
471
  .sub-lbl{font-size:10px;text-transform:uppercase;color:var(--text-3);font-weight:700;letter-spacing:.05em;margin:8px 0 5px}
449
472
  .sub-lbl:first-child{margin-top:0}
473
+ .sub-lbl-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:8px 0 5px}
474
+ .sub-lbl-row .sub-lbl{margin:0}
475
+ .css-expand-btn{
476
+ width:22px;height:22px;border:1px solid var(--border);border-radius:5px;background:#fff;
477
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text-3);
478
+ transition:all .15s
479
+ }
480
+ .css-expand-btn:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
481
+
482
+ /* \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 */
483
+ #custom-css-modal{
484
+ position:fixed;inset:0;z-index:12000;background:rgba(15,23,42,.5);
485
+ display:none;align-items:center;justify-content:center;padding:20px
486
+ }
487
+ #custom-css-modal.open{display:flex}
488
+ .custom-css-dialog{
489
+ width:min(860px,96vw);max-height:86vh;background:#fff;border:1px solid var(--border);
490
+ border-radius:10px;box-shadow:0 20px 40px rgba(2,6,23,.35);display:flex;flex-direction:column;overflow:hidden
491
+ }
492
+ .custom-css-head{
493
+ display:flex;align-items:center;justify-content:space-between;gap:10px;
494
+ padding:10px 12px;border-bottom:1px solid var(--border);background:#fff
495
+ }
496
+ .custom-css-title{font-size:12px;font-weight:700;color:var(--text)}
497
+ .custom-css-close{
498
+ border:none;background:transparent;color:var(--text-3);cursor:pointer;width:24px;height:24px;border-radius:5px
499
+ }
500
+ .custom-css-close:hover{background:var(--bg-hover);color:var(--text)}
501
+ #custom-css-modal-textarea{
502
+ width:100%;min-height:360px;max-height:58vh;resize:vertical;border:none;outline:none;
503
+ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
504
+ font-size:12px;line-height:1.5;padding:12px;background:#0b1220;color:#e2e8f0
505
+ }
506
+ .custom-css-actions{
507
+ display:flex;justify-content:flex-end;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:#fff
508
+ }
509
+ .custom-css-btn{
510
+ border:1px solid var(--border);background:#fff;color:var(--text-2);border-radius:6px;padding:6px 10px;cursor:pointer;
511
+ font-size:12px;font-weight:600;font-family:inherit
512
+ }
513
+ .custom-css-btn:hover{background:var(--bg-hover)}
514
+ .custom-css-btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
515
+ .custom-css-btn.primary:hover{filter:brightness(.97)}
450
516
 
451
517
  /* \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 */
452
518
  .shadow-presets{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
@@ -618,10 +684,10 @@ select.pr-inp{cursor:pointer;background:#fff}
618
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>
619
685
  <button class="tb-dk-btn" title="Comments"><i class="bi bi-chat-dots"></i></button>
620
686
  </div>
621
- <!-- btn-close: hidden visually, kept for JS event listener -->
622
- <button id="btn-close" style="display:none" title="Close editor"></button>
623
687
  <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
624
- <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>
625
691
  </div>
626
692
 
627
693
  <!-- url-bar: hidden, kept for JS compatibility -->
@@ -836,6 +902,23 @@ select.pr-inp{cursor:pointer;background:#fff}
836
902
 
837
903
  </div><!-- #app -->
838
904
 
905
+ <!-- Custom CSS full-screen modal -->
906
+ <div id="custom-css-modal" aria-hidden="true">
907
+ <div class="custom-css-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-css-modal-title">
908
+ <div class="custom-css-head">
909
+ <div class="custom-css-title" id="custom-css-modal-title"><i class="bi bi-code-slash"></i> Custom CSS</div>
910
+ <button type="button" class="custom-css-close" onclick="closeCustomCssModal()" title="Close">
911
+ <i class="bi bi-x-lg"></i>
912
+ </button>
913
+ </div>
914
+ <textarea id="custom-css-modal-textarea" spellcheck="false" placeholder="Type your css here"></textarea>
915
+ <div class="custom-css-actions">
916
+ <button type="button" class="custom-css-btn" onclick="closeCustomCssModal()">Cancel</button>
917
+ <button type="button" class="custom-css-btn primary" onclick="applyCustomCssModal()">Add</button>
918
+ </div>
919
+ </div>
920
+ </div>
921
+
839
922
  <!-- CDN scripts -->
840
923
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
841
924
  <script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/builder.js"></script>
@@ -907,13 +990,37 @@ function send(type, payload) {
907
990
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
908
991
  }
909
992
 
993
+ var notificationTimer = null;
994
+ function showEditorNotification(message, kind, durationMs) {
995
+ var id = 've-notification';
996
+ var el = document.getElementById(id);
997
+ if (!el) {
998
+ el = document.createElement('div');
999
+ el.id = id;
1000
+ el.setAttribute('role', 'status');
1001
+ el.setAttribute('aria-live', 'polite');
1002
+ document.body.appendChild(el);
1003
+ }
1004
+ el.className = '';
1005
+ el.classList.add(kind === 'success' ? 'success' : 'error');
1006
+ el.classList.add('show');
1007
+ el.textContent = String(message || 'Something went wrong');
1008
+ if (notificationTimer) clearTimeout(notificationTimer);
1009
+ notificationTimer = setTimeout(function() {
1010
+ var cur = document.getElementById(id);
1011
+ if (!cur) return;
1012
+ cur.classList.remove('show');
1013
+ }, Math.max(1200, durationMs || 2600));
1014
+ }
1015
+
910
1016
  function generatePreviewUrlString(args) {
911
1017
  var baseUrl = (args && args.url) || '';
912
1018
  var test = (args && args.test) || {};
913
1019
  var variation = (args && args.variation) || {};
914
1020
  if (!baseUrl) return '';
915
- var testId = test.iid || test.experimentId || test._id || '';
916
- var variationId = variation.iid || variation._id || '';
1021
+ var testId = test.iid || '';
1022
+ var variationId = variation.iid || '';
1023
+ if (!testId || !variationId) return '';
917
1024
  var cId = String(testId || '') + '_' + String(variationId || '');
918
1025
  var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
919
1026
  return (
@@ -937,6 +1044,10 @@ function simulateExperiment() {
937
1044
  test.pageUrl ||
938
1045
  (test.metadata_1 && test.metadata_1.editor_url) ||
939
1046
  '';
1047
+ if (!test.iid || !activeVariation || !activeVariation.iid) {
1048
+ showEditorNotification('Cannot simulate: missing test.iid or variation.iid.', 'error', 3200);
1049
+ return;
1050
+ }
940
1051
  var url = generatePreviewUrlString({
941
1052
  url: targetUrl,
942
1053
  test: test,
@@ -944,6 +1055,7 @@ function simulateExperiment() {
944
1055
  });
945
1056
  if (!url) {
946
1057
  console.warn('[V2] simulateExperiment: missing target URL');
1058
+ showEditorNotification('Cannot simulate: missing target URL.', 'error', 3200);
947
1059
  return;
948
1060
  }
949
1061
  try {
@@ -961,6 +1073,19 @@ window.addEventListener('message', function(e) {
961
1073
  }
962
1074
  });
963
1075
 
1076
+ document.addEventListener('keydown', function(e) {
1077
+ if (e.key !== 'Escape') return;
1078
+ var modal = document.getElementById('custom-css-modal');
1079
+ if (!modal || !modal.classList.contains('open')) return;
1080
+ closeCustomCssModal();
1081
+ });
1082
+
1083
+ document.addEventListener('click', function(e) {
1084
+ var modal = document.getElementById('custom-css-modal');
1085
+ if (!modal || !modal.classList.contains('open')) return;
1086
+ if (e.target === modal) closeCustomCssModal();
1087
+ });
1088
+
964
1089
  // \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
965
1090
  var experimentData = null;
966
1091
  var variations = [];
@@ -981,6 +1106,8 @@ var iframeContentNavGen = 0;
981
1106
  var iframeContentApplyTimer = null;
982
1107
  var iframeEarlyGranularPrimedForGen = null;
983
1108
  var iframeEarlySyncPrimedForGen = null;
1109
+ var iframeEarlyDomSignature = '';
1110
+ var iframeEarlyDomSignatureNavGen = 0;
984
1111
  /** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
985
1112
  var appliedStructuralChangesetKeys = {};
986
1113
  var isDirty = false;
@@ -992,6 +1119,7 @@ var selectedEl = null;
992
1119
  var selectedElFingerprint = '';
993
1120
  var selectedElRecoverMisses = 0;
994
1121
  var MAX_SELECTED_RECOVER_MISSES = 12;
1122
+ var hoveredTreeEl = null;
995
1123
  var isDeselectingSelection = false;
996
1124
  var suppressClickUntil = 0;
997
1125
  var dragAttachDoc = null;
@@ -1005,6 +1133,7 @@ var iframeSyncAttempts = 0;
1005
1133
  var selectionScrollWin = null;
1006
1134
  var selectionResizeBound = false;
1007
1135
  var clickAttachDoc = null;
1136
+ var hoverAttachDoc = null;
1008
1137
  var changeObserver = null;
1009
1138
  var changeObserverDoc = null;
1010
1139
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
@@ -1040,14 +1169,17 @@ function endSuppressIframeMutationDirty() {
1040
1169
  function commitStateChangesForActiveVariation() {
1041
1170
  if (!activeVarId) return;
1042
1171
  stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1172
+ refreshPersistentChangesetStyleTagForActiveVariation();
1043
1173
  }
1044
1174
 
1045
1175
  function loadStateChangesForActiveVariation() {
1046
1176
  if (!activeVarId) {
1047
1177
  stateChanges = [];
1178
+ refreshPersistentChangesetStyleTagForActiveVariation();
1048
1179
  return;
1049
1180
  }
1050
1181
  stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1182
+ refreshPersistentChangesetStyleTagForActiveVariation();
1051
1183
  }
1052
1184
 
1053
1185
  function recoverSelectedElement(forceDeselectOnMiss) {
@@ -1214,6 +1346,7 @@ function setMode(mode) {
1214
1346
  document.getElementById('btn-mode-editor').classList.toggle('active', mode === 'editor');
1215
1347
  document.getElementById('btn-mode-nav').classList.toggle('active', mode === 'navigate');
1216
1348
  if (mode === 'navigate') {
1349
+ clearTreeHoverHighlight();
1217
1350
  setDragHandleActive(false);
1218
1351
  deselectElement();
1219
1352
  } else if (mode === 'editor') {
@@ -1527,24 +1660,26 @@ function persistActiveVariationChangesets(arr) {
1527
1660
  }
1528
1661
  }
1529
1662
  }
1663
+ refreshPersistentChangesetStyleTagForActiveVariation();
1530
1664
  }
1531
1665
 
1532
1666
  function entrySnapshotKey(entry) {
1533
1667
  if (!entry || !entry.selector) return '';
1668
+ var SEP = '__vve_sep__';
1534
1669
  var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
1535
1670
  return (
1536
1671
  selKey +
1537
- '\0' +
1672
+ SEP +
1538
1673
  normalizeChangesetType(entry) +
1539
- '\0' +
1674
+ SEP +
1540
1675
  String(entry.property || '') +
1541
- '\0' +
1676
+ SEP +
1542
1677
  String(entry.attribute || '') +
1543
- '\0' +
1678
+ SEP +
1544
1679
  String(entry.action || '') +
1545
- '\0' +
1680
+ SEP +
1546
1681
  String(entry.html != null ? 'h' + String(entry.html).length : '') +
1547
- '\0' +
1682
+ SEP +
1548
1683
  String(entry.value != null ? 'v:' + entry.value : '')
1549
1684
  );
1550
1685
  }
@@ -1594,15 +1729,15 @@ function softReloadEditorIframe() {
1594
1729
  var navGen = nextIframeContentNavGen();
1595
1730
  resetIframeBindings();
1596
1731
  setIframePageLoadingUi(true);
1597
- iframe.src = '';
1598
1732
  iframe.src = appendIframeReloadBust(src);
1599
- startIframeContentApplyWatcher(navGen);
1733
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
1600
1734
  scheduleDomTreeRefresh();
1601
1735
  }
1602
1736
 
1603
1737
  /** @returns {boolean} true if a full iframe reload was started */
1604
1738
  function revertChangesetEntryOnDom(entry) {
1605
1739
  if (!entry) return false;
1740
+ var styleOnly = isStyleOnlyChangesetEntry(entry);
1606
1741
  if (entry.selector === '__vvveb_body__') {
1607
1742
  var iframeDoc0 = document.getElementById('iframeId').contentDocument;
1608
1743
  if (!iframeDoc0 || !iframeDoc0.body) return false;
@@ -1623,6 +1758,11 @@ function revertChangesetEntryOnDom(entry) {
1623
1758
  var snap = appliedChangesetSnapshots[k];
1624
1759
  var el = querySelectorResolved(iframeDoc, entry.selector);
1625
1760
  if (!snap || !el) {
1761
+ if (styleOnly) {
1762
+ refreshPersistentChangesetStyleTagForActiveVariation();
1763
+ delete appliedChangesetSnapshots[k];
1764
+ return false;
1765
+ }
1626
1766
  softReloadEditorIframe();
1627
1767
  delete appliedChangesetSnapshots[k];
1628
1768
  return true;
@@ -1637,6 +1777,11 @@ function revertChangesetEntryOnDom(entry) {
1637
1777
  else el.setAttribute(snap.name, snap.v);
1638
1778
  } else if (snap.kind === 'display') el.style.display = snap.v;
1639
1779
  else {
1780
+ if (styleOnly) {
1781
+ refreshPersistentChangesetStyleTagForActiveVariation();
1782
+ delete appliedChangesetSnapshots[k];
1783
+ return false;
1784
+ }
1640
1785
  softReloadEditorIframe();
1641
1786
  delete appliedChangesetSnapshots[k];
1642
1787
  return true;
@@ -1696,7 +1841,9 @@ function renderHistoryTab() {
1696
1841
  var lab = historyEntryTypeLabel(item.entry);
1697
1842
  var val = historyEntryValuePreview(item.entry);
1698
1843
  html +=
1699
- '<div class="state-item">' +
1844
+ '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1845
+ item.idx +
1846
+ ')">' +
1700
1847
  '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1701
1848
  item.idx +
1702
1849
  '</span>' +
@@ -1712,7 +1859,7 @@ function renderHistoryTab() {
1712
1859
  item.idx +
1713
1860
  ')" onclick="removeHistoryChangeset(' +
1714
1861
  item.idx +
1715
- ')">&#x2715;</button>' +
1862
+ ', event)">&#x2715;</button>' +
1716
1863
  '</div>';
1717
1864
  });
1718
1865
  html += '</div>';
@@ -1730,7 +1877,38 @@ function changesetListHasStructural(arr) {
1730
1877
  return false;
1731
1878
  }
1732
1879
 
1733
- function removeHistoryChangeset(idx) {
1880
+ function isStyleOnlyChangesetEntry(entry) {
1881
+ if (!entry) return false;
1882
+ var t = normalizeChangesetType(entry);
1883
+ if (t === 'style') return true;
1884
+ if (t === 'attribute' && String(entry.attribute || '').toLowerCase() === 'style') return true;
1885
+ return false;
1886
+ }
1887
+
1888
+ function focusHistoryChangeset(idx) {
1889
+ var v = getActiveVariationForHistory();
1890
+ if (!v) return;
1891
+ var arr = parseVariationChangesets(v);
1892
+ if (idx < 0 || idx >= arr.length) return;
1893
+ var entry = arr[idx];
1894
+ if (!entry || !entry.selector || entry.selector === '__vvveb_body__') return;
1895
+ try {
1896
+ var iframe = document.getElementById('iframeId');
1897
+ var iframeDoc = iframe && iframe.contentDocument;
1898
+ if (!iframeDoc) return;
1899
+ var el = querySelectorResolved(iframeDoc, entry.selector);
1900
+ if (!el) return;
1901
+ selectElement(el);
1902
+ try {
1903
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
1904
+ } catch(_) {
1905
+ el.scrollIntoView();
1906
+ }
1907
+ } catch(_) {}
1908
+ }
1909
+
1910
+ function removeHistoryChangeset(idx, evt) {
1911
+ if (evt && evt.stopPropagation) evt.stopPropagation();
1734
1912
  var v = getActiveVariationForHistory();
1735
1913
  if (!v) return;
1736
1914
  var arr = parseVariationChangesets(v);
@@ -1738,6 +1916,16 @@ function removeHistoryChangeset(idx) {
1738
1916
  var removed = arr[idx];
1739
1917
  arr.splice(idx, 1);
1740
1918
  persistActiveVariationChangesets(arr);
1919
+ if (isStyleOnlyChangesetEntry(removed)) {
1920
+ try {
1921
+ refreshPersistentChangesetStyleTagForActiveVariation();
1922
+ saveCurrentVariationHtml();
1923
+ } catch(_) {}
1924
+ if (currentMainTab === 'history') renderHistoryTab();
1925
+ recomputeEditorDirty();
1926
+ scheduleDomTreeRefresh();
1927
+ return;
1928
+ }
1741
1929
  var didReload = revertChangesetEntryOnDom(removed);
1742
1930
  try {
1743
1931
  delete varHtmlCache[activeVarId];
@@ -1745,15 +1933,19 @@ function removeHistoryChangeset(idx) {
1745
1933
  // Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
1746
1934
  // structural row remains or was removed (revert may already have started a reload for insert/body).
1747
1935
  var removedType = normalizeChangesetType(removed);
1748
- var needsStructuralReload =
1749
- !didReload &&
1750
- (removedType === 'insert' ||
1751
- removedType === 'reorder' ||
1752
- changesetListHasStructural(arr));
1936
+ var hasStructuralRemaining = changesetListHasStructural(arr);
1937
+ var removedIsStructural = removedType === 'insert' || removedType === 'reorder';
1753
1938
  if (didReload) {
1754
1939
  /* revertChangesetEntryOnDom already kicked off iframe reload */
1755
- } else if (needsStructuralReload) {
1940
+ } else if (removedIsStructural) {
1756
1941
  softReloadEditorIframe();
1942
+ } else if (hasStructuralRemaining) {
1943
+ // Keep current DOM state (already reverted for removed row) and only refresh style layer.
1944
+ // Avoid full reload and avoid re-applying all rows, which can duplicate structural insert/reorder entries.
1945
+ try {
1946
+ refreshPersistentChangesetStyleTagForActiveVariation();
1947
+ saveCurrentVariationHtml();
1948
+ } catch(_) {}
1757
1949
  } else {
1758
1950
  try {
1759
1951
  appliedStructuralChangesetKeys = {};
@@ -2017,6 +2209,27 @@ function bodyHasFirstPaintChild(body) {
2017
2209
  return false;
2018
2210
  }
2019
2211
 
2212
+ function computeIframeDomSignature(doc) {
2213
+ if (!doc || !doc.body) return '';
2214
+ var body = doc.body;
2215
+ var tags = [];
2216
+ var walker = null;
2217
+ try {
2218
+ walker = doc.createTreeWalker(body, NodeFilter.SHOW_ELEMENT, null);
2219
+ } catch(_) {
2220
+ walker = null;
2221
+ }
2222
+ var count = 0;
2223
+ if (walker) {
2224
+ while (walker.nextNode() && count < 400) {
2225
+ var node = walker.currentNode;
2226
+ tags.push((node && node.tagName) ? String(node.tagName).toLowerCase() : '');
2227
+ count += 1;
2228
+ }
2229
+ }
2230
+ return String(body.children ? body.children.length : 0) + '|' + String(count) + '|' + tags.join(',');
2231
+ }
2232
+
2020
2233
  /** True when at least one granular changeset selector already matches (nested content painted). */
2021
2234
  function granularAnySelectorMatches(doc, cs) {
2022
2235
  if (!doc || !cs || !cs.length) return false;
@@ -2121,7 +2334,7 @@ function runConsistencyReconcile() {
2121
2334
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2122
2335
  var granular = filterGranularChangesetEntries(cs);
2123
2336
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
2124
- if (unresolved > 0 || changesetListHasStructural(cs)) {
2337
+ if (unresolved > 0 || hasUnappliedStructuralChangesets(cs)) {
2125
2338
  reapplyActiveVariationGranular(doc);
2126
2339
  registerPendingGranularChangesets(cs, doc);
2127
2340
  }
@@ -2171,6 +2384,7 @@ function resetIframeBindings() {
2171
2384
  appliedStructuralChangesetKeys = {};
2172
2385
  clickAttachDoc = null;
2173
2386
  dragAttachDoc = null;
2387
+ hoverAttachDoc = null;
2174
2388
  changeObserverDoc = null;
2175
2389
  clearPendingGranularChangesets();
2176
2390
  if (changeObserver) {
@@ -2199,7 +2413,7 @@ function loadPage(proxyUrl) {
2199
2413
  iframe.style.display = 'block';
2200
2414
  setIframePageLoadingUi(true);
2201
2415
  iframe.src = appendIframeReloadBust(proxyUrl);
2202
- startIframeContentApplyWatcher(navGen);
2416
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2203
2417
  scheduleDomTreeRefresh();
2204
2418
  }
2205
2419
 
@@ -2271,7 +2485,7 @@ function switchVariation(varId) {
2271
2485
  iframe.src = appendIframeReloadBust(src);
2272
2486
  // Do not sync here: the document is still the previous navigation until the
2273
2487
  // iframe load event; an eager sync attached observers / DOM tree to the wrong document.
2274
- startIframeContentApplyWatcher(navGen);
2488
+ startIframeContentApplyWatcher(navGen, iframe.contentDocument || null);
2275
2489
  scheduleDomTreeRefresh();
2276
2490
  }
2277
2491
  } catch(_) {}
@@ -2491,24 +2705,136 @@ function flushPendingGranularChangesets() {
2491
2705
  function structuralChangesetDedupKey(entry) {
2492
2706
  var nt = normalizeChangesetType(entry);
2493
2707
  if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
2708
+ var SEP = '__vve_sep__';
2494
2709
  var vid = activeVarId || '';
2495
2710
  try {
2496
2711
  return (
2497
2712
  vid +
2498
- '\0' +
2713
+ SEP +
2499
2714
  nt +
2500
- '\0' +
2715
+ SEP +
2501
2716
  entry.selector +
2502
- '\0' +
2717
+ SEP +
2503
2718
  String(entry.action || '') +
2504
- '\0' +
2719
+ SEP +
2505
2720
  String(entry.html != null ? entry.html : '').slice(0, 240) +
2506
- '\0' +
2721
+ SEP +
2507
2722
  String(entry.targetSelector || '')
2508
2723
  );
2509
2724
  } catch(_) {
2510
- return vid + '\0' + nt + '\0' + entry.selector;
2725
+ return vid + SEP + nt + SEP + entry.selector;
2726
+ }
2727
+ }
2728
+
2729
+ function hasUnappliedStructuralChangesets(cs) {
2730
+ if (!cs || !cs.length) return false;
2731
+ for (var i = 0; i < cs.length; i++) {
2732
+ var e = cs[i];
2733
+ var t = normalizeChangesetType(e);
2734
+ if (t !== 'insert' && t !== 'reorder') continue;
2735
+ var k = structuralChangesetDedupKey(e);
2736
+ if (!k) return true;
2737
+ if (!appliedStructuralChangesetKeys[k]) return true;
2738
+ }
2739
+ return false;
2740
+ }
2741
+
2742
+ function parseInlineStyleDeclarations(styleText) {
2743
+ var out = [];
2744
+ if (styleText == null) return out;
2745
+ var s = String(styleText);
2746
+ if (!s.trim()) return out;
2747
+ var parts = s.split(';');
2748
+ for (var i = 0; i < parts.length; i++) {
2749
+ var seg = parts[i];
2750
+ if (!seg) continue;
2751
+ var idx = seg.indexOf(':');
2752
+ if (idx <= 0) continue;
2753
+ var prop = seg.slice(0, idx).trim();
2754
+ var value = seg.slice(idx + 1).trim();
2755
+ if (!prop || !value) continue;
2756
+ out.push({ prop: prop, value: value });
2757
+ }
2758
+ return out;
2759
+ }
2760
+
2761
+ function buildPersistentStyleRulesForActiveVariation() {
2762
+ if (!activeVarId) return '';
2763
+ var v = variations.find(function(x) { return x && x._id === activeVarId; });
2764
+ var parsed = parseVariationChangesets(v);
2765
+ var map = {};
2766
+ var order = [];
2767
+ function put(selector, prop, value) {
2768
+ if (!selector || !prop) return;
2769
+ if (value == null || value === '') return;
2770
+ var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
2771
+ var pr = String(prop).trim();
2772
+ var val = String(value).trim();
2773
+ if (!sel || !pr || !val) return;
2774
+ var k = sel + '__vve_sep__' + pr;
2775
+ if (!map[k]) order.push(k);
2776
+ map[k] = { selector: sel, property: pr, value: val };
2777
+ }
2778
+ for (var i = 0; i < parsed.length; i++) {
2779
+ var e = parsed[i];
2780
+ if (!e) continue;
2781
+ var t = normalizeChangesetType(e);
2782
+ if (t === 'style') {
2783
+ put(e.selector, e.property || e.cssProp, e.value);
2784
+ continue;
2785
+ }
2786
+ if (t === 'attribute' && String(e.attribute || '').toLowerCase() === 'style') {
2787
+ var decls = parseInlineStyleDeclarations(e.value);
2788
+ for (var di = 0; di < decls.length; di++) {
2789
+ put(e.selector, decls[di].prop, decls[di].value);
2790
+ }
2791
+ }
2511
2792
  }
2793
+ for (var j = 0; j < stateChanges.length; j++) {
2794
+ var c = stateChanges[j];
2795
+ if (!c) continue;
2796
+ if (c.cssProp) {
2797
+ put(c.selector, c.cssProp, c.value);
2798
+ continue;
2799
+ }
2800
+ if (c.inputId === 'pp-css') {
2801
+ var liveDecls = parseInlineStyleDeclarations(c.value);
2802
+ for (var ldi = 0; ldi < liveDecls.length; ldi++) {
2803
+ put(c.selector, liveDecls[ldi].prop, liveDecls[ldi].value);
2804
+ }
2805
+ }
2806
+ }
2807
+ var lines = [];
2808
+ for (var oi = 0; oi < order.length; oi++) {
2809
+ var row = map[order[oi]];
2810
+ lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
2811
+ }
2812
+ return lines.join('\\n');
2813
+ }
2814
+
2815
+ function upsertPersistentChangesetStyleTag(iframeDoc, rulesText) {
2816
+ if (!iframeDoc) return;
2817
+ var STYLE_ID = '__vve_persist_changesets_style__';
2818
+ var prev = iframeDoc.getElementById(STYLE_ID);
2819
+ if (!rulesText) {
2820
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
2821
+ return;
2822
+ }
2823
+ var head = iframeDoc.head || iframeDoc.getElementsByTagName('head')[0];
2824
+ if (!head) return;
2825
+ var styleEl = prev || iframeDoc.createElement('style');
2826
+ styleEl.id = STYLE_ID;
2827
+ if (styleEl.textContent !== rulesText) styleEl.textContent = rulesText;
2828
+ if (!styleEl.parentNode) head.appendChild(styleEl);
2829
+ }
2830
+
2831
+ function refreshPersistentChangesetStyleTagForActiveVariation() {
2832
+ try {
2833
+ var iframe = document.getElementById('iframeId');
2834
+ var iframeDoc = iframe && iframe.contentDocument;
2835
+ if (!iframeDoc) return;
2836
+ upsertPersistentChangesetStyleTag(iframeDoc, buildPersistentStyleRulesForActiveVariation());
2837
+ } catch(_) {}
2512
2838
  }
2513
2839
 
2514
2840
  /**
@@ -2545,23 +2871,11 @@ function applyChangesetEntry(entry, iframeDoc) {
2545
2871
  else if (entry.value != null) el.textContent = entry.value;
2546
2872
  break;
2547
2873
  case 'style':
2548
- if (entry.property) {
2549
- var propKebab = entry.property;
2550
- var cam = camelize(propKebab);
2551
- if (entry.value == null || entry.value === '') {
2552
- try { el.style.removeProperty(propKebab); } catch(_) {}
2553
- try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
2554
- } else {
2555
- try {
2556
- el.style.setProperty(propKebab, entry.value, 'important');
2557
- } catch(_) {
2558
- el.style[cam] = entry.value;
2559
- }
2560
- }
2561
- }
2874
+ // Style changes are applied via persistent stylesheet injection.
2562
2875
  break;
2563
2876
  case 'attribute':
2564
2877
  if (entry.attribute && entry.value != null) {
2878
+ if (String(entry.attribute).toLowerCase() === 'style') break;
2565
2879
  el.setAttribute(entry.attribute, entry.value);
2566
2880
  }
2567
2881
  break;
@@ -2598,6 +2912,7 @@ function applyActiveVariationHtml() {
2598
2912
 
2599
2913
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2600
2914
  var cs = parseVariationChangesets(variation);
2915
+ refreshPersistentChangesetStyleTagForActiveVariation();
2601
2916
 
2602
2917
  beginSuppressIframeMutationDirty();
2603
2918
  try {
@@ -2612,6 +2927,7 @@ function applyActiveVariationHtml() {
2612
2927
  for (var i = 0; i < cs.length; i++) {
2613
2928
  applyChangesetEntry(cs[i], iframeDoc);
2614
2929
  }
2930
+ refreshPersistentChangesetStyleTagForActiveVariation();
2615
2931
  // Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
2616
2932
  registerPendingGranularChangesets(cs, iframeDoc);
2617
2933
  } finally {
@@ -2660,6 +2976,7 @@ function applyVariationGranularOnly(iframeDoc) {
2660
2976
  for (var i = 0; i < cs.length; i++) {
2661
2977
  applyChangesetEntry(cs[i], iframeDoc);
2662
2978
  }
2979
+ refreshPersistentChangesetStyleTagForActiveVariation();
2663
2980
  registerPendingGranularChangesets(cs, iframeDoc);
2664
2981
  } finally {
2665
2982
  endSuppressIframeMutationDirty();
@@ -2678,16 +2995,19 @@ function reapplyActiveVariationGranular(iframeDoc) {
2678
2995
  for (var i = 0; i < cs.length; i++) {
2679
2996
  applyChangesetEntry(cs[i], iframeDoc);
2680
2997
  }
2998
+ refreshPersistentChangesetStyleTagForActiveVariation();
2681
2999
  } finally {
2682
3000
  endSuppressIframeMutationDirty();
2683
3001
  }
2684
3002
  }
2685
3003
 
2686
3004
  /** Poll iframe document during navigation; apply granular edits as soon as DOM can match selectors. */
2687
- function startIframeContentApplyWatcher(navGen) {
3005
+ function startIframeContentApplyWatcher(navGen, prevDocRef) {
2688
3006
  stopIframeContentApplyWatcher();
2689
3007
  iframeEarlyGranularPrimedForGen = null;
2690
3008
  iframeEarlySyncPrimedForGen = null;
3009
+ iframeEarlyDomSignature = '';
3010
+ iframeEarlyDomSignatureNavGen = navGen;
2691
3011
  var iframe = document.getElementById('iframeId');
2692
3012
  iframeContentApplyTimer = setInterval(function() {
2693
3013
  if (navGen !== iframeContentNavGen) {
@@ -2697,6 +3017,7 @@ function startIframeContentApplyWatcher(navGen) {
2697
3017
  try {
2698
3018
  var doc = iframe.contentDocument;
2699
3019
  if (!doc || !doc.body) return;
3020
+ if (prevDocRef && doc === prevDocRef) return;
2700
3021
  var docUrl = '';
2701
3022
  try { docUrl = String(doc.URL || ''); } catch(_) {}
2702
3023
  if (docUrl === 'about:blank') return;
@@ -2731,6 +3052,7 @@ function startIframeContentApplyWatcher(navGen) {
2731
3052
  }
2732
3053
  if (iframeEarlySyncPrimedForGen !== navGen) {
2733
3054
  iframeEarlySyncPrimedForGen = navGen;
3055
+ iframeEarlyDomSignature = computeIframeDomSignature(doc);
2734
3056
  syncIframeInteractions('iframe-early-paint');
2735
3057
  }
2736
3058
  }
@@ -2800,6 +3122,8 @@ function injectIframeSelectionStyles(doc) {
2800
3122
  st.textContent =
2801
3123
  '.vve-selected{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
2802
3124
  'box-shadow:0 0 0 2px rgba(99,102,241,.28),inset 0 0 0 1px rgba(99,102,241,.18)!important;}' +
3125
+ '.vve-tree-hover{outline:2px solid #6366f1!important;outline-offset:2px!important;' +
3126
+ 'box-shadow:0 0 0 2px rgba(99,102,241,.22),inset 0 0 0 1px rgba(99,102,241,.12)!important;}' +
2803
3127
  'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
2804
3128
  '.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
2805
3129
  'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
@@ -2811,6 +3135,47 @@ function injectIframeSelectionStyles(doc) {
2811
3135
  }
2812
3136
  }
2813
3137
 
3138
+ function clearTreeHoverHighlight() {
3139
+ if (!hoveredTreeEl) return;
3140
+ beginSuppressIframeMutationDirty();
3141
+ try {
3142
+ if (hoveredTreeEl.classList) hoveredTreeEl.classList.remove('vve-tree-hover');
3143
+ } catch(_) {
3144
+ } finally {
3145
+ endSuppressIframeMutationDirty();
3146
+ hoveredTreeEl = null;
3147
+ }
3148
+ }
3149
+
3150
+ function setTreeHoverHighlight(el) {
3151
+ if (!el || el.nodeType !== 1) {
3152
+ clearTreeHoverHighlight();
3153
+ return;
3154
+ }
3155
+ if (hoveredTreeEl === el) return;
3156
+ clearTreeHoverHighlight();
3157
+ beginSuppressIframeMutationDirty();
3158
+ try {
3159
+ if (el.classList) el.classList.add('vve-tree-hover');
3160
+ hoveredTreeEl = el;
3161
+ } catch(_) {
3162
+ hoveredTreeEl = null;
3163
+ } finally {
3164
+ endSuppressIframeMutationDirty();
3165
+ }
3166
+ }
3167
+
3168
+ function isTreeHoverOnlyClassMutation(mutation) {
3169
+ if (!mutation || mutation.type !== 'attributes' || mutation.attributeName !== 'class') return false;
3170
+ var oldClass = String(mutation.oldValue || '');
3171
+ var target = mutation.target;
3172
+ var nextClass = '';
3173
+ try {
3174
+ nextClass = target && typeof target.className === 'string' ? target.className : '';
3175
+ } catch(_) {}
3176
+ return oldClass.indexOf('vve-tree-hover') >= 0 || String(nextClass).indexOf('vve-tree-hover') >= 0;
3177
+ }
3178
+
2814
3179
  function setDragHandleActive(on) {
2815
3180
  dragHandleActive = !!on;
2816
3181
  var b = document.getElementById('sf-drag');
@@ -3131,6 +3496,9 @@ function renderDomTree(filterRaw) {
3131
3496
  if (e.target.closest && e.target.closest('.dt-chev')) return;
3132
3497
  selectElementFromTree(el);
3133
3498
  };
3499
+ row.onmouseenter = function() {
3500
+ setTreeHoverHighlight(el);
3501
+ };
3134
3502
  root.appendChild(row);
3135
3503
 
3136
3504
  if (!hasKids || collapsed) return;
@@ -3149,6 +3517,9 @@ function renderDomTree(filterRaw) {
3149
3517
  ? '<div class="dt-muted">No elements match your search.</div>'
3150
3518
  : '<div class="dt-muted">No visible elements yet.</div>';
3151
3519
  }
3520
+ root.onmouseleave = function() {
3521
+ clearTreeHoverHighlight();
3522
+ };
3152
3523
  }
3153
3524
 
3154
3525
  // \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
@@ -3166,6 +3537,40 @@ function pr2(l1, i1, l2, i2) {
3166
3537
  '<div class="pr2-item"><div class="pr2-lbl">'+l2+'</div>'+i2+'</div></div>';
3167
3538
  }
3168
3539
  function subLbl(text) { return '<div class="sub-lbl">'+text+'</div>'; }
3540
+ function openCustomCssModal() {
3541
+ var modal = document.getElementById('custom-css-modal');
3542
+ var ta = document.getElementById('custom-css-modal-textarea');
3543
+ if (!modal || !ta) return;
3544
+ var inp = document.getElementById('pp-css');
3545
+ var v = inp ? inp.value : (selectedEl && selectedEl.getAttribute ? (selectedEl.getAttribute('style') || '') : '');
3546
+ ta.value = v || '';
3547
+ modal.classList.add('open');
3548
+ modal.setAttribute('aria-hidden', 'false');
3549
+ setTimeout(function() {
3550
+ try { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } catch(_) {}
3551
+ }, 0);
3552
+ }
3553
+ function closeCustomCssModal() {
3554
+ var modal = document.getElementById('custom-css-modal');
3555
+ if (!modal) return;
3556
+ modal.classList.remove('open');
3557
+ modal.setAttribute('aria-hidden', 'true');
3558
+ }
3559
+ function applyCustomCssModal() {
3560
+ var ta = document.getElementById('custom-css-modal-textarea');
3561
+ var inp = document.getElementById('pp-css');
3562
+ if (!ta || !inp) {
3563
+ closeCustomCssModal();
3564
+ return;
3565
+ }
3566
+ inp.value = ta.value || '';
3567
+ try {
3568
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
3569
+ } catch(_) {
3570
+ if (typeof inp.oninput === 'function') inp.oninput();
3571
+ }
3572
+ closeCustomCssModal();
3573
+ }
3169
3574
  function weightOpts(cur) {
3170
3575
  return [['100','Thin'],['200','Extra Light'],['300','Light'],['400','Normal'],['500','Medium'],
3171
3576
  ['600','Semi Bold'],['700','Bold'],['800','Extra Bold'],['900','Black']]
@@ -3446,7 +3851,12 @@ function renderRightPanel(el) {
3446
3851
  // \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
3447
3852
  document.getElementById('acc-body-css').innerHTML =
3448
3853
  pr('Classes', '<input class="pr-inp" id="pp-cls" type="text" value="'+esc(el.className||'')+'" placeholder="class1 class2">') +
3449
- subLbl('Custom CSS') +
3854
+ '<div class="sub-lbl-row">' +
3855
+ '<div class="sub-lbl">Custom CSS</div>' +
3856
+ '<button type="button" class="css-expand-btn" title="Open full-screen editor" onclick="openCustomCssModal()">' +
3857
+ '<i class="bi bi-fullscreen"></i>' +
3858
+ '</button>' +
3859
+ '</div>' +
3450
3860
  '<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>';
3451
3861
 
3452
3862
  // \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
@@ -3529,12 +3939,25 @@ function renderRightPanel(el) {
3529
3939
  var sel = buildSelector(el);
3530
3940
  bindings.forEach(function(b){
3531
3941
  var inp = document.getElementById(b[0]);
3532
- if (inp) inp.addEventListener('input', function(){
3942
+ if (inp) {
3943
+ var onValueChange = function() {
3533
3944
  // Read the original value BEFORE applying the change so we can revert later
3534
3945
  var orig = getOriginalValue(b[0], el);
3535
3946
  b[1](inp.value);
3536
- logChange(sel, b[0], inp.value, el, orig);
3537
- });
3947
+ var valueToLog = inp.value;
3948
+ try {
3949
+ console.log('[V2] input changed', {
3950
+ inputId: b[0],
3951
+ rawValue: inp.value,
3952
+ appliedValue: valueToLog,
3953
+ selector: sel,
3954
+ });
3955
+ } catch(_) {}
3956
+ logChange(sel, b[0], valueToLog, el, orig);
3957
+ };
3958
+ inp.addEventListener('input', onValueChange);
3959
+ inp.addEventListener('change', onValueChange);
3960
+ }
3538
3961
  });
3539
3962
  }
3540
3963
 
@@ -3797,6 +4220,32 @@ function attachClickHandler() {
3797
4220
  } catch(_) {}
3798
4221
  }
3799
4222
 
4223
+ function attachIframeHoverHandler() {
4224
+ try {
4225
+ var iframe = document.getElementById('iframeId');
4226
+ var doc = iframe && iframe.contentDocument;
4227
+ if (!doc || !doc.body) return;
4228
+ if (hoverAttachDoc === doc) return;
4229
+ hoverAttachDoc = doc;
4230
+ doc.addEventListener('mousemove', function(e) {
4231
+ if (currentMode !== 'editor') {
4232
+ clearTreeHoverHighlight();
4233
+ return;
4234
+ }
4235
+ var target = e.target;
4236
+ if (!target || target === doc.body || target === doc.documentElement) {
4237
+ clearTreeHoverHighlight();
4238
+ return;
4239
+ }
4240
+ setTreeHoverHighlight(target);
4241
+ }, true);
4242
+ doc.addEventListener('mouseout', function(e) {
4243
+ if (e.relatedTarget) return;
4244
+ clearTreeHoverHighlight();
4245
+ }, true);
4246
+ } catch(_) {}
4247
+ }
4248
+
3800
4249
  function attachChangeObserver() {
3801
4250
  try {
3802
4251
  var iframe = document.getElementById('iframeId');
@@ -3809,27 +4258,15 @@ function attachChangeObserver() {
3809
4258
  changeObserverDoc = null;
3810
4259
  }
3811
4260
  changeObserver = new MutationObserver(function(mutations) {
3812
- // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3813
- var bodyReplaced = false;
4261
+ var hasMeaningfulMutation = false;
3814
4262
  for (var mi = 0; mi < mutations.length; mi++) {
3815
- var m = mutations[mi];
3816
- if (
3817
- m &&
3818
- m.type === 'childList' &&
3819
- m.target === doc.body &&
3820
- m.addedNodes &&
3821
- m.removedNodes &&
3822
- m.addedNodes.length > 0 &&
3823
- m.removedNodes.length > 0
3824
- ) {
3825
- bodyReplaced = true;
4263
+ if (!isTreeHoverOnlyClassMutation(mutations[mi])) {
4264
+ hasMeaningfulMutation = true;
3826
4265
  break;
3827
4266
  }
3828
4267
  }
3829
- if (bodyReplaced) {
3830
- // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3831
- appliedStructuralChangesetKeys = {};
3832
- }
4268
+ if (!hasMeaningfulMutation) return;
4269
+ // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3833
4270
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3834
4271
  // Keep selection sticky by re-resolving from fingerprint.
3835
4272
  recoverSelectedElement(false);
@@ -3839,7 +4276,7 @@ function attachChangeObserver() {
3839
4276
  updateSelectionToolbar();
3840
4277
  });
3841
4278
  changeObserver.observe(doc.body, {
3842
- childList: true, subtree: true, attributes: true, characterData: true
4279
+ childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true
3843
4280
  });
3844
4281
  changeObserverDoc = doc;
3845
4282
  } catch(_) {}
@@ -3863,7 +4300,9 @@ function syncIframeInteractions(reason) {
3863
4300
  }
3864
4301
  showNoUrl(false);
3865
4302
  injectIframeSelectionStyles(doc);
4303
+ refreshPersistentChangesetStyleTagForActiveVariation();
3866
4304
  attachClickHandler();
4305
+ attachIframeHoverHandler();
3867
4306
  attachDragReposition();
3868
4307
  attachChangeObserver();
3869
4308
  startConsistencyWatchdog(doc);
@@ -4222,10 +4661,9 @@ window.addEventListener('load', function() {
4222
4661
  var iframe = document.getElementById('iframeId');
4223
4662
  iframe.addEventListener('load', function() {
4224
4663
  if (!iframe.src || iframe.src === 'about:blank' || iframe.src === window.location.href) return;
4225
- // New iframe navigation: always drop bindings tied to the previous document.
4226
- resetIframeBindings();
4227
4664
  var doc = iframe.contentDocument;
4228
4665
  if (!doc) {
4666
+ resetIframeBindings();
4229
4667
  syncIframeInteractions('iframe-load-no-doc');
4230
4668
  return;
4231
4669
  }
@@ -4234,14 +4672,32 @@ window.addEventListener('load', function() {
4234
4672
  // Stale events: src may already be the proxy URL while the document is still
4235
4673
  // about:blank (e.g. src cleared then reset to force reload). Ask sync path to retry.
4236
4674
  if (docUrl === 'about:blank') {
4675
+ resetIframeBindings();
4237
4676
  syncIframeInteractions('iframe-load-about-blank');
4238
4677
  return;
4239
4678
  }
4679
+ // If early-paint and final load DOM signatures match, avoid a second full apply/reset
4680
+ // that steals focus from sidebar controls while the page is still stabilizing.
4681
+ var shouldRefreshOnFinalLoad = true;
4682
+ if (
4683
+ iframeEarlyDomSignatureNavGen === iframeContentNavGen &&
4684
+ iframeEarlySyncPrimedForGen === iframeContentNavGen &&
4685
+ iframeEarlyDomSignature
4686
+ ) {
4687
+ var finalDomSignature = computeIframeDomSignature(doc);
4688
+ if (finalDomSignature && finalDomSignature === iframeEarlyDomSignature) {
4689
+ shouldRefreshOnFinalLoad = false;
4690
+ }
4691
+ }
4692
+ if (clickAttachDoc !== doc || dragAttachDoc !== doc || changeObserverDoc !== doc || hoverAttachDoc !== doc) {
4693
+ resetIframeBindings();
4694
+ }
4240
4695
  attachIframeLoadingUntilComplete(iframe);
4241
4696
  if (doc.body && iframeDocMatchesNavigatedSrc(iframe, doc)) {
4242
4697
  stopIframeContentApplyWatcher();
4243
- deselectElement();
4244
- applyActiveVariationHtml();
4698
+ if (shouldRefreshOnFinalLoad) {
4699
+ applyActiveVariationHtml();
4700
+ }
4245
4701
  }
4246
4702
  // Always attempt sync; it has its own readiness checks + retry loop.
4247
4703
  syncIframeInteractions('iframe-load');
@@ -4583,9 +5039,12 @@ function createVisualEditorMiddleware(options) {
4583
5039
  }
4584
5040
  );
4585
5041
  if (html.includes("</head>")) {
4586
- html = html.replace("</head>", `${popupHideCss}
4587
- ${consentAllowCss}
4588
- </head>`);
5042
+ html = html.replace(
5043
+ "</head>",
5044
+ `${iframeAlwaysShowCss}
5045
+ ${iframeAlwaysShowCssGuardScript}
5046
+ </head>`
5047
+ );
4589
5048
  }
4590
5049
  html = html.replace(
4591
5050
  /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,