@accelerated-agency/visual-editor 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/vite.cjs +667 -69
  2. package/dist/vite.js +667 -69
  3. package/package.json +1 -1
package/dist/vite.cjs CHANGED
@@ -86,17 +86,26 @@ function hasTrackingMarker(input, markers) {
86
86
  }
87
87
  return false;
88
88
  }
89
+ function patchKnownUnsafeEditorPatterns(scriptTag) {
90
+ let out = String(scriptTag || "");
91
+ out = out.replace(
92
+ /dragRegion\.addEventListener\((['"])pointerdown\1,\s*onPointerDown\);?/g,
93
+ "if (typeof dragRegion !== 'undefined' && dragRegion) dragRegion.addEventListener($1pointerdown$1, onPointerDown);"
94
+ );
95
+ return out;
96
+ }
89
97
  function stripTrackingScriptsFromScrapedHtml(html, markers) {
90
98
  let removedCount = 0;
91
99
  let out = html;
92
100
  out = out.replace(/<script\b[\s\S]*?<\/script>/gi, (tag) => {
101
+ var patchedTag = patchKnownUnsafeEditorPatterns(tag);
93
102
  const srcMatch = tag.match(/\bsrc\s*=\s*(["'])(.*?)\1/i);
94
103
  const src = srcMatch?.[2] || "";
95
104
  if (hasTrackingMarker(src, markers) || hasTrackingMarker(tag, markers)) {
96
105
  removedCount += 1;
97
106
  return "";
98
107
  }
99
- return tag;
108
+ return patchedTag;
100
109
  });
101
110
  out = out.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, (tag) => {
102
111
  if (!hasTrackingMarker(tag, markers)) return tag;
@@ -271,8 +280,97 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
271
280
  .tb-viewport #dev-label{font-size:14px;font-weight:500;color:##404040;min-width:auto;background:none;border:none;padding:0;overflow:visible;white-space:nowrap;text-overflow:clip;border-radius:0;max-width:none}
272
281
  .tb-viewport i{font-size:9px;color:##404040}
273
282
  /* Device toggle buttons */
283
+ .tb-dev-wrap{position:relative;display:flex;align-items:center}
274
284
  .tb-dev-btns{display:flex;align-items:center;gap:1px;padding:8px 4px;background:#F0F0F0;border-radius:7px; height: 30px;width: 116px;}
275
285
  .tb-dev-3btns{display:flex;align-items:center;gap:2px;padding:8px 4px;background:#F0F0F0;border-radius:7px; height: 30px;width: 100px;}
286
+ .tb-dev-menu{
287
+ position:absolute;top:calc(100% + 8px);right:0;z-index:12040;display:none;
288
+ width:264px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:4px 4px;
289
+ box-shadow:0 12px 28px rgba(2,6,23,.18);color:#27272a
290
+ }
291
+ .tb-dev-menu.open{display:block}
292
+ .tb-dev-menu .hd{font-size:12px;font-weight:600;color:#3f3f46;margin-bottom:10px}
293
+ .tb-dev-menu .row{display:flex;align-items:center;gap:8px;}
294
+ .tb-dev-menu .row.height-width-row .row{
295
+ flex-direction: column;
296
+ align-items: flex-start;
297
+ }
298
+ .tb-dev-menu .row.height-width-row label{
299
+ color: var(--content-subtle, #737373);
300
+ font-size: 12px;
301
+ font-style: normal;
302
+ font-weight: 500;
303
+ line-height: 14px; /* 116.667% */
304
+ }
305
+ .tb-dev-menu > .row{
306
+ padding: 4px 12px;
307
+ }
308
+ .tb-dev-menu label{ flex-shrink: 0;
309
+ overflow: hidden;
310
+ color: var(--base-surface, #646465);
311
+ font-size: var(--font-size-sm, 14px);
312
+ font-style: normal;
313
+ font-weight: 500;
314
+ line-height: var(--font-leading-4, 16px);
315
+ white-space: nowrap;
316
+ min-width: fit-content;}
317
+ .tb-dev-menu input{
318
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.20), 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.01);
319
+ width:100%;height:30px;border-radius:6px;
320
+ padding:0 8px;
321
+ font-size:12px;
322
+ color:#18181b;
323
+ background:#fff;
324
+ outline:none;
325
+ color:#404040;
326
+ border-color:transparent;
327
+ }
328
+ .tb-dev-menu input#dev-zoom-level{
329
+ max-width: fit-content;
330
+ margin-left: auto;
331
+ }
332
+ .tb-dev-menu input:focus{
333
+ border-color:#1A1A1A;
334
+ box-shadow:0 0 0 2px rgba(99,102,241,.14)
335
+ }
336
+ .tb-dev-menu .row-split > *{flex:1}
337
+ .tb-dev-menu .panel-toggle label{width:100%}
338
+ .tb-dev-menu .panel-toggle-label{
339
+ overflow:hidden;
340
+ color:var(--content-subtle, #737373);
341
+ text-overflow:ellipsis;
342
+ font-size:var(--font-size-sm, 14px);
343
+ font-style:normal;
344
+ font-weight:500;
345
+ line-height:var(--font-leading-4, 16px);
346
+ white-space:nowrap;
347
+ cursor:pointer;
348
+ }
349
+ .tb-dev-menu .vp-presets{
350
+ margin-top:8px;padding-top:8px;border-top:1px dashed #ececf0;
351
+ display:flex;flex-direction:column;gap:2px
352
+ }
353
+ .tb-dev-menu .vp-preset-btn{
354
+ border:none;background:transparent;text-align:left;cursor:pointer;
355
+ border-radius:6px;
356
+ padding:8px 12px;
357
+ color: var(--content-subtle, #737373);
358
+ text-overflow: ellipsis;
359
+ font-size: var(--font-size-sm, 14px);
360
+ font-style: normal;
361
+ font-weight: 500;
362
+ line-height: var(--font-leading-4, 16px);
363
+ }
364
+ .tb-dev-menu .vp-preset-btn:hover,.tb-dev-menu .vp-preset-btn.active{background:#f4f4f5}
365
+ .tb-dev-menu .ft{
366
+ margin-top:10px;padding-top:8px;border-top:1px solid #ececf0;
367
+ display:flex;justify-content:flex-end
368
+ }
369
+ .tb-dev-menu .apply-btn{
370
+ border:1px solid #d4d4d8;background:#f8fafc;color:#111827;border-radius:6px;
371
+ height:30px;padding:0 10px;font-size:12px;font-weight:600;cursor:pointer
372
+ }
373
+ .tb-dev-menu .apply-btn:hover{background:#eef2ff;border-color:#a5b4fc}
276
374
  /* Dark icon buttons */
277
375
  .tb-dk-btn{width:28px;height:28px;background:transparent;border:none;border-radius:5px;cursor:pointer;color:#71717a;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .12s;flex-shrink:0}
278
376
  .tb-dk-btn:hover{color:#e4e4e7;background:rgba(255,255,255,.07)}
@@ -431,7 +529,12 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
431
529
  /* \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 */
432
530
  #device-frame{
433
531
  width:100%;min-height:100%;display:flex;align-items:stretch;
434
- justify-content:center;transition:max-width .3s ease
532
+ justify-content:center;transform-origin:top center;
533
+ transition:max-width .3s ease,width .2s ease,height .2s ease
534
+ }
535
+ #device-frame.desktop{
536
+ max-width:1440px;
537
+ box-shadow:0 0 0 1px var(--border),0 4px 24px rgba(0,0,0,.08)
435
538
  }
436
539
  #device-frame.tablet{
437
540
  max-width:768px;
@@ -832,10 +935,8 @@ select.pr-inp{cursor:pointer;background:#fff}
832
935
  </head>
833
936
  <body class="mode-editor">
834
937
  <div id="app">
835
-
836
938
  <!-- \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 -->
837
939
  <div id="toolbar">
838
-
839
940
  <!-- Left: Logo + Breadcrumb -->
840
941
  <div class="tb-left">
841
942
  <span class="tb-logo">\u2733</span>
@@ -853,14 +954,48 @@ select.pr-inp{cursor:pointer;background:#fff}
853
954
  <span id="dev-label">1440px</span>
854
955
  <i class="bi bi-chevron-down"></i>
855
956
  </div>
856
- <div class="tb-dev-btns">
857
- <button class="tb-dk-btn active" id="dev-desktop" onclick="setDevice('desktop')" title="Desktop \u2014 full width"><i class="bi bi-display"></i></button>
858
- <button class="tb-dk-btn" id="dev-tablet" onclick="setDevice('tablet')" title="Tablet \u2014 768px"><i class="bi bi-tablet"></i></button>
859
- <button class="tb-dk-btn" id="dev-mobile" onclick="setDevice('mobile')" title="Mobile \u2014 390px"><i class="bi bi-phone"></i></button>
860
- <button class="tb-dk-btn" title="More options"><i class="bi bi-three-dots"></i></button>
957
+ <div class="tb-dev-wrap">
958
+ <div class="tb-dev-btns">
959
+ <button class="tb-dk-btn active" id="dev-desktop" onclick="setDevice('desktop')" title="Desktop \u2014 1440x1024"><i class="bi bi-display"></i></button>
960
+ <button class="tb-dk-btn" id="dev-tablet" onclick="setDevice('tablet')" title="Tablet \u2014 768x1024"><i class="bi bi-tablet"></i></button>
961
+ <button class="tb-dk-btn" id="dev-mobile" onclick="setDevice('mobile')" title="Mobile \u2014 390x844"><i class="bi bi-phone"></i></button>
962
+ <button class="tb-dk-btn" id="dev-more-btn" title="More options"><i class="bi bi-three-dots"></i></button>
963
+ </div>
964
+ <div id="dev-more-menu" class="tb-dev-menu" aria-hidden="true">
965
+ <div class="row row-zoom">
966
+ <label for="dev-zoom-level">Zoom</label>
967
+ <input id="dev-zoom-level" type="number" min="25" max="200" step="5" value="100" />
968
+ </div>
969
+ <div class="row row-split row-width height-width-row">
970
+ <div class="row" style="margin:0">
971
+ <label for="dev-custom-width">Width</label>
972
+ <input id="dev-custom-width" type="number" min="240" max="3840" step="1" value="1440" />
973
+ </div>
974
+ <div class="row row-height" style="margin:0">
975
+ <label for="dev-custom-height">Height</label>
976
+ <input id="dev-custom-height" type="number" min="320" max="3840" step="1" value="1024" />
977
+ </div>
978
+ </div>
979
+ <div class="row panel-toggle">
980
+ <label id="left-panel-toggle-label" class="panel-toggle-label">Collapse left panel</label>
981
+ </div>
982
+
983
+ <div class="vp-presets" id="dev-preset-list" aria-label="Device presets">
984
+ <button type="button" class="vp-preset-btn" data-preset="desktop">Desktop</button>
985
+ <button type="button" class="vp-preset-btn" data-preset="mobile">iPhone 13</button>
986
+ <button type="button" class="vp-preset-btn" data-preset="galaxy-s22">Galaxy S22</button>
987
+ <button type="button" class="vp-preset-btn" data-preset="tablet">iPad</button>
988
+ <button type="button" class="vp-preset-btn" data-preset="galaxy-j1">Galaxy J1</button>
989
+ <button type="button" class="vp-preset-btn" data-preset="iphone-7">iPhone 7</button>
990
+ <button type="button" class="vp-preset-btn" data-preset="responsive">Responsive</button>
991
+ </div>
992
+ <div class="ft">
993
+ <button type="button" class="apply-btn" id="dev-apply-btn">Apply</button>
994
+ </div>
995
+ </div>
861
996
  </div>
862
997
  <button class="tb-dk-btn" id="btn-undo" title="Undo (\u2318Z)"><i class="bi bi-arrow-counterclockwise"></i></button>
863
- <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)"><i class="bi bi-arrow-clockwise"></i></button>
998
+ <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)" style="display:none"><i class="bi bi-arrow-clockwise"></i></button>
864
999
  </div>
865
1000
 
866
1001
  <div id="iframe-loading-toolbar" class="tb-page-loading" aria-live="polite" aria-atomic="true">
@@ -931,8 +1066,8 @@ select.pr-inp{cursor:pointer;background:#fff}
931
1066
  <!-- Elements -->
932
1067
  <div class="lp-sec lp-sec-no-border">
933
1068
  <div class="lp-sec-hd">
934
- <span class="lp-sec-hd-left">Elements <i class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
935
- <button class="lp-add-btn" title="Add element">+ Add</button>
1069
+ <span class="lp-sec-hd-left">Elements <i style="display:none" class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
1070
+ <button class="lp-add-btn" id="btn-add-element" title="Add element">+ Add</button>
936
1071
  </div>
937
1072
 
938
1073
  <!-- Search (hidden, kept for JS) -->
@@ -994,7 +1129,7 @@ select.pr-inp{cursor:pointer;background:#fff}
994
1129
  </div>
995
1130
 
996
1131
  <!-- Device frame containing the editing iframe -->
997
- <div id="device-frame">
1132
+ <div id="device-frame" class="desktop">
998
1133
  <iframe id="iframeId" name="iframeId" allowfullscreen></iframe>
999
1134
  </div>
1000
1135
 
@@ -1002,14 +1137,16 @@ select.pr-inp{cursor:pointer;background:#fff}
1002
1137
 
1003
1138
  <!-- Right panel -->
1004
1139
  <div id="right-panel">
1005
- <!-- Left-tab controls moved here -->
1006
- <div class="section-components-tabs">
1007
- <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1008
- <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1009
- </div>
1010
- <div class="lp-body">
1011
- <div id="tab-components" class="tab-pane"></div>
1012
- <div id="tab-sections" class="tab-pane"></div>
1140
+ <div id="section-components-panel" style="display:none">
1141
+ <!-- Left-tab controls moved here -->
1142
+ <div class="section-components-tabs">
1143
+ <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1144
+ <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1145
+ </div>
1146
+ <div class="lp-body">
1147
+ <div id="tab-components" class="tab-pane"></div>
1148
+ <div id="tab-sections" class="tab-pane"></div>
1149
+ </div>
1013
1150
  </div>
1014
1151
  <!-- Element badge (hidden until selection) -->
1015
1152
  <div id="el-info" style="display:none">
@@ -1362,6 +1499,11 @@ var isDirty = false;
1362
1499
  var vvvebReady = false;
1363
1500
  var currentMode = 'editor';
1364
1501
  var currentDevice = 'desktop';
1502
+ var leftPanelCollapsed = false;
1503
+ var viewportPreset = 'desktop';
1504
+ var viewportWidth = 1440;
1505
+ var viewportHeight = 1024;
1506
+ var viewportZoom = 1;
1365
1507
  var selectedEl = null;
1366
1508
  /** Stable selector fingerprint for resilient selection recovery after DOM churn. */
1367
1509
  var selectedElFingerprint = '';
@@ -1614,15 +1756,202 @@ function setMode(mode) {
1614
1756
  }
1615
1757
 
1616
1758
  // \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
1617
- var DEVICE_LABELS = { desktop: '1440px', tablet: '768px', mobile: '390px' };
1759
+ var DEVICE_PRESETS = {
1760
+ desktop: { label: 'Desktop', width: 1440, height: 1024, device: 'desktop' },
1761
+ tablet: { label: 'iPad', width: 768, height: 1024, device: 'tablet' },
1762
+ mobile: { label: 'iPhone 13', width: 390, height: 844, device: 'mobile' },
1763
+ 'galaxy-s22': { label: 'Galaxy S22', width: 360, height: 780, device: 'mobile' },
1764
+ 'iphone-7': { label: 'iPhone 7', width: 375, height: 667, device: 'mobile' },
1765
+ 'galaxy-j1': { label: 'Galaxy J1', width: 360, height: 640, device: 'mobile' },
1766
+ responsive: { label: 'Responsive', width: 1280, height: 800, device: 'desktop' },
1767
+ };
1768
+
1769
+ function clampViewportNumber(v, fallback, min, max) {
1770
+ var n = parseInt(v, 10);
1771
+ if (!Number.isFinite(n)) return fallback;
1772
+ return Math.max(min, Math.min(max, n));
1773
+ }
1774
+
1775
+ function getViewportFitZoom() {
1776
+ var panel = document.getElementById('iframe-panel');
1777
+ var available = panel ? Math.max(260, panel.clientWidth - 24) : viewportWidth;
1778
+ if (!viewportWidth || viewportWidth <= 0) return 1;
1779
+ return Math.min(1, available / viewportWidth);
1780
+ }
1781
+
1782
+ function getAppliedViewportZoom() {
1783
+ var z = Number(viewportZoom);
1784
+ if (!Number.isFinite(z) || z <= 0) z = 1;
1785
+ z = Math.max(0.25, Math.min(2, z));
1786
+ return Math.min(z, getViewportFitZoom());
1787
+ }
1788
+
1789
+ function updateViewportLabel() {
1790
+ var lbl = document.getElementById('dev-label');
1791
+ if (!lbl) return;
1792
+ var zoomPct = Math.round(getAppliedViewportZoom() * 100);
1793
+ lbl.textContent = viewportWidth + 'x' + viewportHeight + ' \xB7 ' + zoomPct + '%';
1794
+ }
1795
+
1796
+ function syncViewportMenuControls() {
1797
+ var presetButtons = document.querySelectorAll('#dev-preset-list .vp-preset-btn');
1798
+ var widthInp = document.getElementById('dev-custom-width');
1799
+ var heightInp = document.getElementById('dev-custom-height');
1800
+ var zoomInp = document.getElementById('dev-zoom-level');
1801
+ if (presetButtons && presetButtons.length) {
1802
+ for (var i = 0; i < presetButtons.length; i++) {
1803
+ var key = presetButtons[i].getAttribute('data-preset') || '';
1804
+ presetButtons[i].classList.toggle('active', key === viewportPreset);
1805
+ }
1806
+ }
1807
+ if (widthInp) widthInp.value = String(viewportWidth);
1808
+ if (heightInp) heightInp.value = String(viewportHeight);
1809
+ if (zoomInp) zoomInp.value = String(Math.round(getAppliedViewportZoom() * 100));
1810
+ }
1811
+
1812
+ function applyViewportFrame() {
1813
+ var frame = document.getElementById('device-frame');
1814
+ var iframe = document.getElementById('iframeId');
1815
+ if (!frame) return;
1816
+ frame.className = currentDevice;
1817
+ frame.style.width = viewportWidth + 'px';
1818
+ frame.style.height = viewportHeight + 'px';
1819
+ frame.style.maxWidth = 'none';
1820
+ frame.style.zoom = String(getAppliedViewportZoom());
1821
+ if (iframe) {
1822
+ iframe.style.height = viewportHeight + 'px';
1823
+ iframe.style.minHeight = viewportHeight + 'px';
1824
+ }
1825
+ updateViewportLabel();
1826
+ syncViewportMenuControls();
1827
+ if (selectedEl && currentMode === 'editor') requestAnimationFrame(function() { positionSelectionToolbar(); });
1828
+ }
1829
+
1830
+ function setViewportPreset(presetKey) {
1831
+ var preset = DEVICE_PRESETS[presetKey];
1832
+ if (!preset) return;
1833
+ viewportPreset = presetKey;
1834
+ viewportWidth = preset.width;
1835
+ viewportHeight = preset.height;
1836
+ currentDevice = preset.device || 'desktop';
1837
+ ['desktop','tablet','mobile'].forEach(function(d) {
1838
+ document.getElementById('dev-' + d).classList.toggle('active', d === currentDevice);
1839
+ });
1840
+ applyViewportFrame();
1841
+ }
1842
+
1618
1843
  function setDevice(device) {
1844
+ viewportPreset = device;
1619
1845
  currentDevice = device;
1620
- var frame = document.getElementById('device-frame');
1621
- frame.className = device === 'desktop' ? '' : device;
1846
+ var preset = DEVICE_PRESETS[device] || DEVICE_PRESETS.desktop;
1847
+ viewportWidth = preset.width;
1848
+ viewportHeight = preset.height;
1622
1849
  ['desktop','tablet','mobile'].forEach(function(d) {
1623
1850
  document.getElementById('dev-' + d).classList.toggle('active', d === device);
1624
1851
  });
1625
- document.getElementById('dev-label').textContent = DEVICE_LABELS[device] || device;
1852
+ applyViewportFrame();
1853
+ }
1854
+
1855
+ function setViewportCustomFromInputs() {
1856
+ var widthInp = document.getElementById('dev-custom-width');
1857
+ var heightInp = document.getElementById('dev-custom-height');
1858
+ var zoomInp = document.getElementById('dev-zoom-level');
1859
+ viewportWidth = clampViewportNumber(widthInp ? widthInp.value : '', viewportWidth, 240, 3840);
1860
+ viewportHeight = clampViewportNumber(heightInp ? heightInp.value : '', viewportHeight, 320, 3840);
1861
+ var z = clampViewportNumber(zoomInp ? zoomInp.value : '', Math.round(getAppliedViewportZoom() * 100), 25, 200);
1862
+ viewportZoom = z / 100;
1863
+ viewportPreset = 'custom';
1864
+ currentDevice = viewportWidth <= 480 ? 'mobile' : (viewportWidth <= 1024 ? 'tablet' : 'desktop');
1865
+ ['desktop','tablet','mobile'].forEach(function(d) {
1866
+ document.getElementById('dev-' + d).classList.toggle('active', d === currentDevice);
1867
+ });
1868
+ applyViewportFrame();
1869
+ }
1870
+
1871
+ function closeViewportMenu() {
1872
+ var menu = document.getElementById('dev-more-menu');
1873
+ if (!menu) return;
1874
+ menu.classList.remove('open');
1875
+ menu.setAttribute('aria-hidden', 'true');
1876
+ }
1877
+
1878
+ function toggleViewportMenu() {
1879
+ var menu = document.getElementById('dev-more-menu');
1880
+ if (!menu) return;
1881
+ var shouldOpen = !menu.classList.contains('open');
1882
+ menu.classList.toggle('open', shouldOpen);
1883
+ menu.setAttribute('aria-hidden', shouldOpen ? 'false' : 'true');
1884
+ }
1885
+
1886
+ function bindViewportControls() {
1887
+ var btn = document.getElementById('dev-more-btn');
1888
+ var menu = document.getElementById('dev-more-menu');
1889
+ var leftPanelToggleLabel = document.getElementById('left-panel-toggle-label');
1890
+ var presetButtons = document.querySelectorAll('#dev-preset-list .vp-preset-btn');
1891
+ var applyBtn = document.getElementById('dev-apply-btn');
1892
+ var zoomInp = document.getElementById('dev-zoom-level');
1893
+ var viewportBtn = document.querySelector('.tb-viewport');
1894
+ function updateLeftPanelToggleLabel() {
1895
+ if (!leftPanelToggleLabel) return;
1896
+ leftPanelToggleLabel.textContent = leftPanelCollapsed ? 'Expand left panel' : 'Collapse left panel';
1897
+ }
1898
+ function setLeftPanelCollapsed(collapsed) {
1899
+ var panel = document.getElementById('left-panel');
1900
+ leftPanelCollapsed = !!collapsed;
1901
+ if (panel) panel.style.display = leftPanelCollapsed ? 'none' : '';
1902
+ updateLeftPanelToggleLabel();
1903
+ applyViewportFrame();
1904
+ }
1905
+ if (leftPanelToggleLabel) {
1906
+ leftPanelToggleLabel.addEventListener('click', function(e) {
1907
+ e.preventDefault();
1908
+ e.stopPropagation();
1909
+ setLeftPanelCollapsed(!leftPanelCollapsed);
1910
+ });
1911
+ }
1912
+ if (btn) btn.addEventListener('click', function(e) {
1913
+ e.preventDefault();
1914
+ e.stopPropagation();
1915
+ toggleViewportMenu();
1916
+ });
1917
+ if (viewportBtn) viewportBtn.addEventListener('click', function(e) {
1918
+ e.preventDefault();
1919
+ e.stopPropagation();
1920
+ toggleViewportMenu();
1921
+ });
1922
+ if (applyBtn) applyBtn.addEventListener('click', function(e) {
1923
+ e.preventDefault();
1924
+ setViewportCustomFromInputs();
1925
+ closeViewportMenu();
1926
+ });
1927
+ if (zoomInp) {
1928
+ zoomInp.addEventListener('change', function() {
1929
+ var z = clampViewportNumber(zoomInp.value, Math.round(getAppliedViewportZoom() * 100), 25, 200);
1930
+ viewportZoom = z / 100;
1931
+ applyViewportFrame();
1932
+ });
1933
+ }
1934
+ if (presetButtons && presetButtons.length) {
1935
+ for (var i = 0; i < presetButtons.length; i++) {
1936
+ presetButtons[i].addEventListener('click', function(e) {
1937
+ e.preventDefault();
1938
+ var key = this.getAttribute('data-preset');
1939
+ if (!key) return;
1940
+ setViewportPreset(key);
1941
+ });
1942
+ }
1943
+ }
1944
+ document.addEventListener('click', function(e) {
1945
+ if (!menu || !menu.classList.contains('open')) return;
1946
+ if (menu.contains(e.target) || (btn && btn.contains(e.target)) || (viewportBtn && viewportBtn.contains(e.target))) return;
1947
+ closeViewportMenu();
1948
+ });
1949
+ document.addEventListener('keydown', function(e) {
1950
+ if (e.key === 'Escape') closeViewportMenu();
1951
+ });
1952
+ window.addEventListener('resize', function() { applyViewportFrame(); });
1953
+ updateLeftPanelToggleLabel();
1954
+ applyViewportFrame();
1626
1955
  }
1627
1956
 
1628
1957
  // \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
@@ -1666,6 +1995,19 @@ function switchSectionComponentsTab(tab) {
1666
1995
  renderSidebar(inp ? inp.value : '');
1667
1996
  }
1668
1997
 
1998
+ function toggleSectionComponentsPanel(forceVisible) {
1999
+ var panel = document.getElementById('section-components-panel');
2000
+ if (!panel) return;
2001
+ var shouldShow =
2002
+ typeof forceVisible === 'boolean'
2003
+ ? forceVisible
2004
+ : panel.style.display === 'none';
2005
+ panel.style.display = shouldShow ? '' : 'none';
2006
+ if (shouldShow) {
2007
+ switchSectionComponentsTab(currentSectionComponentsTab || 'components');
2008
+ }
2009
+ }
2010
+
1669
2011
  // \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
1670
2012
  function toggleAcc(name) {
1671
2013
  var sec = document.getElementById('acc-' + name);
@@ -1771,9 +2113,60 @@ function getOriginalValue(inputId, el) {
1771
2113
  }
1772
2114
  }
1773
2115
 
2116
+ var PX_DEFAULT_INPUT_IDS = {
2117
+ 'pp-mt': true, 'pp-mr': true, 'pp-mb': true, 'pp-ml': true,
2118
+ 'pp-pt': true, 'pp-pr': true, 'pp-pb': true, 'pp-pl': true,
2119
+ };
2120
+
2121
+ var PX_DEFAULT_CSS_PROPS = {
2122
+ 'margin-top': true, 'margin-right': true, 'margin-bottom': true, 'margin-left': true,
2123
+ 'padding-top': true, 'padding-right': true, 'padding-bottom': true, 'padding-left': true,
2124
+ };
2125
+
2126
+ function normalizeCssValueForProperty(prop, value) {
2127
+ var p = prop == null ? '' : String(prop).trim();
2128
+ var pKebab = p
2129
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
2130
+ .replace(/_/g, '-')
2131
+ .replace(/!important/gi, '')
2132
+ .replace(/[^a-zA-Z-]/g, '')
2133
+ .toLowerCase();
2134
+ var raw = value == null ? '' : String(value).trim();
2135
+ var rawClean = raw.replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim();
2136
+ var numericCandidate = rawClean.replace(/[, ]+/g, '');
2137
+ var isSpacingProp = /^(?:margin|padding)(?:-(?:top|right|bottom|left))?$/.test(pKebab);
2138
+ if (!pKebab || !rawClean) return rawClean;
2139
+ var numericValue = Number(numericCandidate);
2140
+ var shouldAddPx =
2141
+ isSpacingProp &&
2142
+ numericCandidate !== '' &&
2143
+ Number.isFinite(numericValue) &&
2144
+ /^[-+]?(?:[0-9]+(?:[.][0-9]+)?|[.][0-9]+)$/.test(numericCandidate);
2145
+ var out = shouldAddPx ? numericCandidate + 'px' : rawClean;
2146
+ return out;
2147
+ }
2148
+
2149
+ function normalizeChainSetRowUnits(row) {
2150
+ if (!row) return row;
2151
+ if (normalizeChangesetType(row) === 'style') {
2152
+ row.value = normalizeCssValueForProperty(row.property || row.cssProp, row.value);
2153
+ }
2154
+ return row;
2155
+ }
2156
+
2157
+ function normalizeLoggedValue(inputId, value) {
2158
+ var raw = value == null ? '' : String(value).trim();
2159
+ if (!raw) return raw;
2160
+ if (PX_DEFAULT_INPUT_IDS[inputId] && /^-?d+(?:.d+)?$/.test(raw)) {
2161
+ return raw + 'px';
2162
+ }
2163
+ return raw;
2164
+ }
2165
+
1774
2166
  function logChange(selector, inputId, value, targetEl, originalValue) {
1775
2167
  var meta = PROP_META[inputId];
1776
2168
  if (!meta) return;
2169
+ value = normalizeLoggedValue(inputId, value);
1777
2170
  var key = selector + '||' + inputId;
1778
2171
  // Skip trivially empty / reset values \u2014 remove from log if present
1779
2172
  if (value === '' || value === 'none' || value === 'auto' || value === 'normal') {
@@ -1875,6 +2268,24 @@ function syncDesignInput(change) {
1875
2268
  function removeStateChange(idx) {
1876
2269
  var change = stateChanges[idx];
1877
2270
  if (!change) return;
2271
+ if (change.isStructuralLive) {
2272
+ removeSessionStructuralRowByTimestamp(
2273
+ change.structuralVarId || activeVarId,
2274
+ change.vveTs,
2275
+ );
2276
+ stateChanges.splice(idx, 1);
2277
+ commitStateChangesForActiveVariation();
2278
+ renderStatesTab();
2279
+ if (currentMainTab === 'history') renderHistoryTab();
2280
+ try {
2281
+ delete varHtmlCache[activeVarId];
2282
+ } catch(_) {}
2283
+ appliedStructuralChangesetKeys = {};
2284
+ recomputeEditorDirty();
2285
+ scheduleDomTreeRefresh();
2286
+ softReloadEditorIframe();
2287
+ return;
2288
+ }
1878
2289
  revertChangeOnDom(change);
1879
2290
  syncDesignInput(change);
1880
2291
  stateChanges.splice(idx, 1);
@@ -2231,6 +2642,14 @@ function getLatestHistoryUndoTarget() {
2231
2642
  return list.length ? list[0] : null;
2232
2643
  }
2233
2644
 
2645
+ function getLatestLiveUndoTarget() {
2646
+ var list = getUnifiedHistoryItems();
2647
+ for (var i = 0; i < list.length; i++) {
2648
+ if (list[i] && list[i].source === 'live') return list[i];
2649
+ }
2650
+ return null;
2651
+ }
2652
+
2234
2653
  function changesetListHasStructural(arr) {
2235
2654
  if (!arr || !arr.length) return false;
2236
2655
  for (var i = 0; i < arr.length; i++) {
@@ -2329,8 +2748,15 @@ function removeHistoryChangeset(idx, evt) {
2329
2748
  function clearAllHistoryChangesets() {
2330
2749
  var v = getActiveVariationForHistory();
2331
2750
  if (!v) return;
2332
- if (!parseVariationChangesets(v).length) return;
2751
+ var hasSavedChangesets = parseVariationChangesets(v).length > 0;
2752
+ var hasSessionStructural =
2753
+ !!(activeVarId && sessionStructuralChainRowsByVarId[activeVarId] && sessionStructuralChainRowsByVarId[activeVarId].length);
2754
+ if (!hasSavedChangesets && !hasSessionStructural) return;
2755
+
2333
2756
  persistActiveVariationChangesets([]);
2757
+ if (activeVarId) {
2758
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2759
+ }
2334
2760
  appliedChangesetSnapshots = {};
2335
2761
  appliedStructuralChangesetKeys = {};
2336
2762
  try {
@@ -2343,6 +2769,10 @@ function clearAllHistoryChangesets() {
2343
2769
  }
2344
2770
 
2345
2771
  function clearAllUnifiedHistory() {
2772
+ if (activeVarId) {
2773
+ // Ensure structural unsaved rows are also removed by "Clear all changes".
2774
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2775
+ }
2346
2776
  clearAllStates();
2347
2777
  clearAllHistoryChangesets();
2348
2778
  if (currentMainTab === 'history') renderHistoryTab();
@@ -2354,15 +2784,32 @@ var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
2354
2784
 
2355
2785
  function clearVisualEditorLocalStorage() {
2356
2786
  try {
2357
- for (var i = localStorage.length - 1; i >= 0; i--) {
2358
- var k = localStorage.key(i);
2359
- if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2360
- localStorage.removeItem(k);
2787
+ var stores = [];
2788
+ try { stores.push(localStorage); } catch(_) {}
2789
+ try { stores.push(sessionStorage); } catch(_) {}
2790
+ for (var si = 0; si < stores.length; si++) {
2791
+ var store = stores[si];
2792
+ if (!store) continue;
2793
+ for (var i = store.length - 1; i >= 0; i--) {
2794
+ var k = store.key(i);
2795
+ if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2796
+ store.removeItem(k);
2797
+ }
2361
2798
  }
2362
2799
  }
2363
2800
  } catch(_) {}
2364
2801
  }
2365
2802
 
2803
+ function clearPersistedActiveVariationForData(data) {
2804
+ try {
2805
+ var sk = activeVariationStorageKeyFromPayload(data);
2806
+ if (sk && sk !== VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') {
2807
+ try { localStorage.removeItem(sk); } catch(_) {}
2808
+ try { sessionStorage.removeItem(sk); } catch(_) {}
2809
+ }
2810
+ } catch(_) {}
2811
+ }
2812
+
2366
2813
  function activeVariationStorageKeyFromPayload(data) {
2367
2814
  return (
2368
2815
  VVE_LOCAL_STORAGE_PREFIX +
@@ -2401,22 +2848,41 @@ function writePersistedActiveVariationId(varId) {
2401
2848
  * @param allowPrevMemory when true, keep in-session activeVarId if still valid (skip-reload path).
2402
2849
  */
2403
2850
  function pickActiveVariationIdForLoad(data, variationsArr, prevMemoryId, allowPrevMemory) {
2404
- var baseline = variationsArr.find(function(v) { return v.baseline; });
2405
- var fallback = (baseline || variationsArr[0] || {})._id || null;
2851
+ var firstNonBaseline = variationsArr.find(function(v) { return !v.baseline; });
2852
+ var fallback = (firstNonBaseline || variationsArr[0] || {})._id || null;
2406
2853
  if (!variationsArr.length) return null;
2407
2854
  if (allowPrevMemory && prevMemoryId && variationsArr.some(function(v) { return v._id === prevMemoryId; })) {
2408
2855
  return prevMemoryId;
2409
2856
  }
2410
2857
  var stored = readPersistedActiveVariationId(data);
2411
2858
  if (stored && variationsArr.some(function(v) { return v._id === stored; })) {
2859
+ var storedVar = variationsArr.find(function(v) { return v._id === stored; });
2860
+ if (storedVar && storedVar.baseline && firstNonBaseline && firstNonBaseline._id) {
2861
+ return firstNonBaseline._id;
2862
+ }
2412
2863
  return stored;
2413
2864
  }
2414
2865
  return fallback;
2415
2866
  }
2416
2867
 
2868
+ function updateExperimentNameLabel(data) {
2869
+ var el = document.getElementById('tb-exp-name');
2870
+ if (!el) return;
2871
+ var name = '';
2872
+ try {
2873
+ name = data && data.name != null ? String(data.name).trim() : '';
2874
+ } catch(_) {
2875
+ name = '';
2876
+ }
2877
+ if (!name) name = 'Visual Editor';
2878
+ el.textContent = name;
2879
+ el.title = name;
2880
+ }
2881
+
2417
2882
  // \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
2418
2883
  function handleLoadExperiment(data) {
2419
2884
  clearPendingGranularChangesets();
2885
+ updateExperimentNameLabel(data);
2420
2886
  var prevKey = experimentData
2421
2887
  ? String(experimentData.experimentId || '') + '|' + String(experimentData.pageUrl || '')
2422
2888
  : '';
@@ -2632,7 +3098,13 @@ function granularAnySelectorMatches(doc, cs) {
2632
3098
 
2633
3099
  /** Bust bfcache / same-URL no-op reloads so the iframe actually re-parses (loading \u2192 interactive). */
2634
3100
  function appendIframeReloadBust(url) {
2635
- return url;
3101
+ try {
3102
+ var u = new URL(String(url || ''), window.location.href);
3103
+ u.searchParams.set('_vve_reload', String(Date.now()));
3104
+ return u.toString();
3105
+ } catch(_) {
3106
+ return url;
3107
+ }
2636
3108
  }
2637
3109
 
2638
3110
  // True when the iframe contentDocument belongs to the current iframe.src navigation.
@@ -3010,11 +3482,55 @@ function appendSessionStructuralChainRow(varId, row) {
3010
3482
  sessionStructuralChainRowsByVarId[varId].push(row);
3011
3483
  }
3012
3484
 
3485
+ function markStructuralRowApplied(row) {
3486
+ try {
3487
+ var k = structuralChangesetDedupKey(row);
3488
+ if (k) appliedStructuralChangesetKeys[k] = true;
3489
+ } catch(_) {}
3490
+ }
3491
+
3492
+ function logStructuralStateChange(row, label, value, targetEl) {
3493
+ if (!row || !row.selector) return;
3494
+ stateChanges.push({
3495
+ selector: row.selector,
3496
+ inputId: 'vve-struct',
3497
+ label: label || 'Structure change',
3498
+ cssProp: null,
3499
+ value: value != null ? String(value) : String(row.type || ''),
3500
+ targetEl: targetEl || null,
3501
+ originalValue: '',
3502
+ vveTs: row.vveTs || nextHistoryTimestamp(),
3503
+ isStructuralLive: true,
3504
+ structuralVarId: activeVarId || null,
3505
+ structuralType: row.type || '',
3506
+ });
3507
+ if (currentMainTab === 'history') renderHistoryTab();
3508
+ commitStateChangesForActiveVariation();
3509
+ }
3510
+
3511
+ function removeSessionStructuralRowByTimestamp(varId, ts) {
3512
+ if (!varId || !ts) return;
3513
+ var arr = sessionStructuralChainRowsByVarId[varId];
3514
+ if (!arr || !arr.length) return;
3515
+ for (var i = arr.length - 1; i >= 0; i--) {
3516
+ if (arr[i] && arr[i].vveTs === ts) {
3517
+ arr.splice(i, 1);
3518
+ return;
3519
+ }
3520
+ }
3521
+ }
3522
+
3013
3523
  /** One States-tab row -> Conversion.io chain-set shape (matches applyChangesetEntry). */
3014
3524
  function stateChangeToChainSet(c) {
3015
3525
  if (!c || !c.selector) return null;
3016
3526
  if (c.cssProp) {
3017
- return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
3527
+ return {
3528
+ selector: c.selector,
3529
+ type: 'style',
3530
+ property: c.cssProp,
3531
+ value: normalizeCssValueForProperty(c.cssProp, c.value),
3532
+ vveTs: c.vveTs || nextHistoryTimestamp()
3533
+ };
3018
3534
  }
3019
3535
  switch (c.inputId) {
3020
3536
  case 'pp-text':
@@ -3186,7 +3702,7 @@ function buildPersistentStyleRulesForActiveVariation() {
3186
3702
  if (value == null || value === '') return;
3187
3703
  var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
3188
3704
  var pr = String(prop).trim();
3189
- var val = String(value).trim();
3705
+ var val = normalizeCssValueForProperty(pr, value);
3190
3706
  if (!sel || !pr || !val) return;
3191
3707
  var k = sel + '__vve_sep__' + pr;
3192
3708
  if (!map[k]) order.push(k);
@@ -3224,7 +3740,8 @@ function buildPersistentStyleRulesForActiveVariation() {
3224
3740
  var lines = [];
3225
3741
  for (var oi = 0; oi < order.length; oi++) {
3226
3742
  var row = map[order[oi]];
3227
- lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
3743
+ var outVal = normalizeCssValueForProperty(row.property, row.value);
3744
+ lines.push(row.selector + ' { ' + row.property + ': ' + outVal + ' !important; }');
3228
3745
  }
3229
3746
  return lines.join('\\n');
3230
3747
  }
@@ -3375,7 +3892,9 @@ function buildPersistedChainSetsForVariation(v) {
3375
3892
  var row = stateChangeToChainSet(sourceStateChanges[si]);
3376
3893
  if (row) overlay.push(row);
3377
3894
  }
3378
- return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
3895
+ var merged = mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
3896
+ for (var mi = 0; mi < merged.length; mi++) normalizeChainSetRowUnits(merged[mi]);
3897
+ return merged;
3379
3898
  }
3380
3899
 
3381
3900
  /**
@@ -3704,12 +4223,15 @@ function duplicateSelectedEl() {
3704
4223
  clone.setAttribute('data-vve-instance', generateVveInstanceId());
3705
4224
  } catch(_) {}
3706
4225
  if (activeVarId) {
3707
- appendSessionStructuralChainRow(activeVarId, {
4226
+ var dupRow = {
3708
4227
  selector: anchorSel,
3709
4228
  type: 'insert',
3710
4229
  action: 'after',
3711
4230
  html: clone.outerHTML,
3712
- });
4231
+ };
4232
+ appendSessionStructuralChainRow(activeVarId, dupRow);
4233
+ markStructuralRowApplied(dupRow);
4234
+ logStructuralStateChange(dupRow, 'Duplicated via toolbar', 'Duplicate inserted', selectedEl);
3713
4235
  }
3714
4236
  selectedEl.parentNode.insertBefore(clone, selectedEl.nextSibling);
3715
4237
  saveCurrentVariationHtml();
@@ -3725,23 +4247,27 @@ function toggleHideSelectedEl() {
3725
4247
  selectedEl.style.visibility = '';
3726
4248
  selectedEl.removeAttribute('data-vve-hidden');
3727
4249
  if (activeVarId) {
3728
- appendSessionStructuralChainRow(activeVarId, {
4250
+ var showRow = {
3729
4251
  selector: hidSel,
3730
4252
  type: 'style',
3731
4253
  property: 'visibility',
3732
4254
  value: '',
3733
- });
4255
+ };
4256
+ appendSessionStructuralChainRow(activeVarId, showRow);
4257
+ logStructuralStateChange(showRow, 'Shown via toolbar', 'Visibility restored', selectedEl);
3734
4258
  }
3735
4259
  } else {
3736
4260
  selectedEl.style.visibility = 'hidden';
3737
4261
  selectedEl.setAttribute('data-vve-hidden', '1');
3738
4262
  if (activeVarId) {
3739
- appendSessionStructuralChainRow(activeVarId, {
4263
+ var hideRow = {
3740
4264
  selector: hidSel,
3741
4265
  type: 'style',
3742
4266
  property: 'visibility',
3743
4267
  value: 'hidden',
3744
- });
4268
+ };
4269
+ appendSessionStructuralChainRow(activeVarId, hideRow);
4270
+ logStructuralStateChange(hideRow, 'Hidden via toolbar', 'Visibility set to hidden', selectedEl);
3745
4271
  }
3746
4272
  }
3747
4273
  saveCurrentVariationHtml();
@@ -3752,7 +4278,11 @@ function deleteSelectedEl() {
3752
4278
  if (!selectedEl || !selectedEl.parentNode) return;
3753
4279
  var delSel = buildSelector(selectedEl);
3754
4280
  selectedEl.remove();
3755
- if (activeVarId) appendSessionStructuralChainRow(activeVarId, { selector: delSel, type: 'remove' });
4281
+ if (activeVarId) {
4282
+ var delRow = { selector: delSel, type: 'remove' };
4283
+ appendSessionStructuralChainRow(activeVarId, delRow);
4284
+ logStructuralStateChange(delRow, 'Deleted via toolbar', 'Element removed', null);
4285
+ }
3756
4286
  saveCurrentVariationHtml();
3757
4287
  recomputeEditorDirty();
3758
4288
  deselectElement();
@@ -4653,19 +5183,25 @@ function recordReorderAfterDrag(movedEl) {
4653
5183
  var prev = movedEl.previousElementSibling;
4654
5184
  var next = movedEl.nextElementSibling;
4655
5185
  if (prev) {
4656
- appendSessionStructuralChainRow(activeVarId, {
5186
+ var reorderAfterRow = {
4657
5187
  selector: buildSelector(movedEl),
4658
5188
  type: 'reorder',
4659
5189
  targetSelector: buildSelector(prev),
4660
5190
  action: 'after',
4661
- });
5191
+ };
5192
+ appendSessionStructuralChainRow(activeVarId, reorderAfterRow);
5193
+ markStructuralRowApplied(reorderAfterRow);
5194
+ logStructuralStateChange(reorderAfterRow, 'Reordered via drag', 'Moved after sibling', movedEl);
4662
5195
  } else if (next) {
4663
- appendSessionStructuralChainRow(activeVarId, {
5196
+ var reorderBeforeRow = {
4664
5197
  selector: buildSelector(movedEl),
4665
5198
  type: 'reorder',
4666
5199
  targetSelector: buildSelector(next),
4667
5200
  action: 'before',
4668
- });
5201
+ };
5202
+ appendSessionStructuralChainRow(activeVarId, reorderBeforeRow);
5203
+ markStructuralRowApplied(reorderBeforeRow);
5204
+ logStructuralStateChange(reorderBeforeRow, 'Reordered via drag', 'Moved before sibling', movedEl);
4669
5205
  }
4670
5206
  }
4671
5207
 
@@ -4946,12 +5482,15 @@ function insertHtml(html) {
4946
5482
  }
4947
5483
  if (firstEl) selectElement(firstEl);
4948
5484
  if (activeVarId) {
4949
- appendSessionStructuralChainRow(activeVarId, {
5485
+ var insertRow = {
4950
5486
  selector: anchorSel,
4951
5487
  type: 'insert',
4952
5488
  action: 'after',
4953
5489
  html: htmlStr,
4954
- });
5490
+ };
5491
+ appendSessionStructuralChainRow(activeVarId, insertRow);
5492
+ markStructuralRowApplied(insertRow);
5493
+ logStructuralStateChange(insertRow, 'Added component/section', 'Inserted new element', firstEl || selectedEl);
4955
5494
  }
4956
5495
  saveCurrentVariationHtml();
4957
5496
  recomputeEditorDirty();
@@ -5041,6 +5580,14 @@ document.getElementById('comp-search').addEventListener('input', function() {
5041
5580
  renderSidebar(this.value);
5042
5581
  }
5043
5582
  });
5583
+ var btnAddElement = document.getElementById('btn-add-element');
5584
+ if (btnAddElement) {
5585
+ btnAddElement.addEventListener('click', function(e) {
5586
+ e.preventDefault();
5587
+ e.stopPropagation();
5588
+ toggleSectionComponentsPanel();
5589
+ });
5590
+ }
5044
5591
 
5045
5592
  // \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
5046
5593
  document.getElementById('btn-save').addEventListener('click', handleSave);
@@ -5081,11 +5628,21 @@ function handleSave() {
5081
5628
  }
5082
5629
 
5083
5630
  function handleClose() {
5631
+ clearPersistedActiveVariationForData(experimentData);
5084
5632
  clearVisualEditorLocalStorage();
5085
5633
  // Unsaved-changes UX lives in the parent (PlatformVisualEditorV2); avoid double confirm here.
5086
5634
  send('close-editor', {});
5087
5635
  }
5088
5636
 
5637
+ // Defensive cleanup: if parent closes/unmounts the editor shell without
5638
+ // invoking handleClose(), clear persisted VVE keys on unload as well.
5639
+ window.addEventListener('pagehide', function() {
5640
+ clearVisualEditorLocalStorage();
5641
+ });
5642
+ window.addEventListener('beforeunload', function() {
5643
+ clearVisualEditorLocalStorage();
5644
+ });
5645
+
5089
5646
  // \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
5090
5647
  function isNativeEditableTarget(target) {
5091
5648
  if (!target || target.nodeType !== 1) return false;
@@ -5107,10 +5664,7 @@ document.addEventListener('keydown', function(e) {
5107
5664
  e.preventDefault();
5108
5665
  runEditorUndo();
5109
5666
  }
5110
- if (meta && e.shiftKey && k === 'z') {
5111
- e.preventDefault();
5112
- runEditorRedo();
5113
- }
5667
+ // Redo is intentionally hidden/disabled in this editor flow.
5114
5668
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
5115
5669
  if (e.key === 'Escape') {
5116
5670
  var openTips = document.querySelectorAll('.ve-pl-tip.is-tip-open');
@@ -5130,11 +5684,17 @@ document.addEventListener('keydown', function(e) {
5130
5684
  }
5131
5685
  });
5132
5686
  function runEditorUndo() {
5133
- var target = getLatestHistoryUndoTarget();
5134
- if (target) {
5135
- removeHistoryItem(target.source, target.idx);
5687
+ // Undo only unsaved in-session edits; do not remove persisted history rows.
5688
+ if (!isDirty) return;
5689
+
5690
+ // 1) Prefer live unsaved change stack (stateChanges shown in History tab).
5691
+ var liveTarget = getLatestLiveUndoTarget();
5692
+ if (liveTarget) {
5693
+ removeHistoryItem('live', liveTarget.idx);
5136
5694
  return;
5137
5695
  }
5696
+
5697
+ // 2) Fallback to Vvveb internal undo stack (e.g. structural drag/drop ops).
5138
5698
  if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
5139
5699
  Vvveb.Undo.undo();
5140
5700
  saveCurrentVariationHtml();
@@ -5157,11 +5717,10 @@ document.getElementById('btn-undo').addEventListener('click', function(e) {
5157
5717
  e.stopPropagation();
5158
5718
  runEditorUndo();
5159
5719
  });
5160
- document.getElementById('btn-redo').addEventListener('click', function(e) {
5161
- e.preventDefault();
5162
- e.stopPropagation();
5163
- runEditorRedo();
5164
- });
5720
+ var btnRedo = document.getElementById('btn-redo');
5721
+ if (btnRedo) {
5722
+ btnRedo.style.display = 'none';
5723
+ }
5165
5724
 
5166
5725
  function layoutLoadingTooltip(host) {
5167
5726
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -5237,6 +5796,7 @@ function registerCROSections() {
5237
5796
 
5238
5797
  window.addEventListener('load', function() {
5239
5798
  registerCROSections();
5799
+ bindViewportControls();
5240
5800
  switchSectionComponentsTab(currentSectionComponentsTab);
5241
5801
  renderElementsTree(document.getElementById('comp-search').value);
5242
5802
  vvvebReady = true;
@@ -5944,8 +6504,32 @@ ${runtimePreflightScript}
5944
6504
  var TARGET_ORIGIN=${JSON.stringify(origin)};
5945
6505
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
5946
6506
  var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
5947
- 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("//");}
5948
- 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;}}
6507
+ var _proxyCtx=(function(){try{return new URL(window.location.href);}catch(_){return null;}})();
6508
+ var PROXY_ROOT="/api/conversion-proxy";
6509
+ var PROXY_PASSWORD=_proxyCtx?_proxyCtx.searchParams.get("password")||"":"";
6510
+ var PROXY_BASE_URL=_proxyCtx?_proxyCtx.searchParams.get("conversionProxyBaseUrl")||"":"";
6511
+ var PROXY_TRACKING_MARKERS=_proxyCtx?_proxyCtx.searchParams.get("trackingMarkers")||"":"";
6512
+ var PROXY_STRICT_FREEZE=_proxyCtx?_proxyCtx.searchParams.get("strictObserverFreeze")||"":"";
6513
+ var PROXY_UPSTREAM_MODE=_proxyCtx?_proxyCtx.searchParams.get("proxy")||"":"";
6514
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
6515
+ function toProxyNetworkUrl(raw){
6516
+ if(isSkippable(raw))return raw;
6517
+ try{
6518
+ var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;
6519
+ var abs=new URL(raw,base);
6520
+ if(abs.origin!==TARGET_ORIGIN)return raw;
6521
+ var prox=new URL(PROXY_ROOT,window.location.origin);
6522
+ prox.searchParams.set("password",PROXY_PASSWORD||"");
6523
+ prox.searchParams.set("url",abs.toString());
6524
+ if(PROXY_BASE_URL)prox.searchParams.set("conversionProxyBaseUrl",PROXY_BASE_URL);
6525
+ if(PROXY_TRACKING_MARKERS)prox.searchParams.set("trackingMarkers",PROXY_TRACKING_MARKERS);
6526
+ if(PROXY_STRICT_FREEZE)prox.searchParams.set("strictObserverFreeze",PROXY_STRICT_FREEZE);
6527
+ if(PROXY_UPSTREAM_MODE)prox.searchParams.set("proxy",PROXY_UPSTREAM_MODE);
6528
+ return prox.toString();
6529
+ }catch(_){
6530
+ return raw;
6531
+ }
6532
+ }
5949
6533
  function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
5950
6534
  function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}
5951
6535
  function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
@@ -5958,9 +6542,9 @@ if(window.fetch){
5958
6542
  var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
5959
6543
  if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
5960
6544
  if(typeof input==="string"){
5961
- input=toAbsoluteOriginUrl(input);
6545
+ input=toProxyNetworkUrl(input);
5962
6546
  }else if(input&&input.url){
5963
- var next=toAbsoluteOriginUrl(input.url);
6547
+ var next=toProxyNetworkUrl(input.url);
5964
6548
  if(next!==input.url){input=new Request(next,input);}
5965
6549
  }
5966
6550
  afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
@@ -5970,8 +6554,22 @@ if(window.fetch){
5970
6554
  try{
5971
6555
  var u=afterUrl?resolveUrl(afterUrl):null;
5972
6556
  var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
5973
- var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
5974
- if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
6557
+ var reqMethod=(init&&init.method?String(init.method):(input&&input.method?String(input.method):"GET")).toUpperCase();
6558
+ var isSafeMethod=reqMethod==="GET"||reqMethod==="HEAD";
6559
+ var path=(u&&u.pathname?u.pathname:"").toLowerCase();
6560
+ var qs=(u&&u.search?u.search:"").toLowerCase();
6561
+ var likelyBackgroundEndpoint=!!(u&&(
6562
+ /(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(path)||
6563
+ path==="/cart"||
6564
+ path==="/cart.js"||
6565
+ path.indexOf("/cart/")===0||
6566
+ path.indexOf("/recommendations/")===0||
6567
+ path.indexOf("/search/suggest")===0||
6568
+ qs.indexOf("sections=")!==-1||
6569
+ qs.indexOf("section_id=")!==-1
6570
+ ));
6571
+ var likelyThirdPartyBg=!sameOrigin||likelyBackgroundEndpoint;
6572
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&isSafeMethod&&likelyThirdPartyBg){
5975
6573
  console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
5976
6574
  return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
5977
6575
  }
@@ -5980,7 +6578,7 @@ if(window.fetch){
5980
6578
  });
5981
6579
  };
5982
6580
  }
5983
- if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}
6581
+ if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toProxyNetworkUrl(url);}}catch(_){}return _open.apply(this,arguments);};}
5984
6582
  if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}
5985
6583
  function withReloadBust(urlRaw){try{var u=resolveUrl(String(urlRaw||""));if(!u)return null;var p=u.pathname||"";var isRootProxyPath=p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0;if(!isRootProxyPath)return null;return u.toString();}catch(_){return null;}}
5986
6584
  document.addEventListener("click",function(ev){try{var t=ev&&ev.target;if(!t||!t.closest)return;var a=t.closest("a[href]");if(a){var href=a.getAttribute("href")||a.href||"";var hasModifier=!!(ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey);var targetAttr=(a.getAttribute("target")||"").toLowerCase();var isNewTab=targetAttr==="_blank";var isDownload=!!a.getAttribute("download");if(hasModifier||isNewTab||isDownload)return;var prox=toProxy(href);if(!prox)return;a.setAttribute("href",prox);if(ev&&typeof ev.preventDefault==="function")ev.preventDefault();safeNavigate(href,"assign");return;}var summary=t.closest("summary[data-link]");if(summary&&summary.getAttribute){var raw=summary.getAttribute("data-link")||"";if(raw){var prox2=toProxy(raw);if(prox2){summary.setAttribute("data-link",prox2);if(ev&&typeof ev.preventDefault==="function")ev.preventDefault();safeNavigate(raw,"assign");}}}}catch(_){}},true);