@accelerated-agency/visual-editor 0.1.5 → 0.1.7
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/bridge-channel-ISTPKGMY.js +1 -10
- package/dist/chunk-WCSTG2IY.js +1 -5
- package/dist/index.js +651 -743
- package/dist/vite.cjs +2067 -0
- package/dist/vite.js +28 -12
- package/package.json +5 -4
package/dist/vite.cjs
ADDED
|
@@ -0,0 +1,2067 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var url = require('url');
|
|
6
|
+
|
|
7
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
11
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
12
|
+
|
|
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 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.`;
|
|
16
|
+
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&¤t.nodeType===1&&depth<5){var part=current.tagName.toLowerCase();if(current.classList&¤t.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'});})();`;
|
|
17
|
+
function buildVvvebEditorHtml() {
|
|
18
|
+
const CRO_SECTIONS = [
|
|
19
|
+
{
|
|
20
|
+
key: "cro/hero",
|
|
21
|
+
name: "CRO Hero",
|
|
22
|
+
icon: "\u{1F3AF}",
|
|
23
|
+
desc: "High-converting hero section",
|
|
24
|
+
html: '<section style="background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:80px 20px;text-align:center"><div style="max-width:700px;margin:0 auto"><h1 style="font-size:2.8rem;font-weight:800;margin-bottom:16px;color:white">Your Compelling Headline Here</h1><p style="font-size:1.15rem;margin-bottom:28px;opacity:0.9">A clear, benefit-focused subheadline that explains exactly what visitors get from you.</p><a href="#" style="display:inline-block;background:#f59e0b;color:white;padding:16px 40px;border-radius:6px;font-size:1.1rem;font-weight:700;text-decoration:none">Get Started Free →</a><p style="margin-top:14px;font-size:13px;opacity:0.65">✓ No credit card required · ✓ 14-day free trial</p></div></section>'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "cro/social-proof",
|
|
28
|
+
name: "Social Proof",
|
|
29
|
+
icon: "\u2B50",
|
|
30
|
+
desc: "Star rating + 3-column testimonials",
|
|
31
|
+
html: '<section style="padding:60px 20px;background:#f9fafb"><div style="max-width:1000px;margin:0 auto"><div style="text-align:center;margin-bottom:36px"><div style="font-size:2rem;color:#f59e0b;letter-spacing:4px">★★★★★</div><p style="font-weight:700;font-size:1.2rem;margin-top:4px">4.9 / 5 from 2,400+ verified reviews</p></div><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px"><div style="background:white;border-radius:8px;padding:24px;box-shadow:0 1px 4px rgba(0,0,0,0.08)"><div style="color:#f59e0b;font-size:1.1rem;margin-bottom:10px">★★★★★</div><p style="font-size:14px;color:#444;margin-bottom:16px">"This product completely transformed our results. 40% increase in conversions within the first month."</p><div style="display:flex;align-items:center;gap:10px"><div style="width:36px;height:36px;border-radius:50%;background:#e0e7ff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px">SJ</div><div><div style="font-weight:600;font-size:13px">Sarah Johnson</div><div style="font-size:11px;color:#888">VP Marketing, TechCorp</div></div></div></div><div style="background:white;border-radius:8px;padding:24px;box-shadow:0 1px 4px rgba(0,0,0,0.08)"><div style="color:#f59e0b;font-size:1.1rem;margin-bottom:10px">★★★★★</div><p style="font-size:14px;color:#444;margin-bottom:16px">"The ROI was immediate. Best investment we have made this year. Highly recommend to any serious marketer."</p><div style="display:flex;align-items:center;gap:10px"><div style="width:36px;height:36px;border-radius:50%;background:#dcfce7;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px">MC</div><div><div style="font-weight:600;font-size:13px">Mark Chen</div><div style="font-size:11px;color:#888">Founder, GrowthLabs</div></div></div></div><div style="background:white;border-radius:8px;padding:24px;box-shadow:0 1px 4px rgba(0,0,0,0.08)"><div style="color:#f59e0b;font-size:1.1rem;margin-bottom:10px">★★★★★</div><p style="font-size:14px;color:#444;margin-bottom:16px">"Incredible support and the product delivers exactly what it promises. Our entire team loves it."</p><div style="display:flex;align-items:center;gap:10px"><div style="width:36px;height:36px;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px">AT</div><div><div style="font-weight:600;font-size:13px">Amanda Torres</div><div style="font-size:11px;color:#888">CMO, ScaleUp Inc</div></div></div></div></div></div></section>'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "cro/urgency-banner",
|
|
35
|
+
name: "Urgency Banner",
|
|
36
|
+
icon: "\u23F0",
|
|
37
|
+
desc: "Countdown timer + limited-time offer",
|
|
38
|
+
html: '<div style="background:#ef4444;color:white;text-align:center;padding:14px;font-size:14px;font-weight:500">🔥 Limited time offer: <strong>50% OFF</strong> ends in <span style="font-family:monospace;font-size:1rem;font-weight:700;background:rgba(0,0,0,0.2);padding:2px 10px;border-radius:4px;margin:0 6px">23:47:12</span>— <a href="#" style="color:white;font-weight:700;text-decoration:underline">Claim your discount →</a></div>'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "cro/trust-badges",
|
|
42
|
+
name: "Trust Badges",
|
|
43
|
+
icon: "\u{1F6E1}\uFE0F",
|
|
44
|
+
desc: "Security seals, payment icons, guarantees",
|
|
45
|
+
html: '<div style="padding:28px 20px;border-top:1px solid #e5e5e5;border-bottom:1px solid #e5e5e5;background:white"><div style="display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:32px;max-width:700px;margin:0 auto"><div style="text-align:center;font-size:12px;color:#555"><div style="font-size:28px;margin-bottom:6px">🔒</div><div style="font-weight:600">SSL Secure</div><div style="font-size:10px;color:#888">256-bit encryption</div></div><div style="text-align:center;font-size:12px;color:#555"><div style="font-size:28px;margin-bottom:6px">↵</div><div style="font-weight:600">30-Day Guarantee</div><div style="font-size:10px;color:#888">Money back, no questions</div></div><div style="text-align:center;font-size:12px;color:#555"><div style="font-size:28px;margin-bottom:6px">⭐</div><div style="font-weight:600">4.9 / 5 Rating</div><div style="font-size:10px;color:#888">From 2,400+ reviews</div></div><div style="text-align:center;font-size:12px;color:#555"><div style="font-size:28px;margin-bottom:6px">💳</div><div style="font-weight:600">Secure Payment</div><div style="font-size:10px;color:#888">Visa · MC · PayPal</div></div></div></div>'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "cro/cta-block",
|
|
49
|
+
name: "CTA Block",
|
|
50
|
+
icon: "\u{1F4E3}",
|
|
51
|
+
desc: "Dark high-impact call-to-action section",
|
|
52
|
+
html: '<section style="background:#1a1a2e;color:white;padding:64px 20px;text-align:center"><div style="max-width:600px;margin:0 auto"><h2 style="font-size:2rem;font-weight:800;color:white;margin-bottom:10px">Ready to get started?</h2><p style="opacity:0.7;margin-bottom:32px">Join 10,000+ companies already seeing results with our platform.</p><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap"><a href="#" style="display:inline-block;background:#f59e0b;color:white;padding:14px 36px;border-radius:6px;font-weight:700;text-decoration:none;font-size:1rem">Start Free Trial</a><a href="#" style="display:inline-block;border:2px solid rgba(255,255,255,0.4);color:white;padding:14px 36px;border-radius:6px;font-weight:600;text-decoration:none;font-size:1rem">Schedule Demo</a></div><p style="font-size:12px;margin-top:18px;opacity:0.45">No credit card required · Cancel anytime</p></div></section>'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "cro/feature-grid",
|
|
56
|
+
name: "Feature Grid",
|
|
57
|
+
icon: "\u2728",
|
|
58
|
+
desc: "3-column key benefits grid",
|
|
59
|
+
html: '<section style="padding:60px 20px;background:white"><div style="max-width:900px;margin:0 auto"><h2 style="text-align:center;font-size:1.8rem;font-weight:800;margin-bottom:48px">Why choose us?</h2><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:32px;text-align:center"><div><div style="font-size:2.5rem;margin-bottom:14px">🚀</div><h5 style="font-weight:700;margin-bottom:8px">Fast Results</h5><p style="color:#666;font-size:14px">See improvements in your conversion rate within the first 30 days or your money back.</p></div><div><div style="font-size:2.5rem;margin-bottom:14px">📊</div><h5 style="font-weight:700;margin-bottom:8px">Data-Driven</h5><p style="color:#666;font-size:14px">Every decision backed by real user data and statistical significance testing.</p></div><div><div style="font-size:2.5rem;margin-bottom:14px">🎯</div><h5 style="font-weight:700;margin-bottom:8px">Precision Targeting</h5><p style="color:#666;font-size:14px">Target the right audience segments with personalized, high-converting experiences.</p></div></div></div></section>'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "cro/exit-intent-bar",
|
|
63
|
+
name: "Exit Intent Bar",
|
|
64
|
+
icon: "\u{1F6AA}",
|
|
65
|
+
desc: "Sticky bottom bar for exit-intent capture",
|
|
66
|
+
html: `<div style="position:fixed;bottom:0;left:0;right:0;background:#1e293b;color:white;padding:14px 20px;display:flex;align-items:center;justify-content:center;gap:16px;z-index:9999;box-shadow:0 -4px 20px rgba(0,0,0,0.2)"><span style="font-size:14px">🔔 <strong>Wait!</strong> Don't leave empty-handed — get 20% off your first order.</span><a href="#" style="background:#f59e0b;color:white;padding:8px 20px;border-radius:4px;font-weight:700;text-decoration:none;font-size:13px;white-space:nowrap">Claim Offer</a><a href="#" style="color:rgba(255,255,255,0.5);font-size:12px;text-decoration:none">No thanks</a></div>`
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "cro/pricing-table",
|
|
70
|
+
name: "Pricing Table",
|
|
71
|
+
icon: "\u{1F4B0}",
|
|
72
|
+
desc: "3-tier pricing with highlighted plan",
|
|
73
|
+
html: '<section style="padding:60px 20px;background:#f8fafc"><div style="max-width:900px;margin:0 auto"><h2 style="text-align:center;font-weight:800;font-size:1.8rem;margin-bottom:8px">Simple, transparent pricing</h2><p style="text-align:center;color:#666;margin-bottom:48px">No hidden fees. Cancel anytime.</p><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px"><div style="background:white;border:1px solid #e5e5e5;border-radius:10px;padding:28px"><h3 style="font-weight:700;margin-bottom:6px">Starter</h3><div style="font-size:2rem;font-weight:800;margin-bottom:16px">$29<span style="font-size:1rem;font-weight:400;color:#888">/mo</span></div><ul style="list-style:none;padding:0;font-size:14px;color:#555;margin-bottom:24px"><li style="padding:6px 0;border-bottom:1px solid #f0f0f0">✓ Up to 10,000 visitors/mo</li><li style="padding:6px 0;border-bottom:1px solid #f0f0f0">✓ 5 A/B tests</li><li style="padding:6px 0">✓ Email support</li></ul><a href="#" style="display:block;text-align:center;border:2px solid #e5e5e5;padding:10px;border-radius:6px;font-weight:600;text-decoration:none;color:#333">Get started</a></div><div style="background:#3b82f6;border-radius:10px;padding:28px;position:relative;transform:scale(1.04);box-shadow:0 8px 32px rgba(59,130,246,0.3)"><div style="position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#f59e0b;color:white;font-size:11px;font-weight:700;padding:4px 12px;border-radius:20px">MOST POPULAR</div><h3 style="font-weight:700;margin-bottom:6px;color:white">Pro</h3><div style="font-size:2rem;font-weight:800;margin-bottom:16px;color:white">$79<span style="font-size:1rem;font-weight:400;opacity:0.7">/mo</span></div><ul style="list-style:none;padding:0;font-size:14px;color:rgba(255,255,255,0.85);margin-bottom:24px"><li style="padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.15)">✓ Up to 100,000 visitors/mo</li><li style="padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.15)">✓ Unlimited A/B tests</li><li style="padding:6px 0">✓ Priority support</li></ul><a href="#" style="display:block;text-align:center;background:white;color:#3b82f6;padding:10px;border-radius:6px;font-weight:700;text-decoration:none">Get started</a></div><div style="background:white;border:1px solid #e5e5e5;border-radius:10px;padding:28px"><h3 style="font-weight:700;margin-bottom:6px">Enterprise</h3><div style="font-size:2rem;font-weight:800;margin-bottom:16px">Custom</div><ul style="list-style:none;padding:0;font-size:14px;color:#555;margin-bottom:24px"><li style="padding:6px 0;border-bottom:1px solid #f0f0f0">✓ Unlimited visitors</li><li style="padding:6px 0;border-bottom:1px solid #f0f0f0">✓ White-label option</li><li style="padding:6px 0">✓ Dedicated support</li></ul><a href="#" style="display:block;text-align:center;border:2px solid #e5e5e5;padding:10px;border-radius:6px;font-weight:600;text-decoration:none;color:#333">Contact sales</a></div></div></div></section>'
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
const sectionsJson = JSON.stringify(CRO_SECTIONS);
|
|
77
|
+
return `<!DOCTYPE html>
|
|
78
|
+
<html lang="en">
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="UTF-8">
|
|
81
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
82
|
+
<title>Visual Editor V2</title>
|
|
83
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
84
|
+
<style>
|
|
85
|
+
/* \u2500\u2500 Reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
86
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
87
|
+
html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;font-size:13px;color:#1e293b;background:#f1f5f9}
|
|
88
|
+
|
|
89
|
+
/* \u2500\u2500 CSS variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
90
|
+
:root{
|
|
91
|
+
--bg: #ffffff;
|
|
92
|
+
--bg-sub: #f8fafc;
|
|
93
|
+
--bg-hover: #f1f5f9;
|
|
94
|
+
--border: #e2e8f0;
|
|
95
|
+
--border-sub: #f1f5f9;
|
|
96
|
+
--text: #1e293b;
|
|
97
|
+
--text-2: #475569;
|
|
98
|
+
--text-3: #94a3b8;
|
|
99
|
+
--accent: #6366f1;
|
|
100
|
+
--accent-bg: #eef2ff;
|
|
101
|
+
--accent-txt: #4f46e5;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
105
|
+
#app{display:flex;flex-direction:column;height:100vh}
|
|
106
|
+
#main{display:flex;flex:1;overflow:hidden;min-height:0}
|
|
107
|
+
|
|
108
|
+
/* \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
109
|
+
#toolbar{
|
|
110
|
+
height:48px;background:var(--bg);border-bottom:1px solid var(--border);
|
|
111
|
+
display:flex;align-items:center;padding:0 12px;gap:6px;flex-shrink:0;z-index:100;
|
|
112
|
+
box-shadow:0 1px 3px rgba(0,0,0,.04)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* \u2500\u2500 Left panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
116
|
+
#left-panel{
|
|
117
|
+
width:232px;background:var(--bg-sub);border-right:1px solid var(--border);
|
|
118
|
+
display:flex;flex-direction:column;flex-shrink:0;overflow:hidden
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* \u2500\u2500 Iframe panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
122
|
+
#iframe-panel{
|
|
123
|
+
flex:1;background:#e8ecf0;display:flex;flex-direction:column;
|
|
124
|
+
align-items:center;justify-content:flex-start;overflow:auto;position:relative
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* \u2500\u2500 Right panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
128
|
+
#right-panel{
|
|
129
|
+
width:252px;background:var(--bg);border-left:1px solid var(--border);
|
|
130
|
+
display:flex;flex-direction:column;flex-shrink:0;overflow:hidden
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* \u2500\u2500 Breadcrumb \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
134
|
+
#breadcrumb{
|
|
135
|
+
height:26px;background:var(--bg);border-top:1px solid var(--border);
|
|
136
|
+
display:flex;align-items:center;padding:0 12px;font-size:11px;
|
|
137
|
+
color:var(--text-3);flex-shrink:0;gap:5px;overflow:hidden
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* \u2500\u2500 Loading overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
141
|
+
#loading{
|
|
142
|
+
position:absolute;inset:0;background:rgba(248,250,252,.95);display:flex;
|
|
143
|
+
flex-direction:column;align-items:center;justify-content:center;gap:12px;z-index:50;
|
|
144
|
+
backdrop-filter:blur(2px)
|
|
145
|
+
}
|
|
146
|
+
#loading.hidden{display:none}
|
|
147
|
+
.spinner{width:32px;height:32px;border:2.5px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite}
|
|
148
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
149
|
+
|
|
150
|
+
/* \u2500\u2500 No-URL overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
151
|
+
#no-url{
|
|
152
|
+
display:none;position:absolute;inset:0;flex-direction:column;
|
|
153
|
+
align-items:center;justify-content:center;gap:10px;color:var(--text-3);text-align:center;padding:40px
|
|
154
|
+
}
|
|
155
|
+
#no-url .nu-icon{font-size:48px;opacity:.3}
|
|
156
|
+
|
|
157
|
+
/* \u2500\u2500 Device frame \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
158
|
+
#device-frame{
|
|
159
|
+
width:100%;min-height:100%;display:flex;align-items:stretch;
|
|
160
|
+
justify-content:center;transition:max-width .3s ease
|
|
161
|
+
}
|
|
162
|
+
#device-frame.tablet{
|
|
163
|
+
max-width:768px;
|
|
164
|
+
box-shadow:0 0 0 1px var(--border),0 4px 24px rgba(0,0,0,.08)
|
|
165
|
+
}
|
|
166
|
+
#device-frame.mobile{
|
|
167
|
+
max-width:390px;
|
|
168
|
+
box-shadow:0 0 0 1px var(--border),0 8px 40px rgba(0,0,0,.12)
|
|
169
|
+
}
|
|
170
|
+
#iframeId{flex:1;border:0;background:#fff;display:none;min-height:100%}
|
|
171
|
+
|
|
172
|
+
/* \u2500\u2500 Toolbar atoms \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
173
|
+
.tb-btn{
|
|
174
|
+
background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text-2);
|
|
175
|
+
cursor:pointer;font-size:12px;font-weight:500;padding:5px 11px;transition:all .15s;
|
|
176
|
+
white-space:nowrap;display:flex;align-items:center;gap:5px
|
|
177
|
+
}
|
|
178
|
+
.tb-btn:hover{background:var(--bg-hover);color:var(--text);border-color:#cbd5e1}
|
|
179
|
+
.tb-btn.active,.tb-btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
180
|
+
.tb-btn.primary:hover{background:#4338ca;border-color:#4338ca}
|
|
181
|
+
.tb-btn.icon{width:32px;height:32px;padding:0;justify-content:center;flex-shrink:0}
|
|
182
|
+
.tb-btn.close-btn{color:#94a3b8}
|
|
183
|
+
.tb-btn.close-btn:hover{background:#fef2f2;border-color:#fca5a5;color:#ef4444}
|
|
184
|
+
.tb-sep{width:1px;height:20px;background:var(--border);flex-shrink:0}
|
|
185
|
+
.tb-space{flex:1}
|
|
186
|
+
|
|
187
|
+
/* \u2500\u2500 Toggle group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
188
|
+
.tg{display:flex;background:var(--bg-sub);border-radius:8px;padding:2px;border:1px solid var(--border);gap:1px}
|
|
189
|
+
.tg-btn{
|
|
190
|
+
background:transparent;border:none;color:var(--text-3);cursor:pointer;
|
|
191
|
+
padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600;
|
|
192
|
+
transition:all .15s;white-space:nowrap;display:flex;align-items:center;gap:4px
|
|
193
|
+
}
|
|
194
|
+
.tg-btn:hover{color:var(--text);background:var(--bg-hover)}
|
|
195
|
+
.tg-btn.active{background:#fff;color:var(--accent-txt);box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
|
196
|
+
.tg-btn.icon-only{padding:4px 8px}
|
|
197
|
+
|
|
198
|
+
/* \u2500\u2500 Dirty dot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
199
|
+
#dirty-dot{width:6px;height:6px;border-radius:50%;background:transparent;flex-shrink:0;transition:background .2s}
|
|
200
|
+
#dirty-dot.on{background:#f59e0b}
|
|
201
|
+
|
|
202
|
+
/* \u2500\u2500 URL bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
203
|
+
#url-bar{
|
|
204
|
+
flex:1;max-width:280px;background:var(--bg-sub);border:1px solid var(--border);border-radius:20px;
|
|
205
|
+
color:var(--text-2);font-size:11px;padding:4px 12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* \u2500\u2500 Left panel sections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
209
|
+
.lp-sec{border-bottom:1px solid var(--border);flex-shrink:0}
|
|
210
|
+
.lp-sec-hd{
|
|
211
|
+
padding:8px 12px 6px;font-size:10px;font-weight:700;text-transform:uppercase;
|
|
212
|
+
letter-spacing:.08em;color:var(--text-3);display:flex;align-items:center;justify-content:space-between
|
|
213
|
+
}
|
|
214
|
+
.lp-sec-hd span{color:var(--accent-txt);font-size:10px;font-weight:500;text-transform:none;letter-spacing:0}
|
|
215
|
+
|
|
216
|
+
/* \u2500\u2500 Variation tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
217
|
+
#variation-tabs{padding:4px 8px 8px;display:flex;flex-direction:column;gap:4px}
|
|
218
|
+
.var-tab{
|
|
219
|
+
background:#fff;border:1px solid var(--border);border-radius:20px;color:var(--text-2);
|
|
220
|
+
cursor:pointer;font-size:11px;font-weight:600;padding:5px 12px;transition:all .15s;
|
|
221
|
+
width:100%;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis
|
|
222
|
+
}
|
|
223
|
+
.var-tab:hover{background:var(--bg-hover);border-color:#cbd5e1;color:var(--text)}
|
|
224
|
+
.var-tab.active{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 1px 4px rgba(99,102,241,.3)}
|
|
225
|
+
|
|
226
|
+
/* \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
227
|
+
#comp-search{
|
|
228
|
+
background:#fff;border:1px solid var(--border);border-radius:6px;
|
|
229
|
+
color:var(--text);font-size:12px;padding:6px 10px;width:100%;outline:none
|
|
230
|
+
}
|
|
231
|
+
#comp-search::placeholder{color:var(--text-3)}
|
|
232
|
+
#comp-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,102,241,.12)}
|
|
233
|
+
|
|
234
|
+
/* \u2500\u2500 Left tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
235
|
+
.lp-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;background:#fff}
|
|
236
|
+
.lp-tab{
|
|
237
|
+
flex:1;padding:8px 4px;text-align:center;font-size:11px;color:var(--text-3);
|
|
238
|
+
cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;font-weight:600
|
|
239
|
+
}
|
|
240
|
+
.lp-tab:hover{color:var(--text-2)}
|
|
241
|
+
.lp-tab.active{color:var(--accent-txt);border-bottom-color:var(--accent)}
|
|
242
|
+
|
|
243
|
+
/* \u2500\u2500 Left panel body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
|
+
.lp-body{flex:1;overflow-y:auto}
|
|
245
|
+
.lp-body::-webkit-scrollbar{width:3px}
|
|
246
|
+
.lp-body::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
|
|
247
|
+
.tab-pane{display:none}.tab-pane.active{display:block}
|
|
248
|
+
|
|
249
|
+
/* \u2500\u2500 Component grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
250
|
+
.cg-hdr{padding:8px 10px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)}
|
|
251
|
+
.cg-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;padding:0 8px 8px}
|
|
252
|
+
.cg-item{
|
|
253
|
+
background:#fff;border:1px solid var(--border);border-radius:8px;cursor:pointer;
|
|
254
|
+
padding:10px 4px 8px;text-align:center;transition:all .15s;color:var(--text-2);user-select:none
|
|
255
|
+
}
|
|
256
|
+
.cg-item:hover{background:var(--accent-bg);border-color:var(--accent);color:var(--accent-txt);transform:translateY(-1px);box-shadow:0 2px 8px rgba(99,102,241,.15)}
|
|
257
|
+
.cg-item:active{transform:translateY(0);box-shadow:none}
|
|
258
|
+
.cg-icon{font-size:18px;margin-bottom:4px}
|
|
259
|
+
.cg-name{font-size:10px;line-height:1.2}
|
|
260
|
+
|
|
261
|
+
/* \u2500\u2500 Section list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
262
|
+
.sec-item{
|
|
263
|
+
background:#fff;border:1px solid var(--border);border-radius:8px;
|
|
264
|
+
margin:0 8px 6px;overflow:hidden;transition:all .15s;cursor:pointer
|
|
265
|
+
}
|
|
266
|
+
.sec-item:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 12px rgba(99,102,241,.12)}
|
|
267
|
+
.sec-thumb{
|
|
268
|
+
background:var(--bg-sub);height:52px;display:flex;align-items:center;
|
|
269
|
+
justify-content:center;font-size:22px;border-bottom:1px solid var(--border)
|
|
270
|
+
}
|
|
271
|
+
.sec-info{padding:7px 9px}
|
|
272
|
+
.sec-name{font-size:11px;font-weight:600;color:var(--text)}
|
|
273
|
+
.sec-desc{font-size:10px;color:var(--text-3);margin-top:2px;line-height:1.3}
|
|
274
|
+
|
|
275
|
+
/* \u2500\u2500 Right panel body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
276
|
+
.rp-body{flex:1;overflow-y:auto;overflow-x:hidden;background:#fff;min-width:0}
|
|
277
|
+
.rp-body::-webkit-scrollbar{width:3px}
|
|
278
|
+
.rp-body::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:2px}
|
|
279
|
+
|
|
280
|
+
/* \u2500\u2500 No-selection placeholder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
281
|
+
#no-sel{padding:32px 16px;text-align:center;color:var(--text-3)}
|
|
282
|
+
#no-sel .ns-icon{font-size:32px;margin-bottom:10px;opacity:.4}
|
|
283
|
+
|
|
284
|
+
/* \u2500\u2500 Element info header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
285
|
+
#el-info{padding:8px 12px;border-bottom:1px solid var(--border);background:var(--bg-sub);flex-shrink:0}
|
|
286
|
+
#el-info-tag{font-size:10px;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.05em;font-family:monospace}
|
|
287
|
+
#el-info-sel{font-size:10px;color:var(--accent-txt);font-family:monospace;margin-top:2px;word-break:break-all;opacity:.8}
|
|
288
|
+
|
|
289
|
+
/* \u2500\u2500 Accordion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
290
|
+
.acc-section{border-bottom:1px solid var(--border-sub)}
|
|
291
|
+
.acc-hd{
|
|
292
|
+
display:flex;align-items:center;padding:11px 14px;cursor:pointer;
|
|
293
|
+
user-select:none;transition:background .12s;
|
|
294
|
+
font-size:13px;font-weight:500;color:var(--text)
|
|
295
|
+
}
|
|
296
|
+
.acc-hd:hover{background:var(--bg-hover)}
|
|
297
|
+
.acc-arrow{margin-left:auto;font-size:10px;color:var(--text-3);transition:transform .2s;flex-shrink:0}
|
|
298
|
+
.acc-section.open .acc-arrow{transform:rotate(90deg)}
|
|
299
|
+
.acc-body{display:none;padding:8px 12px 12px;background:#fff;overflow:hidden;box-sizing:border-box}
|
|
300
|
+
.acc-section.open .acc-body{display:block}
|
|
301
|
+
|
|
302
|
+
/* \u2500\u2500 2-col property grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
303
|
+
.pr2{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:7px;min-width:0}
|
|
304
|
+
/* min-width:0 prevents grid children from overflowing their track */
|
|
305
|
+
.pr2-item{display:flex;flex-direction:column;gap:3px;min-width:0;overflow:hidden}
|
|
306
|
+
.pr2-item .pr-inp{width:100%;min-width:0}
|
|
307
|
+
.pr2-lbl{font-size:10px;color:var(--text-3);font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
|
308
|
+
|
|
309
|
+
/* \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 */
|
|
310
|
+
.sub-lbl{font-size:10px;text-transform:uppercase;color:var(--text-3);font-weight:700;letter-spacing:.05em;margin:8px 0 5px}
|
|
311
|
+
.sub-lbl:first-child{margin-top:0}
|
|
312
|
+
|
|
313
|
+
/* \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 */
|
|
314
|
+
.shadow-presets{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
|
|
315
|
+
.shadow-preset{background:var(--bg-sub);border:1px solid var(--border);border-radius:5px;cursor:pointer;padding:3px 8px;font-size:11px;color:var(--text-2);font-family:inherit}
|
|
316
|
+
.shadow-preset:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
|
|
317
|
+
|
|
318
|
+
/* \u2500\u2500 Opacity row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
319
|
+
.op-wrap{display:flex;align-items:center;gap:8px}
|
|
320
|
+
input[type=range].op-slider{flex:1;accent-color:var(--accent);cursor:pointer}
|
|
321
|
+
|
|
322
|
+
/* \u2500\u2500 Property groups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
323
|
+
.pg{padding:10px 10px 4px;border-bottom:1px solid var(--border-sub)}
|
|
324
|
+
.pg-lbl{font-size:10px;font-weight:700;text-transform:uppercase;color:var(--text-3);letter-spacing:.06em;margin-bottom:8px}
|
|
325
|
+
.pr{display:flex;align-items:center;gap:6px;margin-bottom:7px}
|
|
326
|
+
.pr-lbl{font-size:11px;color:var(--text-2);width:68px;flex-shrink:0}
|
|
327
|
+
.pr-inp{
|
|
328
|
+
flex:1;background:var(--bg-sub);border:1px solid var(--border);border-radius:5px;
|
|
329
|
+
color:var(--text);font-size:12px;padding:4px 7px;outline:none;min-width:0
|
|
330
|
+
}
|
|
331
|
+
.pr-inp:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(99,102,241,.12);background:#fff}
|
|
332
|
+
textarea.pr-inp{resize:vertical;min-height:54px;font-family:inherit}
|
|
333
|
+
select.pr-inp{cursor:pointer;background:#fff}
|
|
334
|
+
.pr-color{width:28px;height:28px;padding:0;background:transparent;border-radius:5px;cursor:pointer;flex-shrink:0;border:1px solid var(--border)}
|
|
335
|
+
.pr-num{width:58px;flex:none}
|
|
336
|
+
.pr-unit{font-size:11px;color:var(--text-3)}
|
|
337
|
+
|
|
338
|
+
/* \u2500\u2500 Advanced panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
339
|
+
.adv-section{padding:10px}
|
|
340
|
+
.adv-row{margin-bottom:8px}
|
|
341
|
+
.adv-key{font-size:10px;color:var(--text-3);margin-bottom:3px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
|
|
342
|
+
.adv-val{font-size:11px;color:var(--text-2);word-break:break-all;background:var(--bg-sub);padding:5px 7px;border-radius:5px;font-family:monospace;border:1px solid var(--border)}
|
|
343
|
+
|
|
344
|
+
/* \u2500\u2500 Selected element ring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
345
|
+
.vve-selected{outline:2px solid var(--accent) !important;outline-offset:1px !important}
|
|
346
|
+
|
|
347
|
+
/* iframe is always interactive \u2014 mode behaviour is controlled by JS handlers */
|
|
348
|
+
#iframeId{pointer-events:all}
|
|
349
|
+
|
|
350
|
+
/* \u2500\u2500 Image properties panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
351
|
+
.img-preview{
|
|
352
|
+
width:100%;height:72px;object-fit:contain;border:1px solid var(--border);
|
|
353
|
+
border-radius:6px;background:var(--bg-sub);margin-bottom:8px;display:block
|
|
354
|
+
}
|
|
355
|
+
.img-row{display:flex;align-items:center;gap:4px;margin-bottom:5px;min-width:0}
|
|
356
|
+
.img-row .pr-inp{min-width:0}
|
|
357
|
+
.img-desc{width:58px!important;flex:none!important}
|
|
358
|
+
.img-cond-op{width:100px!important;flex:none!important}
|
|
359
|
+
.img-cond-val{width:62px!important;flex:none!important}
|
|
360
|
+
.img-remove{
|
|
361
|
+
background:none;border:none;cursor:pointer;color:var(--text-3);
|
|
362
|
+
font-size:13px;padding:2px 5px;line-height:1;border-radius:4px;
|
|
363
|
+
font-family:inherit;flex-shrink:0
|
|
364
|
+
}
|
|
365
|
+
.img-remove:hover{color:#ef4444;background:#fef2f2}
|
|
366
|
+
.img-add{
|
|
367
|
+
display:flex;align-items:center;gap:6px;width:100%;padding:5px 8px;
|
|
368
|
+
border:1px dashed var(--border);border-radius:6px;cursor:pointer;
|
|
369
|
+
color:var(--text-3);font-size:11px;background:none;font-family:inherit;
|
|
370
|
+
transition:all .15s;margin-top:2px
|
|
371
|
+
}
|
|
372
|
+
.img-add:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
|
|
373
|
+
|
|
374
|
+
/* \u2500\u2500 Right panel main tabs (Design / States / History) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
375
|
+
#main-tabs{display:flex;border-bottom:1px solid var(--border);background:#fff;flex-shrink:0}
|
|
376
|
+
.main-tab{
|
|
377
|
+
flex:1;padding:10px 4px;text-align:center;font-size:12px;color:var(--text-3);
|
|
378
|
+
cursor:pointer;border:none;background:transparent;border-bottom:2px solid transparent;
|
|
379
|
+
transition:all .15s;font-weight:600;font-family:inherit
|
|
380
|
+
}
|
|
381
|
+
.main-tab:hover{color:var(--text-2)}
|
|
382
|
+
.main-tab.active{color:var(--accent-txt);border-bottom-color:var(--accent)}
|
|
383
|
+
.rp-pane{flex:1;overflow-y:auto;overflow-x:hidden;min-width:0;display:none}
|
|
384
|
+
.rp-pane.active{display:block}
|
|
385
|
+
|
|
386
|
+
/* \u2500\u2500 States 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
387
|
+
.states-empty{padding:36px 16px;text-align:center;color:var(--text-3);font-size:12px}
|
|
388
|
+
.states-empty i{font-size:30px;display:block;margin-bottom:10px;opacity:.3}
|
|
389
|
+
.state-group{border-bottom:1px solid var(--border-sub)}
|
|
390
|
+
.state-group-sel{
|
|
391
|
+
padding:8px 12px 3px;font-size:10px;font-family:monospace;
|
|
392
|
+
color:var(--text-3);font-weight:700;word-break:break-all;letter-spacing:-.01em
|
|
393
|
+
}
|
|
394
|
+
.state-item{display:flex;align-items:center;padding:4px 10px 4px 18px;gap:6px}
|
|
395
|
+
.state-item-label{flex:1;font-size:11px;color:var(--text-2)}
|
|
396
|
+
.state-item-val{
|
|
397
|
+
font-size:10px;color:var(--accent-txt);font-family:monospace;
|
|
398
|
+
max-width:68px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
399
|
+
background:var(--accent-bg);padding:1px 5px;border-radius:3px
|
|
400
|
+
}
|
|
401
|
+
.state-remove{
|
|
402
|
+
background:none;border:none;cursor:pointer;color:var(--text-3);
|
|
403
|
+
font-size:13px;padding:2px 5px;line-height:1;border-radius:4px;
|
|
404
|
+
font-family:inherit;flex-shrink:0
|
|
405
|
+
}
|
|
406
|
+
.state-remove:hover{color:#ef4444;background:#fef2f2}
|
|
407
|
+
#states-clear{
|
|
408
|
+
display:block;width:calc(100% - 24px);margin:10px 12px;
|
|
409
|
+
background:none;border:1px solid var(--border);border-radius:6px;
|
|
410
|
+
padding:6px;font-size:11px;color:var(--text-2);cursor:pointer;font-family:inherit;
|
|
411
|
+
transition:all .15s
|
|
412
|
+
}
|
|
413
|
+
#states-clear:hover{border-color:#fca5a5;color:#ef4444;background:#fef2f2}
|
|
414
|
+
|
|
415
|
+
/* \u2500\u2500 History tab (placeholder) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
416
|
+
.tab-ph{padding:40px 16px;text-align:center;color:var(--text-3)}
|
|
417
|
+
.tab-ph i{font-size:30px;display:block;margin-bottom:10px;opacity:.3}
|
|
418
|
+
.tab-ph p{font-size:12px;line-height:1.6;margin-top:6px}
|
|
419
|
+
</style>
|
|
420
|
+
</head>
|
|
421
|
+
<body class="mode-editor">
|
|
422
|
+
<div id="app">
|
|
423
|
+
|
|
424
|
+
<!-- \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->
|
|
425
|
+
<div id="toolbar">
|
|
426
|
+
<span style="font-size:10px;font-weight:800;color:var(--accent-txt);letter-spacing:.06em;flex-shrink:0;background:var(--accent-bg);border:1px solid #c7d2fe;padding:2px 6px;border-radius:4px">V2</span>
|
|
427
|
+
<div id="dirty-dot" title="Unsaved changes"></div>
|
|
428
|
+
<div class="tb-sep"></div>
|
|
429
|
+
|
|
430
|
+
<!-- Edit / Navigate mode -->
|
|
431
|
+
<div class="tg">
|
|
432
|
+
<button class="tg-btn active" id="btn-mode-editor" onclick="setMode('editor')" title="Edit mode \u2014 click elements to edit">
|
|
433
|
+
<i class="bi bi-pencil-square"></i> Edit
|
|
434
|
+
</button>
|
|
435
|
+
<button class="tg-btn" id="btn-mode-nav" onclick="setMode('navigate')" title="Navigate mode \u2014 interact with page normally">
|
|
436
|
+
<i class="bi bi-cursor"></i> Navigate
|
|
437
|
+
</button>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div class="tb-sep"></div>
|
|
441
|
+
|
|
442
|
+
<!-- Device width -->
|
|
443
|
+
<div class="tg">
|
|
444
|
+
<button class="tg-btn icon-only active" id="dev-desktop" onclick="setDevice('desktop')" title="Desktop \u2014 full width">
|
|
445
|
+
<i class="bi bi-display"></i>
|
|
446
|
+
</button>
|
|
447
|
+
<button class="tg-btn icon-only" id="dev-tablet" onclick="setDevice('tablet')" title="Tablet \u2014 768 px">
|
|
448
|
+
<i class="bi bi-tablet"></i>
|
|
449
|
+
</button>
|
|
450
|
+
<button class="tg-btn icon-only" id="dev-mobile" onclick="setDevice('mobile')" title="Mobile \u2014 390 px">
|
|
451
|
+
<i class="bi bi-phone"></i>
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
<span id="dev-label" style="font-size:11px;color:var(--text-3);min-width:56px">Desktop</span>
|
|
455
|
+
|
|
456
|
+
<div class="tb-sep"></div>
|
|
457
|
+
<div id="url-bar" title="">No page loaded</div>
|
|
458
|
+
<div class="tb-space"></div>
|
|
459
|
+
|
|
460
|
+
<!-- Undo / Redo -->
|
|
461
|
+
<button class="tb-btn icon" id="btn-undo" title="Undo (\u2318Z)"><i class="bi bi-arrow-counterclockwise"></i></button>
|
|
462
|
+
<button class="tb-btn icon" id="btn-redo" title="Redo (\u2318\u21E7Z)"><i class="bi bi-arrow-clockwise"></i></button>
|
|
463
|
+
<div class="tb-sep"></div>
|
|
464
|
+
|
|
465
|
+
<button class="tb-btn primary" id="btn-save"><i class="bi bi-floppy"></i> Save</button>
|
|
466
|
+
<button class="tb-btn icon close-btn" id="btn-close" title="Close editor"><i class="bi bi-x-lg"></i></button>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<!-- \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->
|
|
470
|
+
<div id="main">
|
|
471
|
+
|
|
472
|
+
<!-- Left panel -->
|
|
473
|
+
<div id="left-panel">
|
|
474
|
+
|
|
475
|
+
<!-- Variations -->
|
|
476
|
+
<div class="lp-sec">
|
|
477
|
+
<div class="lp-sec-hd">
|
|
478
|
+
Variations
|
|
479
|
+
<span id="active-var-label"></span>
|
|
480
|
+
</div>
|
|
481
|
+
<div id="variation-tabs"></div>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<!-- Search -->
|
|
485
|
+
<div style="padding:8px;flex-shrink:0">
|
|
486
|
+
<input type="search" id="comp-search" placeholder="Search components…" autocomplete="off">
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Tabs -->
|
|
490
|
+
<div class="lp-tabs" style="flex-shrink:0">
|
|
491
|
+
<div class="lp-tab active" onclick="switchLeftTab('components')">Components</div>
|
|
492
|
+
<div class="lp-tab" onclick="switchLeftTab('sections')">Sections</div>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<!-- Tab content -->
|
|
496
|
+
<div class="lp-body">
|
|
497
|
+
<div id="tab-components" class="tab-pane active"></div>
|
|
498
|
+
<div id="tab-sections" class="tab-pane"></div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
</div><!-- #left-panel -->
|
|
502
|
+
|
|
503
|
+
<!-- Center / iframe panel -->
|
|
504
|
+
<div id="iframe-panel">
|
|
505
|
+
|
|
506
|
+
<!-- Loading overlay (absolute, covers center) -->
|
|
507
|
+
<div id="loading">
|
|
508
|
+
<div class="spinner"></div>
|
|
509
|
+
<div id="load-status" style="font-size:12px;color:var(--text-3)">Initialising…</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<!-- No-URL overlay (absolute, covers center) -->
|
|
513
|
+
<div id="no-url">
|
|
514
|
+
<div class="nu-icon"><i class="bi bi-globe"></i></div>
|
|
515
|
+
<div style="font-weight:600;font-size:14px;color:var(--text-2)">No page loaded</div>
|
|
516
|
+
<div style="font-size:12px;color:var(--text-3)">Waiting for experiment data…</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<!-- Device frame containing the editing iframe -->
|
|
520
|
+
<div id="device-frame">
|
|
521
|
+
<iframe id="iframeId" name="iframeId" allowfullscreen></iframe>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
</div><!-- #iframe-panel -->
|
|
525
|
+
|
|
526
|
+
<!-- Right panel -->
|
|
527
|
+
<div id="right-panel">
|
|
528
|
+
|
|
529
|
+
<!-- Element badge (hidden until selection) -->
|
|
530
|
+
<div id="el-info" style="display:none">
|
|
531
|
+
<div id="el-info-tag"></div>
|
|
532
|
+
<div id="el-info-sel"></div>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<!-- \u2500\u2500 3 main tabs \u2500\u2500 -->
|
|
536
|
+
<div id="main-tabs">
|
|
537
|
+
<button class="main-tab active" onclick="switchMainTab('design')">Design</button>
|
|
538
|
+
<button class="main-tab" onclick="switchMainTab('states')">States</button>
|
|
539
|
+
<button class="main-tab" onclick="switchMainTab('history')">History</button>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<!-- \u2500\u2500 Design pane (all accordion sections) \u2500\u2500 -->
|
|
543
|
+
<div id="tab-design" class="rp-pane active">
|
|
544
|
+
<div id="no-sel">
|
|
545
|
+
<div class="ns-icon"><i class="bi bi-cursor-text"></i></div>
|
|
546
|
+
Click any element on the page to edit its properties
|
|
547
|
+
</div>
|
|
548
|
+
<div id="rp-accordion" style="display:none">
|
|
549
|
+
|
|
550
|
+
<!-- Image section \u2014 only shown when an <img> is selected -->
|
|
551
|
+
<div class="acc-section open" id="acc-image" style="display:none">
|
|
552
|
+
<div class="acc-hd" onclick="toggleAcc('image')">Image<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
553
|
+
<div class="acc-body" id="acc-body-image"></div>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div class="acc-section open" id="acc-content">
|
|
557
|
+
<div class="acc-hd" onclick="toggleAcc('content')">HTML Content<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
558
|
+
<div class="acc-body" id="acc-body-content"></div>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<div class="acc-section" id="acc-typography">
|
|
562
|
+
<div class="acc-hd" onclick="toggleAcc('typography')">Typography<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
563
|
+
<div class="acc-body" id="acc-body-typography"></div>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<div class="acc-section" id="acc-background">
|
|
567
|
+
<div class="acc-hd" onclick="toggleAcc('background')">Background<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
568
|
+
<div class="acc-body" id="acc-body-background"></div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<div class="acc-section" id="acc-opacity">
|
|
572
|
+
<div class="acc-hd" onclick="toggleAcc('opacity')">Opacity<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
573
|
+
<div class="acc-body" id="acc-body-opacity"></div>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<div class="acc-section" id="acc-border">
|
|
577
|
+
<div class="acc-hd" onclick="toggleAcc('border')">Border<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
578
|
+
<div class="acc-body" id="acc-body-border"></div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<div class="acc-section" id="acc-shadow">
|
|
582
|
+
<div class="acc-hd" onclick="toggleAcc('shadow')">Shadow<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
583
|
+
<div class="acc-body" id="acc-body-shadow"></div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div class="acc-section" id="acc-spacing">
|
|
587
|
+
<div class="acc-hd" onclick="toggleAcc('spacing')">Spacing<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
588
|
+
<div class="acc-body" id="acc-body-spacing"></div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div class="acc-section" id="acc-size">
|
|
592
|
+
<div class="acc-hd" onclick="toggleAcc('size')">Size & Position<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
593
|
+
<div class="acc-body" id="acc-body-size"></div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div class="acc-section" id="acc-device">
|
|
597
|
+
<div class="acc-hd" onclick="toggleAcc('device')">Device styling rules<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
598
|
+
<div class="acc-body" id="acc-body-device"></div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div class="acc-section" id="acc-css">
|
|
602
|
+
<div class="acc-hd" onclick="toggleAcc('css')">CSS and Classes<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
603
|
+
<div class="acc-body" id="acc-body-css"></div>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<div class="acc-section" id="acc-attributes">
|
|
607
|
+
<div class="acc-hd" onclick="toggleAcc('attributes')">Attributes<i class="bi bi-chevron-right acc-arrow"></i></div>
|
|
608
|
+
<div class="acc-body" id="acc-body-attributes"></div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
</div><!-- #rp-accordion -->
|
|
612
|
+
</div><!-- #tab-design -->
|
|
613
|
+
|
|
614
|
+
<!-- \u2500\u2500 States pane \u2500\u2500 -->
|
|
615
|
+
<div id="tab-states" class="rp-pane">
|
|
616
|
+
<div id="states-list">
|
|
617
|
+
<div class="states-empty">
|
|
618
|
+
<i class="bi bi-layers"></i>
|
|
619
|
+
No changes yet \u2014 edit elements on the page to see states here
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div><!-- #tab-states -->
|
|
623
|
+
|
|
624
|
+
<!-- \u2500\u2500 History pane (future) \u2500\u2500 -->
|
|
625
|
+
<div id="tab-history" class="rp-pane">
|
|
626
|
+
<div class="tab-ph">
|
|
627
|
+
<i class="bi bi-clock-history"></i>
|
|
628
|
+
<strong style="display:block;font-size:13px;color:var(--text-2)">History</strong>
|
|
629
|
+
<p>Full edit history with time-travel undo is coming in a future release.</p>
|
|
630
|
+
</div>
|
|
631
|
+
</div><!-- #tab-history -->
|
|
632
|
+
|
|
633
|
+
</div><!-- #right-panel -->
|
|
634
|
+
|
|
635
|
+
</div><!-- #main -->
|
|
636
|
+
|
|
637
|
+
<!-- Breadcrumb -->
|
|
638
|
+
<div id="breadcrumb">
|
|
639
|
+
<i class="bi bi-code-slash" style="opacity:.4"></i>
|
|
640
|
+
<span id="bc-path" style="color:var(--text-3)">No element selected</span>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
</div><!-- #app -->
|
|
644
|
+
|
|
645
|
+
<!-- CDN scripts -->
|
|
646
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
647
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/builder.js"></script>
|
|
648
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/undo.js"></script>
|
|
649
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/inputs.js"></script>
|
|
650
|
+
<!-- components.js defines shared colour-class arrays used by bootstrap5/widgets components -->
|
|
651
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components.js"></script>
|
|
652
|
+
<script>
|
|
653
|
+
/* Safety stub: if components.js didn't define these, create empty arrays so
|
|
654
|
+
components-bootstrap5.js doesn't throw ReferenceError on load. */
|
|
655
|
+
if (typeof bgcolorClasses === 'undefined') window.bgcolorClasses = [];
|
|
656
|
+
if (typeof colorClasses === 'undefined') window.colorClasses = [];
|
|
657
|
+
if (typeof textColorClasses=== 'undefined') window.textColorClasses= [];
|
|
658
|
+
if (typeof borderClasses === 'undefined') window.borderClasses = [];
|
|
659
|
+
if (typeof sizeClasses === 'undefined') window.sizeClasses = [];
|
|
660
|
+
</script>
|
|
661
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-bootstrap5.js"></script>
|
|
662
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-widgets.js"></script>
|
|
663
|
+
|
|
664
|
+
<script>
|
|
665
|
+
'use strict';
|
|
666
|
+
|
|
667
|
+
// \u2500\u2500 CRO definitions + Base components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
668
|
+
var CRO_SECTIONS = ${sectionsJson};
|
|
669
|
+
|
|
670
|
+
var BASE_COMPONENTS = [
|
|
671
|
+
{ name:'Heading', icon:'bi-type-h1', html:'<h2>Your Heading</h2>' },
|
|
672
|
+
{ name:'Paragraph', icon:'bi-paragraph', html:'<p>Your text here. Click to edit.</p>' },
|
|
673
|
+
{ name:'Image', icon:'bi-image', html:'<img src="https://placehold.co/600x300/1a1a2e/a78bfa?text=Image" alt="Image" style="max-width:100%">' },
|
|
674
|
+
{ name:'Button', icon:'bi-hand-index', html:'<button class="btn btn-primary">Click me</button>' },
|
|
675
|
+
{ name:'Link', icon:'bi-link-45deg', html:'<a href="#">Link text</a>' },
|
|
676
|
+
{ name:'Divider', icon:'bi-dash-lg', html:'<hr>' },
|
|
677
|
+
{ name:'List', icon:'bi-list-ul', html:'<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>' },
|
|
678
|
+
{ name:'Video', icon:'bi-play-circle', html:'<video controls style="max-width:100%"><source src="" type="video/mp4"></video>' },
|
|
679
|
+
{ name:'Form', icon:'bi-ui-radios', html:'<form class="p-3"><div class="mb-3"><label class="form-label">Name</label><input type="text" class="form-control"></div><div class="mb-3"><label class="form-label">Email</label><input type="email" class="form-control"></div><button type="submit" class="btn btn-primary">Submit</button></form>' },
|
|
680
|
+
{ name:'Input', icon:'bi-input-cursor-text', html:'<input type="text" class="form-control" placeholder="Enter text">' },
|
|
681
|
+
{ name:'Textarea', icon:'bi-textarea-resize', html:'<textarea class="form-control" rows="3" placeholder="Enter text"></textarea>' },
|
|
682
|
+
{ name:'Select', icon:'bi-menu-button-wide', html:'<select class="form-select"><option>Option 1</option><option>Option 2</option></select>' },
|
|
683
|
+
{ name:'Card', icon:'bi-card-text', html:'<div class="card"><div class="card-body"><h5 class="card-title">Card Title</h5><p class="card-text">Card content here.</p><a href="#" class="btn btn-primary">Action</a></div></div>' },
|
|
684
|
+
{ name:'Alert', icon:'bi-exclamation-triangle', html:'<div class="alert alert-info" role="alert">This is an alert message.</div>' },
|
|
685
|
+
{ name:'Badge', icon:'bi-tag', html:'<span class="badge bg-primary">New</span>' },
|
|
686
|
+
{ name:'Container', icon:'bi-layout-three-columns', html:'<div class="container py-4"><div class="row g-3"><div class="col"><p>Column 1</p></div><div class="col"><p>Column 2</p></div><div class="col"><p>Column 3</p></div></div></div>' },
|
|
687
|
+
{ name:'2 Cols', icon:'bi-layout-split', html:'<div class="row g-3"><div class="col-md-6"><p>Left column</p></div><div class="col-md-6"><p>Right column</p></div></div>' },
|
|
688
|
+
{ name:'Navbar', icon:'bi-layout-navbar', html:'<nav class="navbar navbar-expand-lg navbar-dark bg-dark"><div class="container-fluid"><a class="navbar-brand" href="#">Brand</a></div></nav>' },
|
|
689
|
+
];
|
|
690
|
+
|
|
691
|
+
// \u2500\u2500 PostMessage bridge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
692
|
+
var CHANNEL = 'vvveb-bridge';
|
|
693
|
+
|
|
694
|
+
function send(type, payload) {
|
|
695
|
+
window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
window.addEventListener('message', function(e) {
|
|
699
|
+
if (!e.data || e.data.channel !== CHANNEL) return;
|
|
700
|
+
switch (e.data.type) {
|
|
701
|
+
case 'load-experiment': handleLoadExperiment(e.data.payload); break;
|
|
702
|
+
case 'close-editor': handleClose(); break;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// \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
|
|
707
|
+
var experimentData = null;
|
|
708
|
+
var variations = [];
|
|
709
|
+
var activeVarId = null;
|
|
710
|
+
var varHtmlCache = {};
|
|
711
|
+
var isDirty = false;
|
|
712
|
+
var vvvebReady = false;
|
|
713
|
+
var currentMode = 'editor';
|
|
714
|
+
var currentDevice = 'desktop';
|
|
715
|
+
var selectedEl = null;
|
|
716
|
+
var currentMainTab = 'design';
|
|
717
|
+
// Each entry: {selector, label, cssProp, value, targetEl}
|
|
718
|
+
// cssProp is null for non-CSS attributes (href, alt, classes\u2026)
|
|
719
|
+
var stateChanges = [];
|
|
720
|
+
|
|
721
|
+
// \u2500\u2500 Dirty tracking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
722
|
+
function markDirty() {
|
|
723
|
+
isDirty = true;
|
|
724
|
+
document.getElementById('dirty-dot').classList.add('on');
|
|
725
|
+
send('mutations-changed', {});
|
|
726
|
+
}
|
|
727
|
+
function markClean() {
|
|
728
|
+
isDirty = false;
|
|
729
|
+
document.getElementById('dirty-dot').classList.remove('on');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// \u2500\u2500 Mode toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
733
|
+
function setMode(mode) {
|
|
734
|
+
currentMode = mode;
|
|
735
|
+
document.body.className = 'mode-' + mode;
|
|
736
|
+
document.getElementById('btn-mode-editor').classList.toggle('active', mode === 'editor');
|
|
737
|
+
document.getElementById('btn-mode-nav').classList.toggle('active', mode === 'navigate');
|
|
738
|
+
if (mode === 'navigate') deselectElement();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// \u2500\u2500 Device toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
742
|
+
var DEVICE_LABELS = { desktop: 'Desktop', tablet: 'Tablet \xB7 768px', mobile: 'Mobile \xB7 390px' };
|
|
743
|
+
function setDevice(device) {
|
|
744
|
+
currentDevice = device;
|
|
745
|
+
var frame = document.getElementById('device-frame');
|
|
746
|
+
frame.className = device === 'desktop' ? '' : device;
|
|
747
|
+
['desktop','tablet','mobile'].forEach(function(d) {
|
|
748
|
+
document.getElementById('dev-' + d).classList.toggle('active', d === device);
|
|
749
|
+
});
|
|
750
|
+
document.getElementById('dev-label').textContent = DEVICE_LABELS[device] || device;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// \u2500\u2500 Left panel tab switch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
754
|
+
function switchLeftTab(tab) {
|
|
755
|
+
document.querySelectorAll('.lp-tab').forEach(function(t, i) {
|
|
756
|
+
t.classList.toggle('active', (i === 0 && tab === 'components') || (i === 1 && tab === 'sections'));
|
|
757
|
+
});
|
|
758
|
+
document.getElementById('tab-components').classList.toggle('active', tab === 'components');
|
|
759
|
+
document.getElementById('tab-sections').classList.toggle('active', tab === 'sections');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// \u2500\u2500 Accordion toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
763
|
+
function toggleAcc(name) {
|
|
764
|
+
var sec = document.getElementById('acc-' + name);
|
|
765
|
+
if (sec) sec.classList.toggle('open');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// \u2500\u2500 Main tab switching (Design / States / History) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
769
|
+
function switchMainTab(tab) {
|
|
770
|
+
currentMainTab = tab;
|
|
771
|
+
document.querySelectorAll('.main-tab').forEach(function(btn, i) {
|
|
772
|
+
btn.classList.toggle('active', ['design','states','history'][i] === tab);
|
|
773
|
+
});
|
|
774
|
+
['design','states','history'].forEach(function(t) {
|
|
775
|
+
var pane = document.getElementById('tab-' + t);
|
|
776
|
+
if (pane) pane.classList.toggle('active', t === tab);
|
|
777
|
+
});
|
|
778
|
+
if (tab === 'states') renderStatesTab();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// \u2500\u2500 Change log (States 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
|
|
782
|
+
// Map of input-id \u2192 {label, cssProp} used by logChange
|
|
783
|
+
var PROP_META = {
|
|
784
|
+
'pp-color': {label:'Text color', cssProp:'color'},
|
|
785
|
+
'pp-fs': {label:'Font size', cssProp:'font-size'},
|
|
786
|
+
'pp-fw': {label:'Font weight', cssProp:'font-weight'},
|
|
787
|
+
'pp-ff': {label:'Font family', cssProp:'font-family'},
|
|
788
|
+
'pp-ta': {label:'Text align', cssProp:'text-align'},
|
|
789
|
+
'pp-lh': {label:'Line height', cssProp:'line-height'},
|
|
790
|
+
'pp-ls': {label:'Letter spacing', cssProp:'letter-spacing'},
|
|
791
|
+
'pp-td': {label:'Text decoration', cssProp:'text-decoration'},
|
|
792
|
+
'pp-bg': {label:'Background color', cssProp:'background-color'},
|
|
793
|
+
'pp-bgi': {label:'Background image', cssProp:'background-image'},
|
|
794
|
+
'pp-bgs': {label:'Background size', cssProp:'background-size'},
|
|
795
|
+
'pp-bgr': {label:'Background repeat', cssProp:'background-repeat'},
|
|
796
|
+
'pp-op': {label:'Opacity', cssProp:'opacity'},
|
|
797
|
+
'pp-op-range':{label:'Opacity', cssProp:'opacity'},
|
|
798
|
+
'pp-bst': {label:'Border style', cssProp:'border-style'},
|
|
799
|
+
'pp-bw': {label:'Border width', cssProp:'border-width'},
|
|
800
|
+
'pp-bc': {label:'Border color', cssProp:'border-color'},
|
|
801
|
+
'pp-brtl': {label:'Radius top-left', cssProp:'border-top-left-radius'},
|
|
802
|
+
'pp-brtr': {label:'Radius top-right', cssProp:'border-top-right-radius'},
|
|
803
|
+
'pp-brbl': {label:'Radius bottom-left', cssProp:'border-bottom-left-radius'},
|
|
804
|
+
'pp-brbr': {label:'Radius bottom-right', cssProp:'border-bottom-right-radius'},
|
|
805
|
+
'pp-bs': {label:'Box shadow', cssProp:'box-shadow'},
|
|
806
|
+
'pp-mt': {label:'Margin top', cssProp:'margin-top'},
|
|
807
|
+
'pp-mr': {label:'Margin right', cssProp:'margin-right'},
|
|
808
|
+
'pp-mb': {label:'Margin bottom', cssProp:'margin-bottom'},
|
|
809
|
+
'pp-ml': {label:'Margin left', cssProp:'margin-left'},
|
|
810
|
+
'pp-pt': {label:'Padding top', cssProp:'padding-top'},
|
|
811
|
+
'pp-pr': {label:'Padding right', cssProp:'padding-right'},
|
|
812
|
+
'pp-pb': {label:'Padding bottom', cssProp:'padding-bottom'},
|
|
813
|
+
'pp-pl': {label:'Padding left', cssProp:'padding-left'},
|
|
814
|
+
'pp-w': {label:'Width', cssProp:'width'},
|
|
815
|
+
'pp-h': {label:'Height', cssProp:'height'},
|
|
816
|
+
'pp-minw': {label:'Min width', cssProp:'min-width'},
|
|
817
|
+
'pp-minh': {label:'Min height', cssProp:'min-height'},
|
|
818
|
+
'pp-maxw': {label:'Max width', cssProp:'max-width'},
|
|
819
|
+
'pp-maxh': {label:'Max height', cssProp:'max-height'},
|
|
820
|
+
'pp-disp': {label:'Display', cssProp:'display'},
|
|
821
|
+
'pp-pos': {label:'Position', cssProp:'position'},
|
|
822
|
+
'pp-top': {label:'Top', cssProp:'top'},
|
|
823
|
+
'pp-left': {label:'Left', cssProp:'left'},
|
|
824
|
+
'pp-bottom': {label:'Bottom', cssProp:'bottom'},
|
|
825
|
+
'pp-right': {label:'Right', cssProp:'right'},
|
|
826
|
+
'pp-cls': {label:'Classes', cssProp:null},
|
|
827
|
+
'pp-css': {label:'Custom CSS', cssProp:null},
|
|
828
|
+
'pp-id': {label:'ID', cssProp:null},
|
|
829
|
+
'pp-href': {label:'Href', cssProp:null},
|
|
830
|
+
'pp-target': {label:'Target', cssProp:null},
|
|
831
|
+
'pp-src': {label:'Src', cssProp:null},
|
|
832
|
+
'pp-alt': {label:'Alt', cssProp:null},
|
|
833
|
+
'pp-ph': {label:'Placeholder', cssProp:null},
|
|
834
|
+
'pp-text': {label:'Inner text', cssProp:null},
|
|
835
|
+
'pp-html': {label:'Inner HTML', cssProp:null},
|
|
836
|
+
'pp-mob-css': {label:'Mobile CSS', cssProp:null},
|
|
837
|
+
'pp-tab-css': {label:'Tablet CSS', cssProp:null},
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// Read the element's CURRENT value for a given inputId (called BEFORE the change is applied).
|
|
841
|
+
function getOriginalValue(inputId, el) {
|
|
842
|
+
var meta = PROP_META[inputId];
|
|
843
|
+
if (!meta) return '';
|
|
844
|
+
if (meta.cssProp) {
|
|
845
|
+
// Inline style value (may be empty string if not set)
|
|
846
|
+
var camel = meta.cssProp.replace(/-([a-z])/g, function(_, l){ return l.toUpperCase(); });
|
|
847
|
+
return el.style[camel] || '';
|
|
848
|
+
}
|
|
849
|
+
switch (inputId) {
|
|
850
|
+
case 'pp-text': return el.innerText || '';
|
|
851
|
+
case 'pp-html': return el.innerHTML || '';
|
|
852
|
+
case 'pp-cls': return el.className || '';
|
|
853
|
+
case 'pp-id': return el.id || '';
|
|
854
|
+
case 'pp-href': return el.getAttribute('href') || '';
|
|
855
|
+
case 'pp-target': return el.getAttribute('target') || '';
|
|
856
|
+
case 'pp-src': return el.getAttribute('src') || '';
|
|
857
|
+
case 'pp-alt': return el.getAttribute('alt') || '';
|
|
858
|
+
case 'pp-ph': return el.getAttribute('placeholder') || '';
|
|
859
|
+
case 'pp-css': return el.getAttribute('style') || '';
|
|
860
|
+
case 'pp-mob-css': return el.dataset.mobileCss || '';
|
|
861
|
+
case 'pp-tab-css': return el.dataset.tabletCss || '';
|
|
862
|
+
default: return '';
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function logChange(selector, inputId, value, targetEl, originalValue) {
|
|
867
|
+
var meta = PROP_META[inputId];
|
|
868
|
+
if (!meta) return;
|
|
869
|
+
var key = selector + '||' + inputId;
|
|
870
|
+
// Skip trivially empty / reset values \u2014 remove from log if present
|
|
871
|
+
if (value === '' || value === 'none' || value === 'auto' || value === 'normal') {
|
|
872
|
+
stateChanges = stateChanges.filter(function(c) {
|
|
873
|
+
return !(c.selector === selector && c.inputId === inputId);
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
var idx = stateChanges.findIndex(function(c) { return (c.selector + '||' + c.inputId) === key; });
|
|
877
|
+
// Preserve originalValue from the first entry \u2014 don't overwrite on subsequent edits
|
|
878
|
+
var orig = (idx >= 0 && stateChanges[idx].originalValue != null)
|
|
879
|
+
? stateChanges[idx].originalValue
|
|
880
|
+
: (originalValue != null ? originalValue : '');
|
|
881
|
+
var entry = {
|
|
882
|
+
selector: selector, inputId: inputId, label: meta.label,
|
|
883
|
+
cssProp: meta.cssProp, value: value, targetEl: targetEl, originalValue: orig
|
|
884
|
+
};
|
|
885
|
+
if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
|
|
886
|
+
}
|
|
887
|
+
if (currentMainTab === 'states') renderStatesTab();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function renderStatesTab() {
|
|
891
|
+
var container = document.getElementById('states-list');
|
|
892
|
+
if (!stateChanges.length) {
|
|
893
|
+
container.innerHTML = '<div class="states-empty"><i class="bi bi-layers"></i>No changes yet \u2014 edit elements on the page to see states here</div>';
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// Group by selector
|
|
897
|
+
var groups = {};
|
|
898
|
+
var order = [];
|
|
899
|
+
stateChanges.forEach(function(c) {
|
|
900
|
+
if (!groups[c.selector]) { groups[c.selector] = []; order.push(c.selector); }
|
|
901
|
+
groups[c.selector].push(c);
|
|
902
|
+
});
|
|
903
|
+
var html = '<button id="states-clear" onclick="clearAllStates()"><i class="bi bi-trash3"></i> Clear all changes</button>';
|
|
904
|
+
order.forEach(function(sel) {
|
|
905
|
+
html += '<div class="state-group"><div class="state-group-sel">'+esc(sel)+'</div>';
|
|
906
|
+
groups[sel].forEach(function(c) {
|
|
907
|
+
var idx = stateChanges.indexOf(c);
|
|
908
|
+
html += '<div class="state-item">' +
|
|
909
|
+
'<span class="state-item-label">'+esc(c.label)+'</span>' +
|
|
910
|
+
'<span class="state-item-val" title="'+esc(c.value)+'">'+esc(c.value)+'</span>' +
|
|
911
|
+
'<button class="state-remove" title="Remove this change" onclick="removeStateChange('+idx+')">✕</button>' +
|
|
912
|
+
'</div>';
|
|
913
|
+
});
|
|
914
|
+
html += '</div>';
|
|
915
|
+
});
|
|
916
|
+
container.innerHTML = html;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Resolve a live DOM element for a state-change entry.
|
|
920
|
+
// Tries the stored direct reference first; if it's detached or missing,
|
|
921
|
+
// falls back to querySelector(selector) inside the iframe document.
|
|
922
|
+
function resolveChangeEl(change) {
|
|
923
|
+
try {
|
|
924
|
+
var iframeDoc = document.getElementById('iframeId').contentDocument;
|
|
925
|
+
if (!iframeDoc) return null;
|
|
926
|
+
// Prefer the direct reference when still attached to the iframe document
|
|
927
|
+
if (change.targetEl && iframeDoc.contains(change.targetEl)) {
|
|
928
|
+
return change.targetEl;
|
|
929
|
+
}
|
|
930
|
+
// Fallback: re-query by the stored CSS selector
|
|
931
|
+
return iframeDoc.querySelector(change.selector) || null;
|
|
932
|
+
} catch (e) {
|
|
933
|
+
console.warn('[V2] resolveChangeEl:', e);
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function revertChangeOnDom(change) {
|
|
939
|
+
var el = resolveChangeEl(change);
|
|
940
|
+
if (!el) {
|
|
941
|
+
console.warn('[V2] revertChangeOnDom: element not found for selector:', change.selector);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
var orig = change.originalValue != null ? change.originalValue : '';
|
|
945
|
+
|
|
946
|
+
if (change.cssProp) {
|
|
947
|
+
// CSS inline style \u2014 remove the property so the stylesheet takes over
|
|
948
|
+
el.style.removeProperty(change.cssProp);
|
|
949
|
+
// Also clear camelCase alias to handle shorthand setters
|
|
950
|
+
try {
|
|
951
|
+
var camel = change.cssProp.replace(/-([a-z])/g, function(_, l){ return l.toUpperCase(); });
|
|
952
|
+
if (camel in el.style) el.style[camel] = '';
|
|
953
|
+
} catch(_) {}
|
|
954
|
+
} else {
|
|
955
|
+
// Non-CSS attribute / content \u2014 restore the original value
|
|
956
|
+
switch (change.inputId) {
|
|
957
|
+
case 'pp-text': el.innerText = orig; break;
|
|
958
|
+
case 'pp-html': el.innerHTML = orig; break;
|
|
959
|
+
case 'pp-cls': el.className = orig; break;
|
|
960
|
+
case 'pp-id': el.id = orig; break;
|
|
961
|
+
case 'pp-css': orig ? el.setAttribute('style', orig) : el.removeAttribute('style'); break;
|
|
962
|
+
case 'pp-href': orig ? el.setAttribute('href', orig) : el.removeAttribute('href'); break;
|
|
963
|
+
case 'pp-target': orig ? el.setAttribute('target', orig) : el.removeAttribute('target'); break;
|
|
964
|
+
case 'pp-src': orig ? el.setAttribute('src', orig) : el.removeAttribute('src'); break;
|
|
965
|
+
case 'pp-alt': orig ? el.setAttribute('alt', orig) : el.removeAttribute('alt'); break;
|
|
966
|
+
case 'pp-ph': orig ? el.setAttribute('placeholder', orig) : el.removeAttribute('placeholder'); break;
|
|
967
|
+
case 'pp-mob-css': el.dataset.mobileCss = orig; break;
|
|
968
|
+
case 'pp-tab-css': el.dataset.tabletCss = orig; break;
|
|
969
|
+
default: console.warn('[V2] revertChangeOnDom: no revert handler for', change.inputId);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// After reverting a change, sync the Design-tab input back to the original value.
|
|
975
|
+
// Also handles linked inputs (opacity slider \u2194 number, etc.).
|
|
976
|
+
function syncDesignInput(change) {
|
|
977
|
+
var orig = change.originalValue != null ? change.originalValue : '';
|
|
978
|
+
var inp = document.getElementById(change.inputId);
|
|
979
|
+
if (inp) inp.value = orig;
|
|
980
|
+
// Keep paired opacity inputs in sync
|
|
981
|
+
if (change.inputId === 'pp-op' || change.inputId === 'pp-op-range') {
|
|
982
|
+
var paired = document.getElementById(change.inputId === 'pp-op' ? 'pp-op-range' : 'pp-op');
|
|
983
|
+
if (paired) paired.value = orig;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function removeStateChange(idx) {
|
|
988
|
+
var change = stateChanges[idx];
|
|
989
|
+
if (!change) return;
|
|
990
|
+
revertChangeOnDom(change);
|
|
991
|
+
syncDesignInput(change);
|
|
992
|
+
stateChanges.splice(idx, 1);
|
|
993
|
+
renderStatesTab();
|
|
994
|
+
markDirty();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function clearAllStates() {
|
|
998
|
+
stateChanges.forEach(function(c) {
|
|
999
|
+
revertChangeOnDom(c);
|
|
1000
|
+
syncDesignInput(c);
|
|
1001
|
+
});
|
|
1002
|
+
stateChanges = [];
|
|
1003
|
+
renderStatesTab();
|
|
1004
|
+
markDirty();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// \u2500\u2500 Experiment loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1008
|
+
function handleLoadExperiment(data) {
|
|
1009
|
+
experimentData = data;
|
|
1010
|
+
variations = Array.isArray(data.variations) ? data.variations : [];
|
|
1011
|
+
var firstNonControl = variations.find(function(v) { return !v.baseline; });
|
|
1012
|
+
activeVarId = (firstNonControl || variations[0] || {})._id || null;
|
|
1013
|
+
renderVariationTabs();
|
|
1014
|
+
|
|
1015
|
+
var pageUrl = data.pageUrl || '';
|
|
1016
|
+
if (!pageUrl) {
|
|
1017
|
+
document.getElementById('loading').classList.add('hidden');
|
|
1018
|
+
showNoUrl(true);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
var urlBar = document.getElementById('url-bar');
|
|
1022
|
+
urlBar.textContent = pageUrl;
|
|
1023
|
+
urlBar.title = pageUrl;
|
|
1024
|
+
var proxyUrl = '/api/proxy?password=' + encodeURIComponent(data.editorPassword || '') +
|
|
1025
|
+
'&url=' + encodeURIComponent(pageUrl);
|
|
1026
|
+
loadPage(proxyUrl);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function showNoUrl(show) {
|
|
1030
|
+
document.getElementById('no-url').style.display = show ? 'flex' : 'none';
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function loadPage(proxyUrl) {
|
|
1034
|
+
showNoUrl(false);
|
|
1035
|
+
var loading = document.getElementById('loading');
|
|
1036
|
+
loading.classList.remove('hidden');
|
|
1037
|
+
document.getElementById('load-status').textContent = 'Loading page\u2026';
|
|
1038
|
+
var iframe = document.getElementById('iframeId');
|
|
1039
|
+
iframe.style.display = 'block';
|
|
1040
|
+
iframe.src = proxyUrl;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// \u2500\u2500 Variation management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1044
|
+
function renderVariationTabs() {
|
|
1045
|
+
var container = document.getElementById('variation-tabs');
|
|
1046
|
+
container.innerHTML = '';
|
|
1047
|
+
variations.forEach(function(v) {
|
|
1048
|
+
var label = v.name || (v.baseline ? 'Control' : 'Variation');
|
|
1049
|
+
var btn = document.createElement('button');
|
|
1050
|
+
btn.className = 'var-tab' + (v._id === activeVarId ? ' active' : '');
|
|
1051
|
+
btn.textContent = label;
|
|
1052
|
+
btn.title = label;
|
|
1053
|
+
btn.onclick = function() { switchVariation(v._id); };
|
|
1054
|
+
container.appendChild(btn);
|
|
1055
|
+
});
|
|
1056
|
+
var active = variations.find(function(v) { return v._id === activeVarId; });
|
|
1057
|
+
document.getElementById('active-var-label').textContent = active ? active.name || '' : '';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function switchVariation(varId) {
|
|
1061
|
+
if (varId === activeVarId) return;
|
|
1062
|
+
saveCurrentVariationHtml();
|
|
1063
|
+
activeVarId = varId;
|
|
1064
|
+
renderVariationTabs();
|
|
1065
|
+
deselectElement();
|
|
1066
|
+
try {
|
|
1067
|
+
var iframe = document.getElementById('iframeId');
|
|
1068
|
+
var saved = varHtmlCache[varId];
|
|
1069
|
+
if (saved) {
|
|
1070
|
+
iframe.contentDocument.body.innerHTML = saved;
|
|
1071
|
+
attachClickHandler();
|
|
1072
|
+
attachChangeObserver();
|
|
1073
|
+
} else {
|
|
1074
|
+
var loading = document.getElementById('loading');
|
|
1075
|
+
loading.classList.remove('hidden');
|
|
1076
|
+
document.getElementById('load-status').textContent = 'Switching variation\u2026';
|
|
1077
|
+
var src = iframe.src;
|
|
1078
|
+
iframe.src = '';
|
|
1079
|
+
iframe.src = src;
|
|
1080
|
+
}
|
|
1081
|
+
} catch(_) {}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function saveCurrentVariationHtml() {
|
|
1085
|
+
if (!activeVarId) return;
|
|
1086
|
+
try {
|
|
1087
|
+
varHtmlCache[activeVarId] = document.getElementById('iframeId').contentDocument.body.innerHTML;
|
|
1088
|
+
} catch(_) {}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** Convert a kebab-case CSS property to camelCase for el.style access */
|
|
1092
|
+
function camelize(str) {
|
|
1093
|
+
return (str || '').replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Apply a single changeset entry to an element inside the editor iframe.
|
|
1098
|
+
* Handles the Conversion.io backend format:
|
|
1099
|
+
* { selector, type: 'content'|'style'|'attribute'|'insert'|'remove', ... }
|
|
1100
|
+
* and the V2 full-body snapshot:
|
|
1101
|
+
* { selector: '__vvveb_body__', html: '<full body html>' }
|
|
1102
|
+
*/
|
|
1103
|
+
function applyChangesetEntry(entry, iframeDoc) {
|
|
1104
|
+
if (!entry || !entry.selector) return;
|
|
1105
|
+
|
|
1106
|
+
// \u2500\u2500 V2 full-body snapshot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1107
|
+
if (entry.selector === '__vvveb_body__' && entry.html != null) {
|
|
1108
|
+
iframeDoc.body.innerHTML = entry.html;
|
|
1109
|
+
varHtmlCache[activeVarId] = entry.html;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// \u2500\u2500 Standard granular changeset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1114
|
+
var el = null;
|
|
1115
|
+
try { el = iframeDoc.querySelector(entry.selector); } catch(_) {}
|
|
1116
|
+
if (!el) return;
|
|
1117
|
+
|
|
1118
|
+
switch (entry.type) {
|
|
1119
|
+
case 'content':
|
|
1120
|
+
if (entry.html != null) el.innerHTML = entry.html;
|
|
1121
|
+
else if (entry.value != null) el.textContent = entry.value;
|
|
1122
|
+
break;
|
|
1123
|
+
case 'style':
|
|
1124
|
+
if (entry.property && entry.value != null) {
|
|
1125
|
+
el.style[camelize(entry.property)] = entry.value;
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
case 'attribute':
|
|
1129
|
+
if (entry.attribute && entry.value != null) {
|
|
1130
|
+
el.setAttribute(entry.attribute, entry.value);
|
|
1131
|
+
}
|
|
1132
|
+
break;
|
|
1133
|
+
case 'insert': {
|
|
1134
|
+
var pos = entry.action === 'before' ? 'beforebegin' : 'afterend';
|
|
1135
|
+
if (entry.html) el.insertAdjacentHTML(pos, entry.html);
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
case 'remove':
|
|
1139
|
+
el.style.display = 'none';
|
|
1140
|
+
break;
|
|
1141
|
+
default: break;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function applyActiveVariationHtml() {
|
|
1146
|
+
if (!activeVarId) return;
|
|
1147
|
+
|
|
1148
|
+
// If we have an in-session HTML snapshot, use it (user edited in this session)
|
|
1149
|
+
var saved = varHtmlCache[activeVarId];
|
|
1150
|
+
if (saved) {
|
|
1151
|
+
try { document.getElementById('iframeId').contentDocument.body.innerHTML = saved; } catch(_) {}
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Apply pre-existing changesets from the loaded experiment
|
|
1156
|
+
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
1157
|
+
if (!variation || !variation.changesets) return;
|
|
1158
|
+
try {
|
|
1159
|
+
var cs = JSON.parse(variation.changesets);
|
|
1160
|
+
if (!Array.isArray(cs) || cs.length === 0) return;
|
|
1161
|
+
var iframeDoc = document.getElementById('iframeId').contentDocument;
|
|
1162
|
+
cs.forEach(function(entry) { applyChangesetEntry(entry, iframeDoc); });
|
|
1163
|
+
} catch(_) {}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// \u2500\u2500 Element selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1167
|
+
function selectElement(el) {
|
|
1168
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
|
|
1169
|
+
selectedEl = el;
|
|
1170
|
+
try { el.classList.add('vve-selected'); } catch(_) {}
|
|
1171
|
+
document.getElementById('bc-path').textContent = buildSelector(el);
|
|
1172
|
+
document.getElementById('bc-path').style.color = 'var(--accent-txt)';
|
|
1173
|
+
document.getElementById('no-sel').style.display = 'none';
|
|
1174
|
+
renderRightPanel(el);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function deselectElement() {
|
|
1178
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
|
|
1179
|
+
document.getElementById('no-sel').style.display = '';
|
|
1180
|
+
document.getElementById('el-info').style.display = 'none';
|
|
1181
|
+
document.getElementById('rp-accordion').style.display = 'none';
|
|
1182
|
+
document.getElementById('bc-path').textContent = 'No element selected';
|
|
1183
|
+
document.getElementById('bc-path').style.color = 'var(--text-3)';
|
|
1184
|
+
// Switch back to Design tab so no-sel message is visible
|
|
1185
|
+
switchMainTab('design');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// \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
|
|
1189
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1190
|
+
function rgbToHex(rgb) {
|
|
1191
|
+
var m = (rgb||'').match(/\\d+/g);
|
|
1192
|
+
if (!m || m.length < 3) return '#000000';
|
|
1193
|
+
return '#' + m.slice(0,3).map(function(n){ return ('0'+parseInt(n).toString(16)).slice(-2); }).join('');
|
|
1194
|
+
}
|
|
1195
|
+
function pr(label, inputHtml) {
|
|
1196
|
+
return '<div class="pr"><span class="pr-lbl">'+label+'</span>'+inputHtml+'</div>';
|
|
1197
|
+
}
|
|
1198
|
+
function pr2(l1, i1, l2, i2) {
|
|
1199
|
+
return '<div class="pr2"><div class="pr2-item"><div class="pr2-lbl">'+l1+'</div>'+i1+'</div>' +
|
|
1200
|
+
'<div class="pr2-item"><div class="pr2-lbl">'+l2+'</div>'+i2+'</div></div>';
|
|
1201
|
+
}
|
|
1202
|
+
function subLbl(text) { return '<div class="sub-lbl">'+text+'</div>'; }
|
|
1203
|
+
function weightOpts(cur) {
|
|
1204
|
+
return [['100','Thin'],['200','Extra Light'],['300','Light'],['400','Normal'],['500','Medium'],
|
|
1205
|
+
['600','Semi Bold'],['700','Bold'],['800','Extra Bold'],['900','Black']]
|
|
1206
|
+
.map(function(p){return '<option value="'+p[0]+'"'+(cur===p[0]?' selected':'')+'>'+p[1]+'</option>';}).join('');
|
|
1207
|
+
}
|
|
1208
|
+
function selOpts(arr, cur) {
|
|
1209
|
+
return arr.map(function(v){return '<option value="'+v+'"'+(cur===v?' selected':'')+'>'+v+'</option>';}).join('');
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// \u2500\u2500 Shadow preset helper (called from data-val buttons) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1213
|
+
function applyShadowPreset(val) {
|
|
1214
|
+
var inp = document.getElementById('pp-bs');
|
|
1215
|
+
if (!inp) return;
|
|
1216
|
+
inp.value = val;
|
|
1217
|
+
inp.dispatchEvent(new Event('input'));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// \u2500\u2500 Image section 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
|
|
1221
|
+
var _imageEl = null; // the <img> element currently being edited
|
|
1222
|
+
var _srcsetEntries = []; // [{url, descriptor}]
|
|
1223
|
+
var _sizesEntries = []; // [{condition, value}]
|
|
1224
|
+
|
|
1225
|
+
function parseSrcset(str) {
|
|
1226
|
+
if (!str) return [];
|
|
1227
|
+
return str.split(',').map(function(s) {
|
|
1228
|
+
var p = s.trim().split(/s+/);
|
|
1229
|
+
return {url: p[0] || '', descriptor: p[1] || ''};
|
|
1230
|
+
}).filter(function(e){ return e.url; });
|
|
1231
|
+
}
|
|
1232
|
+
function buildSrcset(entries) {
|
|
1233
|
+
return entries.map(function(e){ return e.descriptor ? e.url+' '+e.descriptor : e.url; }).join(', ');
|
|
1234
|
+
}
|
|
1235
|
+
function parseSizes(str) {
|
|
1236
|
+
if (!str) return [];
|
|
1237
|
+
return str.split(',').map(function(s) {
|
|
1238
|
+
s = s.trim();
|
|
1239
|
+
var m = s.match(/^(([^)]+))s+(.+)$/);
|
|
1240
|
+
return m ? {condition: m[1], value: m[2]} : {condition: '', value: s};
|
|
1241
|
+
}).filter(function(e){ return e.value; });
|
|
1242
|
+
}
|
|
1243
|
+
function buildSizes(entries) {
|
|
1244
|
+
return entries.map(function(e){ return e.condition ? '('+e.condition+') '+e.value : e.value; }).join(', ');
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function renderImageSection(el) {
|
|
1248
|
+
_imageEl = el;
|
|
1249
|
+
_srcsetEntries = parseSrcset(el.getAttribute('srcset'));
|
|
1250
|
+
_sizesEntries = parseSizes(el.getAttribute('sizes'));
|
|
1251
|
+
var sel = buildSelector(el);
|
|
1252
|
+
var html = '';
|
|
1253
|
+
|
|
1254
|
+
// Live preview thumbnail
|
|
1255
|
+
if (el.getAttribute('src')) {
|
|
1256
|
+
html += '<img class="img-preview" src="'+esc(el.getAttribute('src'))+'" alt="">';
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Src + Alt
|
|
1260
|
+
html += pr('Src', '<input class="pr-inp" id="pp-img-src" type="url" value="'+esc(el.getAttribute('src')||'')+'" placeholder="https://\u2026">');
|
|
1261
|
+
html += pr('Alt', '<input class="pr-inp" id="pp-img-alt" type="text" value="'+esc(el.getAttribute('alt')||'')+'">');
|
|
1262
|
+
|
|
1263
|
+
// Srcset entries
|
|
1264
|
+
html += subLbl('Srcset');
|
|
1265
|
+
_srcsetEntries.forEach(function(entry, i) {
|
|
1266
|
+
html += '<div class="img-row">' +
|
|
1267
|
+
'<input class="pr-inp" type="url" id="pp-se-url-'+i+'" value="'+esc(entry.url)+'" placeholder="https://img.jpg">' +
|
|
1268
|
+
'<input class="pr-inp img-desc" type="text" id="pp-se-desc-'+i+'" value="'+esc(entry.descriptor)+'" placeholder="760w">' +
|
|
1269
|
+
'<button class="img-remove" type="button" onclick="removeSrcsetEntry('+i+')">✕</button>' +
|
|
1270
|
+
'</div>';
|
|
1271
|
+
});
|
|
1272
|
+
html += '<button class="img-add" type="button" onclick="addSrcsetEntry()"><i class="bi bi-plus-lg"></i> Add srcset entry</button>';
|
|
1273
|
+
|
|
1274
|
+
// Sizes / media conditions
|
|
1275
|
+
html += subLbl('Media conditions');
|
|
1276
|
+
_sizesEntries.forEach(function(entry, i) {
|
|
1277
|
+
html += '<div class="img-row">' +
|
|
1278
|
+
'<select class="pr-inp img-cond-op" id="pp-sz-op-'+i+'">' +
|
|
1279
|
+
['max-width','min-width','max-height','min-height'].map(function(op){
|
|
1280
|
+
return '<option value="'+op+'"'+(entry.condition.split(':')[0].trim()===op?' selected':'')+'>'+op+'</option>';
|
|
1281
|
+
}).join('') +
|
|
1282
|
+
'</select>' +
|
|
1283
|
+
'<input class="pr-inp img-cond-val" type="text" id="pp-sz-val-'+i+'" value="'+esc((entry.condition.split(':')[1]||'').trim()||'')+'" placeholder="760px">' +
|
|
1284
|
+
'<input class="pr-inp" type="text" id="pp-sz-size-'+i+'" value="'+esc(entry.value)+'" placeholder="760px">' +
|
|
1285
|
+
'<button class="img-remove" type="button" onclick="removeSizesEntry('+i+')">✕</button>' +
|
|
1286
|
+
'</div>';
|
|
1287
|
+
});
|
|
1288
|
+
html += '<button class="img-add" type="button" onclick="addSizesEntry()"><i class="bi bi-plus-lg"></i> Add media condition</button>';
|
|
1289
|
+
|
|
1290
|
+
document.getElementById('acc-body-image').innerHTML = html;
|
|
1291
|
+
|
|
1292
|
+
// Wire src + alt
|
|
1293
|
+
var srcInp = document.getElementById('pp-img-src');
|
|
1294
|
+
if (srcInp) srcInp.addEventListener('input', function() {
|
|
1295
|
+
var orig = getOriginalValue('pp-src', el);
|
|
1296
|
+
el.setAttribute('src', srcInp.value);
|
|
1297
|
+
var prev = document.querySelector('.img-preview');
|
|
1298
|
+
if (prev) prev.src = srcInp.value;
|
|
1299
|
+
logChange(sel, 'pp-src', srcInp.value, el, orig);
|
|
1300
|
+
markDirty();
|
|
1301
|
+
});
|
|
1302
|
+
var altInp = document.getElementById('pp-img-alt');
|
|
1303
|
+
if (altInp) altInp.addEventListener('input', function() {
|
|
1304
|
+
var orig = getOriginalValue('pp-alt', el);
|
|
1305
|
+
el.setAttribute('alt', altInp.value);
|
|
1306
|
+
logChange(sel, 'pp-alt', altInp.value, el, orig);
|
|
1307
|
+
markDirty();
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// Wire srcset entry inputs
|
|
1311
|
+
_srcsetEntries.forEach(function(entry, i) {
|
|
1312
|
+
var uInp = document.getElementById('pp-se-url-'+i);
|
|
1313
|
+
var dInp = document.getElementById('pp-se-desc-'+i);
|
|
1314
|
+
function flushSrcset() {
|
|
1315
|
+
el.setAttribute('srcset', buildSrcset(_srcsetEntries));
|
|
1316
|
+
markDirty();
|
|
1317
|
+
}
|
|
1318
|
+
if (uInp) uInp.addEventListener('input', function(){ _srcsetEntries[i].url = uInp.value; flushSrcset(); });
|
|
1319
|
+
if (dInp) dInp.addEventListener('input', function(){ _srcsetEntries[i].descriptor = dInp.value; flushSrcset(); });
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
// Wire sizes/condition inputs
|
|
1323
|
+
_sizesEntries.forEach(function(entry, i) {
|
|
1324
|
+
var opInp = document.getElementById('pp-sz-op-'+i);
|
|
1325
|
+
var valInp = document.getElementById('pp-sz-val-'+i);
|
|
1326
|
+
var sizeInp = document.getElementById('pp-sz-size-'+i);
|
|
1327
|
+
function buildCondition() {
|
|
1328
|
+
var op = opInp ? opInp.value : (_sizesEntries[i].condition.split(':')[0].trim());
|
|
1329
|
+
var val = valInp ? valInp.value : '';
|
|
1330
|
+
_sizesEntries[i].condition = op + ': ' + val;
|
|
1331
|
+
}
|
|
1332
|
+
function flushSizes() {
|
|
1333
|
+
el.setAttribute('sizes', buildSizes(_sizesEntries));
|
|
1334
|
+
markDirty();
|
|
1335
|
+
}
|
|
1336
|
+
if (opInp) opInp.addEventListener('change', function(){ buildCondition(); flushSizes(); });
|
|
1337
|
+
if (valInp) valInp.addEventListener('input', function(){ buildCondition(); flushSizes(); });
|
|
1338
|
+
if (sizeInp) sizeInp.addEventListener('input', function(){ _sizesEntries[i].value = sizeInp.value; flushSizes(); });
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function addSrcsetEntry() {
|
|
1343
|
+
_srcsetEntries.push({url:'', descriptor:''});
|
|
1344
|
+
if (_imageEl) renderImageSection(_imageEl);
|
|
1345
|
+
}
|
|
1346
|
+
function removeSrcsetEntry(i) {
|
|
1347
|
+
_srcsetEntries.splice(i, 1);
|
|
1348
|
+
if (_imageEl) { _imageEl.setAttribute('srcset', buildSrcset(_srcsetEntries)); renderImageSection(_imageEl); markDirty(); }
|
|
1349
|
+
}
|
|
1350
|
+
function addSizesEntry() {
|
|
1351
|
+
_sizesEntries.push({condition:'max-width: 760px', value:'760px'});
|
|
1352
|
+
if (_imageEl) renderImageSection(_imageEl);
|
|
1353
|
+
}
|
|
1354
|
+
function removeSizesEntry(i) {
|
|
1355
|
+
_sizesEntries.splice(i, 1);
|
|
1356
|
+
if (_imageEl) { _imageEl.setAttribute('sizes', buildSizes(_sizesEntries)); renderImageSection(_imageEl); markDirty(); }
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// \u2500\u2500 Right panel rendering (accordion) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1360
|
+
function renderRightPanel(el) {
|
|
1361
|
+
if (!el) return;
|
|
1362
|
+
var cs = el.ownerDocument.defaultView.getComputedStyle(el);
|
|
1363
|
+
var tag = el.tagName.toLowerCase();
|
|
1364
|
+
|
|
1365
|
+
// Show element info header
|
|
1366
|
+
document.getElementById('el-info-tag').textContent = '<'+tag+'>';
|
|
1367
|
+
document.getElementById('el-info-sel').textContent = buildSelector(el);
|
|
1368
|
+
document.getElementById('el-info').style.display = '';
|
|
1369
|
+
document.getElementById('rp-accordion').style.display = '';
|
|
1370
|
+
|
|
1371
|
+
// \u2500\u2500 Image section (show + auto-open for <img>) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1372
|
+
var imgSec = document.getElementById('acc-image');
|
|
1373
|
+
if (tag === 'img') {
|
|
1374
|
+
imgSec.style.display = '';
|
|
1375
|
+
if (!imgSec.classList.contains('open')) imgSec.classList.add('open');
|
|
1376
|
+
renderImageSection(el);
|
|
1377
|
+
} else {
|
|
1378
|
+
imgSec.style.display = 'none';
|
|
1379
|
+
_imageEl = null;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// \u2500\u2500 Typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1383
|
+
document.getElementById('acc-body-typography').innerHTML =
|
|
1384
|
+
pr('Color', '<input type="color" class="pr-color" id="pp-color" value="'+esc(rgbToHex(cs.color))+'"><span style="font-size:11px;color:var(--text-3);flex:1;font-family:monospace">'+esc(cs.color)+'</span>') +
|
|
1385
|
+
pr2('Font size', '<input class="pr-inp" type="number" id="pp-fs" min="8" max="200" value="'+parseInt(cs.fontSize||'16')+'">',
|
|
1386
|
+
'Font weight', '<select class="pr-inp" id="pp-fw">'+weightOpts(cs.fontWeight)+'</select>') +
|
|
1387
|
+
pr('Font family', '<input class="pr-inp" id="pp-ff" type="text" value="'+esc(el.style.fontFamily||'')+'" placeholder="inherit">') +
|
|
1388
|
+
pr('Text align', '<select class="pr-inp" id="pp-ta">'+selOpts(['left','center','right','justify'],cs.textAlign)+'</select>') +
|
|
1389
|
+
pr2('Line height', '<input class="pr-inp" type="text" id="pp-lh" value="'+esc(el.style.lineHeight||'')+'" placeholder="normal">',
|
|
1390
|
+
'Letter spacing', '<input class="pr-inp" type="text" id="pp-ls" value="'+esc(el.style.letterSpacing||'')+'" placeholder="0">') +
|
|
1391
|
+
pr('Text decoration', '<select class="pr-inp" id="pp-td">'+selOpts(['none','underline','line-through','overline'],cs.textDecoration.split(' ')[0])+'</select>');
|
|
1392
|
+
|
|
1393
|
+
// \u2500\u2500 Background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1394
|
+
var bgiVal = (el.style.backgroundImage||'').replace(/url(['"]?([^'"]+)['"]?)/,'$1');
|
|
1395
|
+
document.getElementById('acc-body-background').innerHTML =
|
|
1396
|
+
pr('Color', '<input type="color" class="pr-color" id="pp-bg" value="'+esc(rgbToHex(cs.backgroundColor))+'"><span style="font-size:11px;color:var(--text-3);flex:1;font-family:monospace">'+esc(cs.backgroundColor)+'</span>') +
|
|
1397
|
+
pr('Image URL', '<input class="pr-inp" id="pp-bgi" type="url" value="'+esc(bgiVal)+'" placeholder="https://\u2026">') +
|
|
1398
|
+
pr2('Size', '<select class="pr-inp" id="pp-bgs">'+selOpts(['auto','cover','contain'],el.style.backgroundSize||'auto')+'</select>',
|
|
1399
|
+
'Repeat', '<select class="pr-inp" id="pp-bgr">'+selOpts(['repeat','no-repeat','repeat-x','repeat-y'],el.style.backgroundRepeat||'repeat')+'</select>');
|
|
1400
|
+
|
|
1401
|
+
// \u2500\u2500 Opacity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1402
|
+
var opVal = parseFloat(cs.opacity||'1').toFixed(2);
|
|
1403
|
+
document.getElementById('acc-body-opacity').innerHTML =
|
|
1404
|
+
'<div class="op-wrap">' +
|
|
1405
|
+
'<input type="range" class="op-slider" id="pp-op-range" min="0" max="1" step="0.01" value="'+opVal+'">' +
|
|
1406
|
+
'<input class="pr-inp pr-num" type="number" id="pp-op" min="0" max="1" step="0.01" value="'+opVal+'" style="width:54px;flex:none">' +
|
|
1407
|
+
'</div>';
|
|
1408
|
+
|
|
1409
|
+
// \u2500\u2500 Border \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1410
|
+
document.getElementById('acc-body-border').innerHTML =
|
|
1411
|
+
pr2('Style', '<select class="pr-inp" id="pp-bst">'+selOpts(['none','solid','dashed','dotted','double'],cs.borderStyle)+'</select>',
|
|
1412
|
+
'Color', '<input type="color" class="pr-color" id="pp-bc" value="'+esc(rgbToHex(cs.borderColor))+'">') +
|
|
1413
|
+
pr('Width', '<input class="pr-inp pr-num" type="number" id="pp-bw" min="0" max="20" value="'+parseInt(cs.borderWidth||'0')+'"><span class="pr-unit">px</span>') +
|
|
1414
|
+
subLbl('Border Radius') +
|
|
1415
|
+
'<div class="pr2">' +
|
|
1416
|
+
'<div class="pr2-item"><div class="pr2-lbl">Top Left</div><input class="pr-inp" type="number" id="pp-brtl" min="0" value="'+parseInt(cs.borderTopLeftRadius||'0')+'"></div>' +
|
|
1417
|
+
'<div class="pr2-item"><div class="pr2-lbl">Top Right</div><input class="pr-inp" type="number" id="pp-brtr" min="0" value="'+parseInt(cs.borderTopRightRadius||'0')+'"></div>' +
|
|
1418
|
+
'<div class="pr2-item"><div class="pr2-lbl">Bottom Left</div><input class="pr-inp" type="number" id="pp-brbl" min="0" value="'+parseInt(cs.borderBottomLeftRadius||'0')+'"></div>' +
|
|
1419
|
+
'<div class="pr2-item"><div class="pr2-lbl">Bottom Right</div><input class="pr-inp" type="number" id="pp-brbr" min="0" value="'+parseInt(cs.borderBottomRightRadius||'0')+'"></div>' +
|
|
1420
|
+
'</div>';
|
|
1421
|
+
|
|
1422
|
+
// \u2500\u2500 Shadow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1423
|
+
document.getElementById('acc-body-shadow').innerHTML =
|
|
1424
|
+
pr('Box Shadow', '<input class="pr-inp" id="pp-bs" type="text" value="'+esc(el.style.boxShadow||'')+'" placeholder="none">') +
|
|
1425
|
+
'<div class="shadow-presets">' +
|
|
1426
|
+
[['None','none'],['xs','0 1px 2px rgba(0,0,0,.1)'],['sm','0 2px 8px rgba(0,0,0,.15)'],['md','0 4px 24px rgba(0,0,0,.2)'],['lg','0 8px 40px rgba(0,0,0,.3)']].map(function(p){
|
|
1427
|
+
return '<button class="shadow-preset" type="button" onclick="applyShadowPreset(this.dataset.val)" data-val="'+esc(p[1])+'">'+p[0]+'</button>';
|
|
1428
|
+
}).join('') +
|
|
1429
|
+
'</div>';
|
|
1430
|
+
|
|
1431
|
+
// \u2500\u2500 Spacing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1432
|
+
document.getElementById('acc-body-spacing').innerHTML =
|
|
1433
|
+
subLbl('Margin') +
|
|
1434
|
+
'<div class="pr2">' +
|
|
1435
|
+
'<div class="pr2-item"><div class="pr2-lbl">Top</div><input class="pr-inp" type="number" id="pp-mt" value="'+parseInt(cs.marginTop||'0')+'"></div>' +
|
|
1436
|
+
'<div class="pr2-item"><div class="pr2-lbl">Right</div><input class="pr-inp" type="number" id="pp-mr" value="'+parseInt(cs.marginRight||'0')+'"></div>' +
|
|
1437
|
+
'<div class="pr2-item"><div class="pr2-lbl">Bottom</div><input class="pr-inp" type="number" id="pp-mb" value="'+parseInt(cs.marginBottom||'0')+'"></div>' +
|
|
1438
|
+
'<div class="pr2-item"><div class="pr2-lbl">Left</div><input class="pr-inp" type="number" id="pp-ml" value="'+parseInt(cs.marginLeft||'0')+'"></div>' +
|
|
1439
|
+
'</div>' +
|
|
1440
|
+
subLbl('Padding') +
|
|
1441
|
+
'<div class="pr2">' +
|
|
1442
|
+
'<div class="pr2-item"><div class="pr2-lbl">Top</div><input class="pr-inp" type="number" id="pp-pt" value="'+parseInt(cs.paddingTop||'0')+'"></div>' +
|
|
1443
|
+
'<div class="pr2-item"><div class="pr2-lbl">Right</div><input class="pr-inp" type="number" id="pp-pr" value="'+parseInt(cs.paddingRight||'0')+'"></div>' +
|
|
1444
|
+
'<div class="pr2-item"><div class="pr2-lbl">Bottom</div><input class="pr-inp" type="number" id="pp-pb" value="'+parseInt(cs.paddingBottom||'0')+'"></div>' +
|
|
1445
|
+
'<div class="pr2-item"><div class="pr2-lbl">Left</div><input class="pr-inp" type="number" id="pp-pl" value="'+parseInt(cs.paddingLeft||'0')+'"></div>' +
|
|
1446
|
+
'</div>';
|
|
1447
|
+
|
|
1448
|
+
// \u2500\u2500 Size & Position \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1449
|
+
document.getElementById('acc-body-size').innerHTML =
|
|
1450
|
+
pr2('Width', '<input class="pr-inp" type="text" id="pp-w" value="'+esc(el.style.width||'')+'" placeholder="auto">',
|
|
1451
|
+
'Height', '<input class="pr-inp" type="text" id="pp-h" value="'+esc(el.style.height||'')+'" placeholder="auto">') +
|
|
1452
|
+
pr2('Min Width', '<input class="pr-inp" type="text" id="pp-minw" value="'+esc(el.style.minWidth||'')+'" placeholder="0">',
|
|
1453
|
+
'Min Height','<input class="pr-inp" type="text" id="pp-minh" value="'+esc(el.style.minHeight||'')+'">') +
|
|
1454
|
+
pr2('Max Width', '<input class="pr-inp" type="text" id="pp-maxw" value="'+esc(el.style.maxWidth||'')+'" placeholder="none">',
|
|
1455
|
+
'Max Height','<input class="pr-inp" type="text" id="pp-maxh" value="'+esc(el.style.maxHeight||'')+'">') +
|
|
1456
|
+
pr2('Display', '<select class="pr-inp" id="pp-disp">'+selOpts(['block','inline-block','flex','inline-flex','grid','inline','none'],cs.display)+'</select>',
|
|
1457
|
+
'Position', '<select class="pr-inp" id="pp-pos">'+selOpts(['static','relative','absolute','fixed','sticky'],cs.position)+'</select>') +
|
|
1458
|
+
pr2('Top', '<input class="pr-inp" type="text" id="pp-top" value="'+esc(el.style.top||'')+'" placeholder="auto">',
|
|
1459
|
+
'Left', '<input class="pr-inp" type="text" id="pp-left" value="'+esc(el.style.left||'')+'" placeholder="auto">') +
|
|
1460
|
+
pr2('Bottom', '<input class="pr-inp" type="text" id="pp-bottom" value="'+esc(el.style.bottom||'')+'" placeholder="auto">',
|
|
1461
|
+
'Right', '<input class="pr-inp" type="text" id="pp-right" value="'+esc(el.style.right||'')+'" placeholder="auto">');
|
|
1462
|
+
|
|
1463
|
+
// \u2500\u2500 Device styling rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1464
|
+
document.getElementById('acc-body-device').innerHTML =
|
|
1465
|
+
subLbl('Mobile (\u2264768px)') +
|
|
1466
|
+
'<textarea class="pr-inp" id="pp-mob-css" style="width:100%;min-height:60px;font-family:monospace;font-size:11px;margin-bottom:8px" placeholder="/* styles for mobile */">'+esc(el.dataset.mobileCss||'')+'</textarea>' +
|
|
1467
|
+
subLbl('Tablet (\u22641024px)') +
|
|
1468
|
+
'<textarea class="pr-inp" id="pp-tab-css" style="width:100%;min-height:60px;font-family:monospace;font-size:11px" placeholder="/* styles for tablet */">'+esc(el.dataset.tabletCss||'')+'</textarea>';
|
|
1469
|
+
|
|
1470
|
+
// \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
|
|
1471
|
+
document.getElementById('acc-body-css').innerHTML =
|
|
1472
|
+
pr('Classes', '<input class="pr-inp" id="pp-cls" type="text" value="'+esc(el.className||'')+'" placeholder="class1 class2">') +
|
|
1473
|
+
subLbl('Custom CSS') +
|
|
1474
|
+
'<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>';
|
|
1475
|
+
|
|
1476
|
+
// \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
|
|
1477
|
+
var attrHtml =
|
|
1478
|
+
pr('ID', '<input class="pr-inp" id="pp-id" type="text" value="'+esc(el.id||'')+'" placeholder="element-id">');
|
|
1479
|
+
if (tag==='a') attrHtml +=
|
|
1480
|
+
pr('Href', '<input class="pr-inp" id="pp-href" type="url" value="'+esc(el.getAttribute('href')||'')+'" placeholder="https://">') +
|
|
1481
|
+
pr('Target', '<select class="pr-inp" id="pp-target">'+selOpts(['','_blank','_self','_parent'],el.getAttribute('target')||'')+'</select>');
|
|
1482
|
+
if (tag==='img') attrHtml +=
|
|
1483
|
+
pr('Src', '<input class="pr-inp" id="pp-src" type="url" value="'+esc(el.getAttribute('src')||'')+'">') +
|
|
1484
|
+
pr('Alt', '<input class="pr-inp" id="pp-alt" type="text" value="'+esc(el.getAttribute('alt')||'')+'">');
|
|
1485
|
+
if (tag==='input'||tag==='textarea') attrHtml +=
|
|
1486
|
+
pr('Placeholder', '<input class="pr-inp" id="pp-ph" type="text" value="'+esc(el.getAttribute('placeholder')||'')+'">');
|
|
1487
|
+
document.getElementById('acc-body-attributes').innerHTML = attrHtml;
|
|
1488
|
+
|
|
1489
|
+
// \u2500\u2500 HTML Content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1490
|
+
document.getElementById('acc-body-content').innerHTML =
|
|
1491
|
+
subLbl('Inner Text') +
|
|
1492
|
+
'<textarea class="pr-inp" id="pp-text" style="width:100%;min-height:60px;margin-bottom:8px">'+esc(el.innerText||'')+'</textarea>' +
|
|
1493
|
+
subLbl('Inner HTML') +
|
|
1494
|
+
'<textarea class="pr-inp" id="pp-html" style="width:100%;min-height:70px;font-family:monospace;font-size:11px">'+esc(el.innerHTML||'')+'</textarea>';
|
|
1495
|
+
|
|
1496
|
+
// \u2500\u2500 Wire up all inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1497
|
+
var bindings = [
|
|
1498
|
+
['pp-color', function(v){el.style.color=v}],
|
|
1499
|
+
['pp-fs', function(v){el.style.fontSize=v+'px'}],
|
|
1500
|
+
['pp-fw', function(v){el.style.fontWeight=v}],
|
|
1501
|
+
['pp-ff', function(v){el.style.fontFamily=v}],
|
|
1502
|
+
['pp-ta', function(v){el.style.textAlign=v}],
|
|
1503
|
+
['pp-lh', function(v){el.style.lineHeight=v}],
|
|
1504
|
+
['pp-ls', function(v){el.style.letterSpacing=v}],
|
|
1505
|
+
['pp-td', function(v){el.style.textDecoration=v}],
|
|
1506
|
+
['pp-bg', function(v){el.style.backgroundColor=v}],
|
|
1507
|
+
['pp-bgi', function(v){el.style.backgroundImage=v?'url('+v+')':''}],
|
|
1508
|
+
['pp-bgs', function(v){el.style.backgroundSize=v}],
|
|
1509
|
+
['pp-bgr', function(v){el.style.backgroundRepeat=v}],
|
|
1510
|
+
['pp-op', function(v){el.style.opacity=v;var r=document.getElementById('pp-op-range');if(r)r.value=v}],
|
|
1511
|
+
['pp-op-range',function(v){el.style.opacity=v;var i=document.getElementById('pp-op');if(i)i.value=parseFloat(v).toFixed(2)}],
|
|
1512
|
+
['pp-bst', function(v){el.style.borderStyle=v}],
|
|
1513
|
+
['pp-bw', function(v){el.style.borderWidth=v+'px';if(parseInt(v)>0&&!el.style.borderStyle)el.style.borderStyle='solid'}],
|
|
1514
|
+
['pp-bc', function(v){el.style.borderColor=v}],
|
|
1515
|
+
['pp-brtl', function(v){el.style.borderTopLeftRadius=v+'px'}],
|
|
1516
|
+
['pp-brtr', function(v){el.style.borderTopRightRadius=v+'px'}],
|
|
1517
|
+
['pp-brbl', function(v){el.style.borderBottomLeftRadius=v+'px'}],
|
|
1518
|
+
['pp-brbr', function(v){el.style.borderBottomRightRadius=v+'px'}],
|
|
1519
|
+
['pp-bs', function(v){el.style.boxShadow=v}],
|
|
1520
|
+
['pp-mt', function(v){el.style.marginTop=v+'px'}],
|
|
1521
|
+
['pp-mr', function(v){el.style.marginRight=v+'px'}],
|
|
1522
|
+
['pp-mb', function(v){el.style.marginBottom=v+'px'}],
|
|
1523
|
+
['pp-ml', function(v){el.style.marginLeft=v+'px'}],
|
|
1524
|
+
['pp-pt', function(v){el.style.paddingTop=v+'px'}],
|
|
1525
|
+
['pp-pr', function(v){el.style.paddingRight=v+'px'}],
|
|
1526
|
+
['pp-pb', function(v){el.style.paddingBottom=v+'px'}],
|
|
1527
|
+
['pp-pl', function(v){el.style.paddingLeft=v+'px'}],
|
|
1528
|
+
['pp-w', function(v){el.style.width=v}],
|
|
1529
|
+
['pp-h', function(v){el.style.height=v}],
|
|
1530
|
+
['pp-minw', function(v){el.style.minWidth=v}],
|
|
1531
|
+
['pp-minh', function(v){el.style.minHeight=v}],
|
|
1532
|
+
['pp-maxw', function(v){el.style.maxWidth=v}],
|
|
1533
|
+
['pp-maxh', function(v){el.style.maxHeight=v}],
|
|
1534
|
+
['pp-disp', function(v){el.style.display=v}],
|
|
1535
|
+
['pp-pos', function(v){el.style.position=v}],
|
|
1536
|
+
['pp-top', function(v){el.style.top=v}],
|
|
1537
|
+
['pp-left', function(v){el.style.left=v}],
|
|
1538
|
+
['pp-bottom', function(v){el.style.bottom=v}],
|
|
1539
|
+
['pp-right', function(v){el.style.right=v}],
|
|
1540
|
+
['pp-mob-css',function(v){el.dataset.mobileCss=v}],
|
|
1541
|
+
['pp-tab-css',function(v){el.dataset.tabletCss=v}],
|
|
1542
|
+
['pp-cls', function(v){el.className=v}],
|
|
1543
|
+
['pp-css', function(v){el.setAttribute('style',v)}],
|
|
1544
|
+
['pp-id', function(v){el.id=v}],
|
|
1545
|
+
['pp-href', function(v){el.setAttribute('href',v)}],
|
|
1546
|
+
['pp-target', function(v){el.setAttribute('target',v)}],
|
|
1547
|
+
['pp-src', function(v){el.setAttribute('src',v)}],
|
|
1548
|
+
['pp-alt', function(v){el.setAttribute('alt',v)}],
|
|
1549
|
+
['pp-ph', function(v){el.setAttribute('placeholder',v)}],
|
|
1550
|
+
['pp-text', function(v){el.innerText=v}],
|
|
1551
|
+
['pp-html', function(v){el.innerHTML=v}],
|
|
1552
|
+
];
|
|
1553
|
+
var sel = buildSelector(el);
|
|
1554
|
+
bindings.forEach(function(b){
|
|
1555
|
+
var inp = document.getElementById(b[0]);
|
|
1556
|
+
if (inp) inp.addEventListener('input', function(){
|
|
1557
|
+
// Read the original value BEFORE applying the change so we can revert later
|
|
1558
|
+
var orig = getOriginalValue(b[0], el);
|
|
1559
|
+
b[1](inp.value);
|
|
1560
|
+
logChange(sel, b[0], inp.value, el, orig);
|
|
1561
|
+
markDirty();
|
|
1562
|
+
});
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// \u2500\u2500 Selector helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1567
|
+
function buildSelector(el) {
|
|
1568
|
+
if (!el) return '';
|
|
1569
|
+
if (el.id) return '#' + el.id;
|
|
1570
|
+
var parts = [], node = el, depth = 0;
|
|
1571
|
+
while (node && node.nodeType === 1 && depth < 5) {
|
|
1572
|
+
if (node.id) { parts.unshift('#' + node.id); break; }
|
|
1573
|
+
var p = node.tagName.toLowerCase();
|
|
1574
|
+
if (node.classList && node.classList.length) p += '.' + Array.from(node.classList).slice(0,2).join('.');
|
|
1575
|
+
var idx = 1, sib = node.previousElementSibling;
|
|
1576
|
+
while (sib) { if (sib.tagName === node.tagName) idx++; sib = sib.previousElementSibling; }
|
|
1577
|
+
if (idx > 1) p += ':nth-of-type(' + idx + ')';
|
|
1578
|
+
parts.unshift(p);
|
|
1579
|
+
node = node.parentElement;
|
|
1580
|
+
depth++;
|
|
1581
|
+
}
|
|
1582
|
+
return parts.join(' > ');
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// \u2500\u2500 Iframe interaction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1586
|
+
function attachClickHandler() {
|
|
1587
|
+
try {
|
|
1588
|
+
var iframe = document.getElementById('iframeId');
|
|
1589
|
+
var doc = iframe.contentDocument;
|
|
1590
|
+
doc.addEventListener('click', function(e) {
|
|
1591
|
+
if (currentMode !== 'editor') return;
|
|
1592
|
+
e.preventDefault();
|
|
1593
|
+
e.stopPropagation();
|
|
1594
|
+
var target = e.target;
|
|
1595
|
+
if (!target || target === doc.body || target === doc.documentElement) {
|
|
1596
|
+
deselectElement(); return;
|
|
1597
|
+
}
|
|
1598
|
+
selectElement(target);
|
|
1599
|
+
}, true);
|
|
1600
|
+
} catch(_) {}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function attachChangeObserver() {
|
|
1604
|
+
try {
|
|
1605
|
+
var iframe = document.getElementById('iframeId');
|
|
1606
|
+
var obs = new MutationObserver(function() { markDirty(); });
|
|
1607
|
+
obs.observe(iframe.contentDocument.body, {
|
|
1608
|
+
childList: true, subtree: true, attributes: true, characterData: true
|
|
1609
|
+
});
|
|
1610
|
+
} catch(_) {}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// \u2500\u2500 HTML insertion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1614
|
+
function insertHtml(html) {
|
|
1615
|
+
if (!html) return;
|
|
1616
|
+
try {
|
|
1617
|
+
var iframe = document.getElementById('iframeId');
|
|
1618
|
+
var doc = iframe.contentDocument;
|
|
1619
|
+
var tmp = doc.createElement('div');
|
|
1620
|
+
tmp.innerHTML = html;
|
|
1621
|
+
var node = tmp.firstElementChild || tmp;
|
|
1622
|
+
if (selectedEl && selectedEl !== doc.body && selectedEl.parentNode) {
|
|
1623
|
+
selectedEl.parentNode.insertBefore(node, selectedEl.nextSibling);
|
|
1624
|
+
} else {
|
|
1625
|
+
doc.body.appendChild(node);
|
|
1626
|
+
}
|
|
1627
|
+
selectElement(node);
|
|
1628
|
+
markDirty();
|
|
1629
|
+
} catch(err) { console.warn('[V2] insertHtml:', err); }
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1633
|
+
function renderSidebar(filter) {
|
|
1634
|
+
var q = (filter || '').toLowerCase();
|
|
1635
|
+
|
|
1636
|
+
// Components tab
|
|
1637
|
+
var compTab = document.getElementById('tab-components');
|
|
1638
|
+
compTab.innerHTML = '';
|
|
1639
|
+
var baseFiltered = BASE_COMPONENTS.filter(function(c) { return !q || c.name.toLowerCase().indexOf(q) >= 0; });
|
|
1640
|
+
if (baseFiltered.length > 0) {
|
|
1641
|
+
var h1 = document.createElement('div'); h1.className = 'cg-hdr'; h1.textContent = 'Base Elements'; compTab.appendChild(h1);
|
|
1642
|
+
var g1 = document.createElement('div'); g1.className = 'cg-grid';
|
|
1643
|
+
baseFiltered.forEach(function(c) {
|
|
1644
|
+
var item = document.createElement('div'); item.className = 'cg-item'; item.title = 'Insert ' + c.name;
|
|
1645
|
+
item.innerHTML = '<div class="cg-icon"><i class="bi ' + c.icon + '"></i></div><div class="cg-name">' + c.name + '</div>';
|
|
1646
|
+
item.onclick = function() { insertHtml(c.html); };
|
|
1647
|
+
g1.appendChild(item);
|
|
1648
|
+
});
|
|
1649
|
+
compTab.appendChild(g1);
|
|
1650
|
+
}
|
|
1651
|
+
if (!q && typeof Vvveb !== 'undefined' && Vvveb.Components && Vvveb.Components.list) {
|
|
1652
|
+
var vvItems = [], clist = Vvveb.Components.list;
|
|
1653
|
+
for (var ck in clist) { if (Object.prototype.hasOwnProperty.call(clist,ck)) vvItems.push({ name:(clist[ck].name||ck).slice(0,14), html:clist[ck].html||'' }); }
|
|
1654
|
+
if (vvItems.length > 0) {
|
|
1655
|
+
var h2 = document.createElement('div'); h2.className = 'cg-hdr'; h2.textContent = 'Bootstrap 5'; compTab.appendChild(h2);
|
|
1656
|
+
var g2 = document.createElement('div'); g2.className = 'cg-grid';
|
|
1657
|
+
vvItems.slice(0,21).forEach(function(c) {
|
|
1658
|
+
var item = document.createElement('div'); item.className = 'cg-item'; item.title = c.name;
|
|
1659
|
+
item.innerHTML = '<div class="cg-icon"><i class="bi bi-puzzle"></i></div><div class="cg-name">' + c.name + '</div>';
|
|
1660
|
+
item.onclick = function() { insertHtml(c.html); };
|
|
1661
|
+
g2.appendChild(item);
|
|
1662
|
+
});
|
|
1663
|
+
compTab.appendChild(g2);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (!compTab.children.length) compTab.innerHTML = '<div style="padding:20px;text-align:center;color:#444;font-size:12px">No components match</div>';
|
|
1667
|
+
|
|
1668
|
+
// Sections tab
|
|
1669
|
+
var secTab = document.getElementById('tab-sections');
|
|
1670
|
+
secTab.innerHTML = '';
|
|
1671
|
+
var croFiltered = CRO_SECTIONS.filter(function(s) { return !q || s.name.toLowerCase().indexOf(q) >= 0 || (s.desc||'').toLowerCase().indexOf(q) >= 0; });
|
|
1672
|
+
if (croFiltered.length > 0) {
|
|
1673
|
+
var ch = document.createElement('div'); ch.className = 'cg-hdr'; ch.textContent = 'CRO Components'; secTab.appendChild(ch);
|
|
1674
|
+
croFiltered.forEach(function(sec) {
|
|
1675
|
+
var item = document.createElement('div'); item.className = 'sec-item';
|
|
1676
|
+
item.innerHTML = '<div class="sec-thumb">'+(sec.icon||'\u{1F4E6}')+'</div><div class="sec-info"><div class="sec-name">'+sec.name+'</div>'+(sec.desc?'<div class="sec-desc">'+sec.desc+'</div>':'')+'</div>';
|
|
1677
|
+
item.onclick = function() { insertHtml(sec.html); };
|
|
1678
|
+
secTab.appendChild(item);
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
if (typeof Vvveb !== 'undefined' && Vvveb.Sections && Vvveb.Sections.list) {
|
|
1682
|
+
var slist = Vvveb.Sections.list, hasVv = false;
|
|
1683
|
+
for (var sk in slist) {
|
|
1684
|
+
if (!Object.prototype.hasOwnProperty.call(slist,sk)) continue;
|
|
1685
|
+
var sec = slist[sk], snm = sec.name || sk;
|
|
1686
|
+
if (q && snm.toLowerCase().indexOf(q) < 0) continue;
|
|
1687
|
+
if (!hasVv) { hasVv = true; var sh = document.createElement('div'); sh.className = 'cg-hdr'; sh.textContent = 'Bootstrap Sections'; secTab.appendChild(sh); }
|
|
1688
|
+
var sel = document.createElement('div'); sel.className = 'sec-item';
|
|
1689
|
+
sel.innerHTML = '<div class="sec-thumb">\u{1F4D0}</div><div class="sec-info"><div class="sec-name">'+snm+'</div></div>';
|
|
1690
|
+
sel.onclick = (function(s){ return function(){ insertHtml(s.html||''); }; })(sec);
|
|
1691
|
+
secTab.appendChild(sel);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!secTab.children.length) secTab.innerHTML = '<div style="padding:20px;text-align:center;color:#444;font-size:12px">No sections match</div>';
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
document.getElementById('comp-search').addEventListener('input', function() { renderSidebar(this.value); });
|
|
1698
|
+
|
|
1699
|
+
// \u2500\u2500 Save / Close \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1700
|
+
document.getElementById('btn-save').addEventListener('click', handleSave);
|
|
1701
|
+
document.getElementById('btn-close').addEventListener('click', handleClose);
|
|
1702
|
+
|
|
1703
|
+
function handleSave() {
|
|
1704
|
+
saveCurrentVariationHtml();
|
|
1705
|
+
var updatedVariations = variations.map(function(v) {
|
|
1706
|
+
var saved = varHtmlCache[v._id];
|
|
1707
|
+
if (!saved) return Object.assign({}, v);
|
|
1708
|
+
return Object.assign({}, v, { changesets: JSON.stringify([{ selector: '__vvveb_body__', html: saved, mutations: [] }]) });
|
|
1709
|
+
});
|
|
1710
|
+
send('save-experiment', { experimentId: experimentData ? experimentData.experimentId : null, variations: updatedVariations });
|
|
1711
|
+
markClean();
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function handleClose() {
|
|
1715
|
+
if (isDirty && !confirm('You have unsaved changes. Discard them?')) return;
|
|
1716
|
+
send('close-editor', {});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// \u2500\u2500 Keyboard shortcuts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1720
|
+
document.addEventListener('keydown', function(e) {
|
|
1721
|
+
var meta = e.metaKey || e.ctrlKey;
|
|
1722
|
+
if (meta && !e.shiftKey && e.key === 'z') { e.preventDefault(); if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.undo(); }
|
|
1723
|
+
if (meta && e.shiftKey && e.key === 'z') { e.preventDefault(); if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.redo(); }
|
|
1724
|
+
if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
|
|
1725
|
+
if (e.key === 'Escape' && selectedEl) deselectElement();
|
|
1726
|
+
});
|
|
1727
|
+
document.getElementById('btn-undo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.undo(); });
|
|
1728
|
+
document.getElementById('btn-redo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.redo(); });
|
|
1729
|
+
|
|
1730
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1731
|
+
function registerCROSections() {
|
|
1732
|
+
if (typeof Vvveb === 'undefined' || !Vvveb.Sections) return;
|
|
1733
|
+
CRO_SECTIONS.forEach(function(sec) { Vvveb.Sections.add(sec.key, { name: sec.name, image: '', html: sec.html }); });
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
window.addEventListener('load', function() {
|
|
1737
|
+
registerCROSections();
|
|
1738
|
+
renderSidebar();
|
|
1739
|
+
vvvebReady = true;
|
|
1740
|
+
|
|
1741
|
+
// CDN scripts loaded \u2014 hide spinner, show no-url state until experiment arrives
|
|
1742
|
+
document.getElementById('loading').classList.add('hidden');
|
|
1743
|
+
showNoUrl(true);
|
|
1744
|
+
|
|
1745
|
+
// After each iframe load: apply variation, wire click+mutation handlers
|
|
1746
|
+
var iframe = document.getElementById('iframeId');
|
|
1747
|
+
iframe.addEventListener('load', function() {
|
|
1748
|
+
if (!iframe.src || iframe.src === 'about:blank' || iframe.src === window.location.href) return;
|
|
1749
|
+
document.getElementById('loading').classList.add('hidden');
|
|
1750
|
+
showNoUrl(false);
|
|
1751
|
+
deselectElement();
|
|
1752
|
+
applyActiveVariationHtml();
|
|
1753
|
+
attachClickHandler();
|
|
1754
|
+
attachChangeObserver();
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
send('editor-ready', {});
|
|
1758
|
+
});
|
|
1759
|
+
</script>
|
|
1760
|
+
</body>
|
|
1761
|
+
</html>`;
|
|
1762
|
+
}
|
|
1763
|
+
var getDefaultAnthropicApiKey = () => {
|
|
1764
|
+
if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
|
|
1765
|
+
try {
|
|
1766
|
+
const currentDir = path__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vite.cjs', document.baseURI).href))));
|
|
1767
|
+
const envPath = path__default.default.resolve(
|
|
1768
|
+
currentDir,
|
|
1769
|
+
"../../../conversion-visual-editor/.env.local"
|
|
1770
|
+
);
|
|
1771
|
+
const envFile = fs__default.default.readFileSync(envPath, "utf-8");
|
|
1772
|
+
const match = envFile.match(/ANTHROPIC_API_KEY=(.+)/);
|
|
1773
|
+
return match ? match[1].trim() : "";
|
|
1774
|
+
} catch {
|
|
1775
|
+
return "";
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
function createVisualEditorMiddleware(options) {
|
|
1779
|
+
const anthropicApiKey = options?.anthropicApiKey || getDefaultAnthropicApiKey();
|
|
1780
|
+
const enableGenerateTestApi = options?.enableGenerateTestApi ?? true;
|
|
1781
|
+
const allowedFrameOrigins = options?.allowedFrameOrigins ?? ["*"];
|
|
1782
|
+
function setFrameHeaders(req, res) {
|
|
1783
|
+
res.removeHeader("X-Frame-Options");
|
|
1784
|
+
const requestOrigin = req.headers?.["origin"] || req.headers?.["referer"] || "";
|
|
1785
|
+
if (allowedFrameOrigins.includes("*")) {
|
|
1786
|
+
let frameOrigin = "*";
|
|
1787
|
+
if (requestOrigin) {
|
|
1788
|
+
try {
|
|
1789
|
+
frameOrigin = new URL(requestOrigin).origin;
|
|
1790
|
+
} catch {
|
|
1791
|
+
frameOrigin = "*";
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
res.setHeader("Content-Security-Policy", `frame-ancestors 'self' ${frameOrigin}`);
|
|
1795
|
+
} else {
|
|
1796
|
+
res.setHeader(
|
|
1797
|
+
"Content-Security-Policy",
|
|
1798
|
+
`frame-ancestors 'self' ${allowedFrameOrigins.join(" ")}`
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return async (req, res, next) => {
|
|
1803
|
+
const pathname = (req.url || "").split("?")[0];
|
|
1804
|
+
if (pathname === "/bridge.js") {
|
|
1805
|
+
res.removeHeader("X-Frame-Options");
|
|
1806
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
1807
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1808
|
+
res.end(BRIDGE_SCRIPT);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (pathname === "/vvveb-editor") {
|
|
1812
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1813
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1814
|
+
setFrameHeaders(req, res);
|
|
1815
|
+
res.end(buildVvvebEditorHtml());
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (pathname === "/api/generate-test" && enableGenerateTestApi) {
|
|
1819
|
+
if (req.method === "OPTIONS") {
|
|
1820
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1821
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1822
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1823
|
+
res.statusCode = 204;
|
|
1824
|
+
res.end();
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (req.method !== "POST") {
|
|
1828
|
+
res.statusCode = 405;
|
|
1829
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (!anthropicApiKey) {
|
|
1833
|
+
res.statusCode = 500;
|
|
1834
|
+
res.setHeader("Content-Type", "application/json");
|
|
1835
|
+
res.end(
|
|
1836
|
+
JSON.stringify({ error: "ANTHROPIC_API_KEY is not configured" })
|
|
1837
|
+
);
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
try {
|
|
1841
|
+
const chunks = [];
|
|
1842
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1843
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1844
|
+
if (!body.prompt || !body.pageSnapshot) {
|
|
1845
|
+
res.statusCode = 400;
|
|
1846
|
+
res.setHeader("Content-Type", "application/json");
|
|
1847
|
+
res.end(JSON.stringify({ error: "Missing prompt or pageSnapshot" }));
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const parts = [
|
|
1851
|
+
"## Optimization Goal",
|
|
1852
|
+
body.prompt,
|
|
1853
|
+
"",
|
|
1854
|
+
"## Page Snapshot",
|
|
1855
|
+
JSON.stringify(body.pageSnapshot, null, 2)
|
|
1856
|
+
];
|
|
1857
|
+
const anthropicRes = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
headers: {
|
|
1860
|
+
"Content-Type": "application/json",
|
|
1861
|
+
"x-api-key": anthropicApiKey,
|
|
1862
|
+
"anthropic-version": "2023-06-01"
|
|
1863
|
+
},
|
|
1864
|
+
body: JSON.stringify({
|
|
1865
|
+
model: "claude-sonnet-4-20250514",
|
|
1866
|
+
max_tokens: 4096,
|
|
1867
|
+
system: AI_SYSTEM_PROMPT,
|
|
1868
|
+
messages: [{ role: "user", content: parts.join("\n") }]
|
|
1869
|
+
})
|
|
1870
|
+
});
|
|
1871
|
+
if (!anthropicRes.ok) {
|
|
1872
|
+
const errText = await anthropicRes.text();
|
|
1873
|
+
res.statusCode = 502;
|
|
1874
|
+
res.setHeader("Content-Type", "application/json");
|
|
1875
|
+
res.end(
|
|
1876
|
+
JSON.stringify({
|
|
1877
|
+
error: `Anthropic API returned ${anthropicRes.status}`,
|
|
1878
|
+
detail: errText
|
|
1879
|
+
})
|
|
1880
|
+
);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
const anthropicData = await anthropicRes.json();
|
|
1884
|
+
const textBlock = anthropicData.content?.find((b) => b.type === "text");
|
|
1885
|
+
if (!textBlock?.text) {
|
|
1886
|
+
res.statusCode = 502;
|
|
1887
|
+
res.setHeader("Content-Type", "application/json");
|
|
1888
|
+
res.end(JSON.stringify({ error: "No text in Anthropic response" }));
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
let cleaned = textBlock.text.trim();
|
|
1892
|
+
if (cleaned.startsWith("```")) {
|
|
1893
|
+
cleaned = cleaned.replace(/^```[a-zA-Z]*\n?/, "").replace(/\n?```\s*$/, "").trim();
|
|
1894
|
+
}
|
|
1895
|
+
let parsed;
|
|
1896
|
+
try {
|
|
1897
|
+
parsed = JSON.parse(cleaned);
|
|
1898
|
+
} catch {
|
|
1899
|
+
res.statusCode = 502;
|
|
1900
|
+
res.setHeader("Content-Type", "application/json");
|
|
1901
|
+
res.end(JSON.stringify({ error: "Invalid JSON from Claude", raw: cleaned }));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
const now = Date.now();
|
|
1905
|
+
if (parsed.mutations) {
|
|
1906
|
+
parsed.mutations = parsed.mutations.map((m, i) => ({
|
|
1907
|
+
...m,
|
|
1908
|
+
id: m.id || `ai_mut_${String(i + 1).padStart(3, "0")}`,
|
|
1909
|
+
timestamp: now + i
|
|
1910
|
+
}));
|
|
1911
|
+
}
|
|
1912
|
+
res.setHeader("Content-Type", "application/json");
|
|
1913
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1914
|
+
res.end(JSON.stringify(parsed));
|
|
1915
|
+
} catch (err) {
|
|
1916
|
+
res.statusCode = 500;
|
|
1917
|
+
res.setHeader("Content-Type", "application/json");
|
|
1918
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1919
|
+
}
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (pathname.startsWith("/api/proxy")) {
|
|
1923
|
+
try {
|
|
1924
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
1925
|
+
const targetUrl = url.searchParams.get("url");
|
|
1926
|
+
const password = url.searchParams.get("password") || "";
|
|
1927
|
+
if (!targetUrl) {
|
|
1928
|
+
res.statusCode = 400;
|
|
1929
|
+
res.end(JSON.stringify({ error: "Missing url parameter" }));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const parsed = new URL(targetUrl);
|
|
1933
|
+
const origin = parsed.origin;
|
|
1934
|
+
const method = (req.method || "GET").toUpperCase();
|
|
1935
|
+
const headers = {
|
|
1936
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
1937
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
1938
|
+
};
|
|
1939
|
+
let cookieHeader = "";
|
|
1940
|
+
if (password) {
|
|
1941
|
+
const passResp = await fetch(`${origin}/password`, {
|
|
1942
|
+
method: "POST",
|
|
1943
|
+
headers: {
|
|
1944
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1945
|
+
...headers
|
|
1946
|
+
},
|
|
1947
|
+
body: `form_type=storefront_password&utf8=%E2%9C%93&password=${encodeURIComponent(
|
|
1948
|
+
password
|
|
1949
|
+
)}`,
|
|
1950
|
+
redirect: "manual"
|
|
1951
|
+
});
|
|
1952
|
+
const setCookies = passResp.headers.getSetCookie?.() || [];
|
|
1953
|
+
cookieHeader = setCookies.map((c) => c.split(";")[0]).join("; ");
|
|
1954
|
+
}
|
|
1955
|
+
const fetchHeaders = { ...headers };
|
|
1956
|
+
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
|
1957
|
+
const lowerKey = key.toLowerCase();
|
|
1958
|
+
if (!value || [
|
|
1959
|
+
"host",
|
|
1960
|
+
"connection",
|
|
1961
|
+
"content-length",
|
|
1962
|
+
"accept-encoding",
|
|
1963
|
+
"origin",
|
|
1964
|
+
"referer"
|
|
1965
|
+
].includes(lowerKey)) {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
fetchHeaders[key] = Array.isArray(value) ? value.join(",") : String(value);
|
|
1969
|
+
});
|
|
1970
|
+
fetchHeaders.Origin = origin;
|
|
1971
|
+
fetchHeaders.Referer = targetUrl;
|
|
1972
|
+
if (cookieHeader) {
|
|
1973
|
+
fetchHeaders.Cookie = fetchHeaders.Cookie ? `${fetchHeaders.Cookie}; ${cookieHeader}` : cookieHeader;
|
|
1974
|
+
}
|
|
1975
|
+
let requestBody;
|
|
1976
|
+
if (!["GET", "HEAD"].includes(method)) {
|
|
1977
|
+
const chunks = [];
|
|
1978
|
+
for await (const chunk of req) {
|
|
1979
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1980
|
+
}
|
|
1981
|
+
if (chunks.length > 0) requestBody = Buffer.concat(chunks);
|
|
1982
|
+
}
|
|
1983
|
+
const upstream = await fetch(targetUrl, {
|
|
1984
|
+
method,
|
|
1985
|
+
headers: fetchHeaders,
|
|
1986
|
+
body: requestBody ? Buffer.from(requestBody) : null,
|
|
1987
|
+
redirect: "follow"
|
|
1988
|
+
});
|
|
1989
|
+
const responseContentType = upstream.headers.get("content-type") || "";
|
|
1990
|
+
const isHtmlResponse = responseContentType.includes("text/html");
|
|
1991
|
+
const secFetchDest = (req.headers?.["sec-fetch-dest"] || "").toLowerCase();
|
|
1992
|
+
const isNavigationRequest = secFetchDest === "iframe" || secFetchDest === "document" || secFetchDest === "";
|
|
1993
|
+
if (!isHtmlResponse || !isNavigationRequest) {
|
|
1994
|
+
const binary = Buffer.from(await upstream.arrayBuffer());
|
|
1995
|
+
res.statusCode = upstream.status;
|
|
1996
|
+
if (responseContentType) {
|
|
1997
|
+
res.setHeader("Content-Type", responseContentType);
|
|
1998
|
+
}
|
|
1999
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2000
|
+
res.end(binary);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
let html = await upstream.text();
|
|
2004
|
+
if (html.includes("form_type") && html.includes("storefront_password")) {
|
|
2005
|
+
res.statusCode = 401;
|
|
2006
|
+
res.end(JSON.stringify({ error: "Password authentication failed." }));
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
html = html.replace(/(href|src|action)="\/(?!\/)/g, `$1="${origin}/`);
|
|
2010
|
+
const escapedOrigin = origin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2011
|
+
const proxyBase = `/api/proxy?password=${encodeURIComponent(password)}&url=`;
|
|
2012
|
+
html = html.replace(
|
|
2013
|
+
new RegExp(`(href|action)="${escapedOrigin}(/[^"]*)"`, "g"),
|
|
2014
|
+
(match, attr, urlPath) => {
|
|
2015
|
+
const lastSegment = urlPath.split("?")[0].split("#")[0].split("/").pop() || "";
|
|
2016
|
+
if (attr === "href" && /\.\w{1,5}$/.test(lastSegment)) {
|
|
2017
|
+
return match;
|
|
2018
|
+
}
|
|
2019
|
+
return `${attr}="${proxyBase}${encodeURIComponent(origin + urlPath)}"`;
|
|
2020
|
+
}
|
|
2021
|
+
);
|
|
2022
|
+
if (html.includes("</head>")) {
|
|
2023
|
+
html = html.replace("</head>", `${popupHideCss}
|
|
2024
|
+
</head>`);
|
|
2025
|
+
}
|
|
2026
|
+
const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{arguments[1]=toAbsoluteOriginUrl(url);}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
|
|
2027
|
+
if (html.includes("</head>")) {
|
|
2028
|
+
html = html.replace("</head>", `${runtimeProxyScript}
|
|
2029
|
+
</head>`);
|
|
2030
|
+
} else {
|
|
2031
|
+
html = runtimeProxyScript + html;
|
|
2032
|
+
}
|
|
2033
|
+
const bridgeScript = `<script src="/bridge.js"></script>`;
|
|
2034
|
+
html = html.includes("</body>") ? html.replace("</body>", `${bridgeScript}
|
|
2035
|
+
</body>`) : html + bridgeScript;
|
|
2036
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2037
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2038
|
+
setFrameHeaders(req, res);
|
|
2039
|
+
res.end(html);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
res.statusCode = 500;
|
|
2042
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2043
|
+
}
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
next();
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
function visualEditorProxyPlugin(options) {
|
|
2050
|
+
const mw = createVisualEditorMiddleware(options);
|
|
2051
|
+
return {
|
|
2052
|
+
name: "visual-editor-proxy",
|
|
2053
|
+
generateBundle() {
|
|
2054
|
+
this.emitFile({ type: "asset", fileName: "bridge.js", source: BRIDGE_SCRIPT });
|
|
2055
|
+
this.emitFile({ type: "asset", fileName: "vvveb-editor/index.html", source: buildVvvebEditorHtml() });
|
|
2056
|
+
},
|
|
2057
|
+
configureServer(server) {
|
|
2058
|
+
server.middlewares.use(mw);
|
|
2059
|
+
},
|
|
2060
|
+
configurePreviewServer(server) {
|
|
2061
|
+
server.middlewares.use(mw);
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
exports.createVisualEditorMiddleware = createVisualEditorMiddleware;
|
|
2067
|
+
exports.visualEditorProxyPlugin = visualEditorProxyPlugin;
|