@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.js CHANGED
@@ -78,17 +78,26 @@ function hasTrackingMarker(input, markers) {
78
78
  }
79
79
  return false;
80
80
  }
81
+ function patchKnownUnsafeEditorPatterns(scriptTag) {
82
+ let out = String(scriptTag || "");
83
+ out = out.replace(
84
+ /dragRegion\.addEventListener\((['"])pointerdown\1,\s*onPointerDown\);?/g,
85
+ "if (typeof dragRegion !== 'undefined' && dragRegion) dragRegion.addEventListener($1pointerdown$1, onPointerDown);"
86
+ );
87
+ return out;
88
+ }
81
89
  function stripTrackingScriptsFromScrapedHtml(html, markers) {
82
90
  let removedCount = 0;
83
91
  let out = html;
84
92
  out = out.replace(/<script\b[\s\S]*?<\/script>/gi, (tag) => {
93
+ var patchedTag = patchKnownUnsafeEditorPatterns(tag);
85
94
  const srcMatch = tag.match(/\bsrc\s*=\s*(["'])(.*?)\1/i);
86
95
  const src = srcMatch?.[2] || "";
87
96
  if (hasTrackingMarker(src, markers) || hasTrackingMarker(tag, markers)) {
88
97
  removedCount += 1;
89
98
  return "";
90
99
  }
91
- return tag;
100
+ return patchedTag;
92
101
  });
93
102
  out = out.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, (tag) => {
94
103
  if (!hasTrackingMarker(tag, markers)) return tag;
@@ -263,8 +272,97 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
263
272
  .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}
264
273
  .tb-viewport i{font-size:9px;color:##404040}
265
274
  /* Device toggle buttons */
275
+ .tb-dev-wrap{position:relative;display:flex;align-items:center}
266
276
  .tb-dev-btns{display:flex;align-items:center;gap:1px;padding:8px 4px;background:#F0F0F0;border-radius:7px; height: 30px;width: 116px;}
267
277
  .tb-dev-3btns{display:flex;align-items:center;gap:2px;padding:8px 4px;background:#F0F0F0;border-radius:7px; height: 30px;width: 100px;}
278
+ .tb-dev-menu{
279
+ position:absolute;top:calc(100% + 8px);right:0;z-index:12040;display:none;
280
+ width:264px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:4px 4px;
281
+ box-shadow:0 12px 28px rgba(2,6,23,.18);color:#27272a
282
+ }
283
+ .tb-dev-menu.open{display:block}
284
+ .tb-dev-menu .hd{font-size:12px;font-weight:600;color:#3f3f46;margin-bottom:10px}
285
+ .tb-dev-menu .row{display:flex;align-items:center;gap:8px;}
286
+ .tb-dev-menu .row.height-width-row .row{
287
+ flex-direction: column;
288
+ align-items: flex-start;
289
+ }
290
+ .tb-dev-menu .row.height-width-row label{
291
+ color: var(--content-subtle, #737373);
292
+ font-size: 12px;
293
+ font-style: normal;
294
+ font-weight: 500;
295
+ line-height: 14px; /* 116.667% */
296
+ }
297
+ .tb-dev-menu > .row{
298
+ padding: 4px 12px;
299
+ }
300
+ .tb-dev-menu label{ flex-shrink: 0;
301
+ overflow: hidden;
302
+ color: var(--base-surface, #646465);
303
+ font-size: var(--font-size-sm, 14px);
304
+ font-style: normal;
305
+ font-weight: 500;
306
+ line-height: var(--font-leading-4, 16px);
307
+ white-space: nowrap;
308
+ min-width: fit-content;}
309
+ .tb-dev-menu input{
310
+ 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);
311
+ width:100%;height:30px;border-radius:6px;
312
+ padding:0 8px;
313
+ font-size:12px;
314
+ color:#18181b;
315
+ background:#fff;
316
+ outline:none;
317
+ color:#404040;
318
+ border-color:transparent;
319
+ }
320
+ .tb-dev-menu input#dev-zoom-level{
321
+ max-width: fit-content;
322
+ margin-left: auto;
323
+ }
324
+ .tb-dev-menu input:focus{
325
+ border-color:#1A1A1A;
326
+ box-shadow:0 0 0 2px rgba(99,102,241,.14)
327
+ }
328
+ .tb-dev-menu .row-split > *{flex:1}
329
+ .tb-dev-menu .panel-toggle label{width:100%}
330
+ .tb-dev-menu .panel-toggle-label{
331
+ overflow:hidden;
332
+ color:var(--content-subtle, #737373);
333
+ text-overflow:ellipsis;
334
+ font-size:var(--font-size-sm, 14px);
335
+ font-style:normal;
336
+ font-weight:500;
337
+ line-height:var(--font-leading-4, 16px);
338
+ white-space:nowrap;
339
+ cursor:pointer;
340
+ }
341
+ .tb-dev-menu .vp-presets{
342
+ margin-top:8px;padding-top:8px;border-top:1px dashed #ececf0;
343
+ display:flex;flex-direction:column;gap:2px
344
+ }
345
+ .tb-dev-menu .vp-preset-btn{
346
+ border:none;background:transparent;text-align:left;cursor:pointer;
347
+ border-radius:6px;
348
+ padding:8px 12px;
349
+ color: var(--content-subtle, #737373);
350
+ text-overflow: ellipsis;
351
+ font-size: var(--font-size-sm, 14px);
352
+ font-style: normal;
353
+ font-weight: 500;
354
+ line-height: var(--font-leading-4, 16px);
355
+ }
356
+ .tb-dev-menu .vp-preset-btn:hover,.tb-dev-menu .vp-preset-btn.active{background:#f4f4f5}
357
+ .tb-dev-menu .ft{
358
+ margin-top:10px;padding-top:8px;border-top:1px solid #ececf0;
359
+ display:flex;justify-content:flex-end
360
+ }
361
+ .tb-dev-menu .apply-btn{
362
+ border:1px solid #d4d4d8;background:#f8fafc;color:#111827;border-radius:6px;
363
+ height:30px;padding:0 10px;font-size:12px;font-weight:600;cursor:pointer
364
+ }
365
+ .tb-dev-menu .apply-btn:hover{background:#eef2ff;border-color:#a5b4fc}
268
366
  /* Dark icon buttons */
269
367
  .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}
270
368
  .tb-dk-btn:hover{color:#e4e4e7;background:rgba(255,255,255,.07)}
@@ -423,7 +521,12 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
423
521
  /* \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 */
424
522
  #device-frame{
425
523
  width:100%;min-height:100%;display:flex;align-items:stretch;
426
- justify-content:center;transition:max-width .3s ease
524
+ justify-content:center;transform-origin:top center;
525
+ transition:max-width .3s ease,width .2s ease,height .2s ease
526
+ }
527
+ #device-frame.desktop{
528
+ max-width:1440px;
529
+ box-shadow:0 0 0 1px var(--border),0 4px 24px rgba(0,0,0,.08)
427
530
  }
428
531
  #device-frame.tablet{
429
532
  max-width:768px;
@@ -824,10 +927,8 @@ select.pr-inp{cursor:pointer;background:#fff}
824
927
  </head>
825
928
  <body class="mode-editor">
826
929
  <div id="app">
827
-
828
930
  <!-- \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 -->
829
931
  <div id="toolbar">
830
-
831
932
  <!-- Left: Logo + Breadcrumb -->
832
933
  <div class="tb-left">
833
934
  <span class="tb-logo">\u2733</span>
@@ -845,14 +946,48 @@ select.pr-inp{cursor:pointer;background:#fff}
845
946
  <span id="dev-label">1440px</span>
846
947
  <i class="bi bi-chevron-down"></i>
847
948
  </div>
848
- <div class="tb-dev-btns">
849
- <button class="tb-dk-btn active" id="dev-desktop" onclick="setDevice('desktop')" title="Desktop \u2014 full width"><i class="bi bi-display"></i></button>
850
- <button class="tb-dk-btn" id="dev-tablet" onclick="setDevice('tablet')" title="Tablet \u2014 768px"><i class="bi bi-tablet"></i></button>
851
- <button class="tb-dk-btn" id="dev-mobile" onclick="setDevice('mobile')" title="Mobile \u2014 390px"><i class="bi bi-phone"></i></button>
852
- <button class="tb-dk-btn" title="More options"><i class="bi bi-three-dots"></i></button>
949
+ <div class="tb-dev-wrap">
950
+ <div class="tb-dev-btns">
951
+ <button class="tb-dk-btn active" id="dev-desktop" onclick="setDevice('desktop')" title="Desktop \u2014 1440x1024"><i class="bi bi-display"></i></button>
952
+ <button class="tb-dk-btn" id="dev-tablet" onclick="setDevice('tablet')" title="Tablet \u2014 768x1024"><i class="bi bi-tablet"></i></button>
953
+ <button class="tb-dk-btn" id="dev-mobile" onclick="setDevice('mobile')" title="Mobile \u2014 390x844"><i class="bi bi-phone"></i></button>
954
+ <button class="tb-dk-btn" id="dev-more-btn" title="More options"><i class="bi bi-three-dots"></i></button>
955
+ </div>
956
+ <div id="dev-more-menu" class="tb-dev-menu" aria-hidden="true">
957
+ <div class="row row-zoom">
958
+ <label for="dev-zoom-level">Zoom</label>
959
+ <input id="dev-zoom-level" type="number" min="25" max="200" step="5" value="100" />
960
+ </div>
961
+ <div class="row row-split row-width height-width-row">
962
+ <div class="row" style="margin:0">
963
+ <label for="dev-custom-width">Width</label>
964
+ <input id="dev-custom-width" type="number" min="240" max="3840" step="1" value="1440" />
965
+ </div>
966
+ <div class="row row-height" style="margin:0">
967
+ <label for="dev-custom-height">Height</label>
968
+ <input id="dev-custom-height" type="number" min="320" max="3840" step="1" value="1024" />
969
+ </div>
970
+ </div>
971
+ <div class="row panel-toggle">
972
+ <label id="left-panel-toggle-label" class="panel-toggle-label">Collapse left panel</label>
973
+ </div>
974
+
975
+ <div class="vp-presets" id="dev-preset-list" aria-label="Device presets">
976
+ <button type="button" class="vp-preset-btn" data-preset="desktop">Desktop</button>
977
+ <button type="button" class="vp-preset-btn" data-preset="mobile">iPhone 13</button>
978
+ <button type="button" class="vp-preset-btn" data-preset="galaxy-s22">Galaxy S22</button>
979
+ <button type="button" class="vp-preset-btn" data-preset="tablet">iPad</button>
980
+ <button type="button" class="vp-preset-btn" data-preset="galaxy-j1">Galaxy J1</button>
981
+ <button type="button" class="vp-preset-btn" data-preset="iphone-7">iPhone 7</button>
982
+ <button type="button" class="vp-preset-btn" data-preset="responsive">Responsive</button>
983
+ </div>
984
+ <div class="ft">
985
+ <button type="button" class="apply-btn" id="dev-apply-btn">Apply</button>
986
+ </div>
987
+ </div>
853
988
  </div>
854
989
  <button class="tb-dk-btn" id="btn-undo" title="Undo (\u2318Z)"><i class="bi bi-arrow-counterclockwise"></i></button>
855
- <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)"><i class="bi bi-arrow-clockwise"></i></button>
990
+ <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)" style="display:none"><i class="bi bi-arrow-clockwise"></i></button>
856
991
  </div>
857
992
 
858
993
  <div id="iframe-loading-toolbar" class="tb-page-loading" aria-live="polite" aria-atomic="true">
@@ -923,8 +1058,8 @@ select.pr-inp{cursor:pointer;background:#fff}
923
1058
  <!-- Elements -->
924
1059
  <div class="lp-sec lp-sec-no-border">
925
1060
  <div class="lp-sec-hd">
926
- <span class="lp-sec-hd-left">Elements <i class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
927
- <button class="lp-add-btn" title="Add element">+ Add</button>
1061
+ <span class="lp-sec-hd-left">Elements <i style="display:none" class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
1062
+ <button class="lp-add-btn" id="btn-add-element" title="Add element">+ Add</button>
928
1063
  </div>
929
1064
 
930
1065
  <!-- Search (hidden, kept for JS) -->
@@ -986,7 +1121,7 @@ select.pr-inp{cursor:pointer;background:#fff}
986
1121
  </div>
987
1122
 
988
1123
  <!-- Device frame containing the editing iframe -->
989
- <div id="device-frame">
1124
+ <div id="device-frame" class="desktop">
990
1125
  <iframe id="iframeId" name="iframeId" allowfullscreen></iframe>
991
1126
  </div>
992
1127
 
@@ -994,14 +1129,16 @@ select.pr-inp{cursor:pointer;background:#fff}
994
1129
 
995
1130
  <!-- Right panel -->
996
1131
  <div id="right-panel">
997
- <!-- Left-tab controls moved here -->
998
- <div class="section-components-tabs">
999
- <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1000
- <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1001
- </div>
1002
- <div class="lp-body">
1003
- <div id="tab-components" class="tab-pane"></div>
1004
- <div id="tab-sections" class="tab-pane"></div>
1132
+ <div id="section-components-panel" style="display:none">
1133
+ <!-- Left-tab controls moved here -->
1134
+ <div class="section-components-tabs">
1135
+ <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1136
+ <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1137
+ </div>
1138
+ <div class="lp-body">
1139
+ <div id="tab-components" class="tab-pane"></div>
1140
+ <div id="tab-sections" class="tab-pane"></div>
1141
+ </div>
1005
1142
  </div>
1006
1143
  <!-- Element badge (hidden until selection) -->
1007
1144
  <div id="el-info" style="display:none">
@@ -1354,6 +1491,11 @@ var isDirty = false;
1354
1491
  var vvvebReady = false;
1355
1492
  var currentMode = 'editor';
1356
1493
  var currentDevice = 'desktop';
1494
+ var leftPanelCollapsed = false;
1495
+ var viewportPreset = 'desktop';
1496
+ var viewportWidth = 1440;
1497
+ var viewportHeight = 1024;
1498
+ var viewportZoom = 1;
1357
1499
  var selectedEl = null;
1358
1500
  /** Stable selector fingerprint for resilient selection recovery after DOM churn. */
1359
1501
  var selectedElFingerprint = '';
@@ -1606,15 +1748,202 @@ function setMode(mode) {
1606
1748
  }
1607
1749
 
1608
1750
  // \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
1609
- var DEVICE_LABELS = { desktop: '1440px', tablet: '768px', mobile: '390px' };
1751
+ var DEVICE_PRESETS = {
1752
+ desktop: { label: 'Desktop', width: 1440, height: 1024, device: 'desktop' },
1753
+ tablet: { label: 'iPad', width: 768, height: 1024, device: 'tablet' },
1754
+ mobile: { label: 'iPhone 13', width: 390, height: 844, device: 'mobile' },
1755
+ 'galaxy-s22': { label: 'Galaxy S22', width: 360, height: 780, device: 'mobile' },
1756
+ 'iphone-7': { label: 'iPhone 7', width: 375, height: 667, device: 'mobile' },
1757
+ 'galaxy-j1': { label: 'Galaxy J1', width: 360, height: 640, device: 'mobile' },
1758
+ responsive: { label: 'Responsive', width: 1280, height: 800, device: 'desktop' },
1759
+ };
1760
+
1761
+ function clampViewportNumber(v, fallback, min, max) {
1762
+ var n = parseInt(v, 10);
1763
+ if (!Number.isFinite(n)) return fallback;
1764
+ return Math.max(min, Math.min(max, n));
1765
+ }
1766
+
1767
+ function getViewportFitZoom() {
1768
+ var panel = document.getElementById('iframe-panel');
1769
+ var available = panel ? Math.max(260, panel.clientWidth - 24) : viewportWidth;
1770
+ if (!viewportWidth || viewportWidth <= 0) return 1;
1771
+ return Math.min(1, available / viewportWidth);
1772
+ }
1773
+
1774
+ function getAppliedViewportZoom() {
1775
+ var z = Number(viewportZoom);
1776
+ if (!Number.isFinite(z) || z <= 0) z = 1;
1777
+ z = Math.max(0.25, Math.min(2, z));
1778
+ return Math.min(z, getViewportFitZoom());
1779
+ }
1780
+
1781
+ function updateViewportLabel() {
1782
+ var lbl = document.getElementById('dev-label');
1783
+ if (!lbl) return;
1784
+ var zoomPct = Math.round(getAppliedViewportZoom() * 100);
1785
+ lbl.textContent = viewportWidth + 'x' + viewportHeight + ' \xB7 ' + zoomPct + '%';
1786
+ }
1787
+
1788
+ function syncViewportMenuControls() {
1789
+ var presetButtons = document.querySelectorAll('#dev-preset-list .vp-preset-btn');
1790
+ var widthInp = document.getElementById('dev-custom-width');
1791
+ var heightInp = document.getElementById('dev-custom-height');
1792
+ var zoomInp = document.getElementById('dev-zoom-level');
1793
+ if (presetButtons && presetButtons.length) {
1794
+ for (var i = 0; i < presetButtons.length; i++) {
1795
+ var key = presetButtons[i].getAttribute('data-preset') || '';
1796
+ presetButtons[i].classList.toggle('active', key === viewportPreset);
1797
+ }
1798
+ }
1799
+ if (widthInp) widthInp.value = String(viewportWidth);
1800
+ if (heightInp) heightInp.value = String(viewportHeight);
1801
+ if (zoomInp) zoomInp.value = String(Math.round(getAppliedViewportZoom() * 100));
1802
+ }
1803
+
1804
+ function applyViewportFrame() {
1805
+ var frame = document.getElementById('device-frame');
1806
+ var iframe = document.getElementById('iframeId');
1807
+ if (!frame) return;
1808
+ frame.className = currentDevice;
1809
+ frame.style.width = viewportWidth + 'px';
1810
+ frame.style.height = viewportHeight + 'px';
1811
+ frame.style.maxWidth = 'none';
1812
+ frame.style.zoom = String(getAppliedViewportZoom());
1813
+ if (iframe) {
1814
+ iframe.style.height = viewportHeight + 'px';
1815
+ iframe.style.minHeight = viewportHeight + 'px';
1816
+ }
1817
+ updateViewportLabel();
1818
+ syncViewportMenuControls();
1819
+ if (selectedEl && currentMode === 'editor') requestAnimationFrame(function() { positionSelectionToolbar(); });
1820
+ }
1821
+
1822
+ function setViewportPreset(presetKey) {
1823
+ var preset = DEVICE_PRESETS[presetKey];
1824
+ if (!preset) return;
1825
+ viewportPreset = presetKey;
1826
+ viewportWidth = preset.width;
1827
+ viewportHeight = preset.height;
1828
+ currentDevice = preset.device || 'desktop';
1829
+ ['desktop','tablet','mobile'].forEach(function(d) {
1830
+ document.getElementById('dev-' + d).classList.toggle('active', d === currentDevice);
1831
+ });
1832
+ applyViewportFrame();
1833
+ }
1834
+
1610
1835
  function setDevice(device) {
1836
+ viewportPreset = device;
1611
1837
  currentDevice = device;
1612
- var frame = document.getElementById('device-frame');
1613
- frame.className = device === 'desktop' ? '' : device;
1838
+ var preset = DEVICE_PRESETS[device] || DEVICE_PRESETS.desktop;
1839
+ viewportWidth = preset.width;
1840
+ viewportHeight = preset.height;
1614
1841
  ['desktop','tablet','mobile'].forEach(function(d) {
1615
1842
  document.getElementById('dev-' + d).classList.toggle('active', d === device);
1616
1843
  });
1617
- document.getElementById('dev-label').textContent = DEVICE_LABELS[device] || device;
1844
+ applyViewportFrame();
1845
+ }
1846
+
1847
+ function setViewportCustomFromInputs() {
1848
+ var widthInp = document.getElementById('dev-custom-width');
1849
+ var heightInp = document.getElementById('dev-custom-height');
1850
+ var zoomInp = document.getElementById('dev-zoom-level');
1851
+ viewportWidth = clampViewportNumber(widthInp ? widthInp.value : '', viewportWidth, 240, 3840);
1852
+ viewportHeight = clampViewportNumber(heightInp ? heightInp.value : '', viewportHeight, 320, 3840);
1853
+ var z = clampViewportNumber(zoomInp ? zoomInp.value : '', Math.round(getAppliedViewportZoom() * 100), 25, 200);
1854
+ viewportZoom = z / 100;
1855
+ viewportPreset = 'custom';
1856
+ currentDevice = viewportWidth <= 480 ? 'mobile' : (viewportWidth <= 1024 ? 'tablet' : 'desktop');
1857
+ ['desktop','tablet','mobile'].forEach(function(d) {
1858
+ document.getElementById('dev-' + d).classList.toggle('active', d === currentDevice);
1859
+ });
1860
+ applyViewportFrame();
1861
+ }
1862
+
1863
+ function closeViewportMenu() {
1864
+ var menu = document.getElementById('dev-more-menu');
1865
+ if (!menu) return;
1866
+ menu.classList.remove('open');
1867
+ menu.setAttribute('aria-hidden', 'true');
1868
+ }
1869
+
1870
+ function toggleViewportMenu() {
1871
+ var menu = document.getElementById('dev-more-menu');
1872
+ if (!menu) return;
1873
+ var shouldOpen = !menu.classList.contains('open');
1874
+ menu.classList.toggle('open', shouldOpen);
1875
+ menu.setAttribute('aria-hidden', shouldOpen ? 'false' : 'true');
1876
+ }
1877
+
1878
+ function bindViewportControls() {
1879
+ var btn = document.getElementById('dev-more-btn');
1880
+ var menu = document.getElementById('dev-more-menu');
1881
+ var leftPanelToggleLabel = document.getElementById('left-panel-toggle-label');
1882
+ var presetButtons = document.querySelectorAll('#dev-preset-list .vp-preset-btn');
1883
+ var applyBtn = document.getElementById('dev-apply-btn');
1884
+ var zoomInp = document.getElementById('dev-zoom-level');
1885
+ var viewportBtn = document.querySelector('.tb-viewport');
1886
+ function updateLeftPanelToggleLabel() {
1887
+ if (!leftPanelToggleLabel) return;
1888
+ leftPanelToggleLabel.textContent = leftPanelCollapsed ? 'Expand left panel' : 'Collapse left panel';
1889
+ }
1890
+ function setLeftPanelCollapsed(collapsed) {
1891
+ var panel = document.getElementById('left-panel');
1892
+ leftPanelCollapsed = !!collapsed;
1893
+ if (panel) panel.style.display = leftPanelCollapsed ? 'none' : '';
1894
+ updateLeftPanelToggleLabel();
1895
+ applyViewportFrame();
1896
+ }
1897
+ if (leftPanelToggleLabel) {
1898
+ leftPanelToggleLabel.addEventListener('click', function(e) {
1899
+ e.preventDefault();
1900
+ e.stopPropagation();
1901
+ setLeftPanelCollapsed(!leftPanelCollapsed);
1902
+ });
1903
+ }
1904
+ if (btn) btn.addEventListener('click', function(e) {
1905
+ e.preventDefault();
1906
+ e.stopPropagation();
1907
+ toggleViewportMenu();
1908
+ });
1909
+ if (viewportBtn) viewportBtn.addEventListener('click', function(e) {
1910
+ e.preventDefault();
1911
+ e.stopPropagation();
1912
+ toggleViewportMenu();
1913
+ });
1914
+ if (applyBtn) applyBtn.addEventListener('click', function(e) {
1915
+ e.preventDefault();
1916
+ setViewportCustomFromInputs();
1917
+ closeViewportMenu();
1918
+ });
1919
+ if (zoomInp) {
1920
+ zoomInp.addEventListener('change', function() {
1921
+ var z = clampViewportNumber(zoomInp.value, Math.round(getAppliedViewportZoom() * 100), 25, 200);
1922
+ viewportZoom = z / 100;
1923
+ applyViewportFrame();
1924
+ });
1925
+ }
1926
+ if (presetButtons && presetButtons.length) {
1927
+ for (var i = 0; i < presetButtons.length; i++) {
1928
+ presetButtons[i].addEventListener('click', function(e) {
1929
+ e.preventDefault();
1930
+ var key = this.getAttribute('data-preset');
1931
+ if (!key) return;
1932
+ setViewportPreset(key);
1933
+ });
1934
+ }
1935
+ }
1936
+ document.addEventListener('click', function(e) {
1937
+ if (!menu || !menu.classList.contains('open')) return;
1938
+ if (menu.contains(e.target) || (btn && btn.contains(e.target)) || (viewportBtn && viewportBtn.contains(e.target))) return;
1939
+ closeViewportMenu();
1940
+ });
1941
+ document.addEventListener('keydown', function(e) {
1942
+ if (e.key === 'Escape') closeViewportMenu();
1943
+ });
1944
+ window.addEventListener('resize', function() { applyViewportFrame(); });
1945
+ updateLeftPanelToggleLabel();
1946
+ applyViewportFrame();
1618
1947
  }
1619
1948
 
1620
1949
  // \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
@@ -1658,6 +1987,19 @@ function switchSectionComponentsTab(tab) {
1658
1987
  renderSidebar(inp ? inp.value : '');
1659
1988
  }
1660
1989
 
1990
+ function toggleSectionComponentsPanel(forceVisible) {
1991
+ var panel = document.getElementById('section-components-panel');
1992
+ if (!panel) return;
1993
+ var shouldShow =
1994
+ typeof forceVisible === 'boolean'
1995
+ ? forceVisible
1996
+ : panel.style.display === 'none';
1997
+ panel.style.display = shouldShow ? '' : 'none';
1998
+ if (shouldShow) {
1999
+ switchSectionComponentsTab(currentSectionComponentsTab || 'components');
2000
+ }
2001
+ }
2002
+
1661
2003
  // \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
1662
2004
  function toggleAcc(name) {
1663
2005
  var sec = document.getElementById('acc-' + name);
@@ -1763,9 +2105,60 @@ function getOriginalValue(inputId, el) {
1763
2105
  }
1764
2106
  }
1765
2107
 
2108
+ var PX_DEFAULT_INPUT_IDS = {
2109
+ 'pp-mt': true, 'pp-mr': true, 'pp-mb': true, 'pp-ml': true,
2110
+ 'pp-pt': true, 'pp-pr': true, 'pp-pb': true, 'pp-pl': true,
2111
+ };
2112
+
2113
+ var PX_DEFAULT_CSS_PROPS = {
2114
+ 'margin-top': true, 'margin-right': true, 'margin-bottom': true, 'margin-left': true,
2115
+ 'padding-top': true, 'padding-right': true, 'padding-bottom': true, 'padding-left': true,
2116
+ };
2117
+
2118
+ function normalizeCssValueForProperty(prop, value) {
2119
+ var p = prop == null ? '' : String(prop).trim();
2120
+ var pKebab = p
2121
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
2122
+ .replace(/_/g, '-')
2123
+ .replace(/!important/gi, '')
2124
+ .replace(/[^a-zA-Z-]/g, '')
2125
+ .toLowerCase();
2126
+ var raw = value == null ? '' : String(value).trim();
2127
+ var rawClean = raw.replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim();
2128
+ var numericCandidate = rawClean.replace(/[, ]+/g, '');
2129
+ var isSpacingProp = /^(?:margin|padding)(?:-(?:top|right|bottom|left))?$/.test(pKebab);
2130
+ if (!pKebab || !rawClean) return rawClean;
2131
+ var numericValue = Number(numericCandidate);
2132
+ var shouldAddPx =
2133
+ isSpacingProp &&
2134
+ numericCandidate !== '' &&
2135
+ Number.isFinite(numericValue) &&
2136
+ /^[-+]?(?:[0-9]+(?:[.][0-9]+)?|[.][0-9]+)$/.test(numericCandidate);
2137
+ var out = shouldAddPx ? numericCandidate + 'px' : rawClean;
2138
+ return out;
2139
+ }
2140
+
2141
+ function normalizeChainSetRowUnits(row) {
2142
+ if (!row) return row;
2143
+ if (normalizeChangesetType(row) === 'style') {
2144
+ row.value = normalizeCssValueForProperty(row.property || row.cssProp, row.value);
2145
+ }
2146
+ return row;
2147
+ }
2148
+
2149
+ function normalizeLoggedValue(inputId, value) {
2150
+ var raw = value == null ? '' : String(value).trim();
2151
+ if (!raw) return raw;
2152
+ if (PX_DEFAULT_INPUT_IDS[inputId] && /^-?d+(?:.d+)?$/.test(raw)) {
2153
+ return raw + 'px';
2154
+ }
2155
+ return raw;
2156
+ }
2157
+
1766
2158
  function logChange(selector, inputId, value, targetEl, originalValue) {
1767
2159
  var meta = PROP_META[inputId];
1768
2160
  if (!meta) return;
2161
+ value = normalizeLoggedValue(inputId, value);
1769
2162
  var key = selector + '||' + inputId;
1770
2163
  // Skip trivially empty / reset values \u2014 remove from log if present
1771
2164
  if (value === '' || value === 'none' || value === 'auto' || value === 'normal') {
@@ -1867,6 +2260,24 @@ function syncDesignInput(change) {
1867
2260
  function removeStateChange(idx) {
1868
2261
  var change = stateChanges[idx];
1869
2262
  if (!change) return;
2263
+ if (change.isStructuralLive) {
2264
+ removeSessionStructuralRowByTimestamp(
2265
+ change.structuralVarId || activeVarId,
2266
+ change.vveTs,
2267
+ );
2268
+ stateChanges.splice(idx, 1);
2269
+ commitStateChangesForActiveVariation();
2270
+ renderStatesTab();
2271
+ if (currentMainTab === 'history') renderHistoryTab();
2272
+ try {
2273
+ delete varHtmlCache[activeVarId];
2274
+ } catch(_) {}
2275
+ appliedStructuralChangesetKeys = {};
2276
+ recomputeEditorDirty();
2277
+ scheduleDomTreeRefresh();
2278
+ softReloadEditorIframe();
2279
+ return;
2280
+ }
1870
2281
  revertChangeOnDom(change);
1871
2282
  syncDesignInput(change);
1872
2283
  stateChanges.splice(idx, 1);
@@ -2223,6 +2634,14 @@ function getLatestHistoryUndoTarget() {
2223
2634
  return list.length ? list[0] : null;
2224
2635
  }
2225
2636
 
2637
+ function getLatestLiveUndoTarget() {
2638
+ var list = getUnifiedHistoryItems();
2639
+ for (var i = 0; i < list.length; i++) {
2640
+ if (list[i] && list[i].source === 'live') return list[i];
2641
+ }
2642
+ return null;
2643
+ }
2644
+
2226
2645
  function changesetListHasStructural(arr) {
2227
2646
  if (!arr || !arr.length) return false;
2228
2647
  for (var i = 0; i < arr.length; i++) {
@@ -2321,8 +2740,15 @@ function removeHistoryChangeset(idx, evt) {
2321
2740
  function clearAllHistoryChangesets() {
2322
2741
  var v = getActiveVariationForHistory();
2323
2742
  if (!v) return;
2324
- if (!parseVariationChangesets(v).length) return;
2743
+ var hasSavedChangesets = parseVariationChangesets(v).length > 0;
2744
+ var hasSessionStructural =
2745
+ !!(activeVarId && sessionStructuralChainRowsByVarId[activeVarId] && sessionStructuralChainRowsByVarId[activeVarId].length);
2746
+ if (!hasSavedChangesets && !hasSessionStructural) return;
2747
+
2325
2748
  persistActiveVariationChangesets([]);
2749
+ if (activeVarId) {
2750
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2751
+ }
2326
2752
  appliedChangesetSnapshots = {};
2327
2753
  appliedStructuralChangesetKeys = {};
2328
2754
  try {
@@ -2335,6 +2761,10 @@ function clearAllHistoryChangesets() {
2335
2761
  }
2336
2762
 
2337
2763
  function clearAllUnifiedHistory() {
2764
+ if (activeVarId) {
2765
+ // Ensure structural unsaved rows are also removed by "Clear all changes".
2766
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2767
+ }
2338
2768
  clearAllStates();
2339
2769
  clearAllHistoryChangesets();
2340
2770
  if (currentMainTab === 'history') renderHistoryTab();
@@ -2346,15 +2776,32 @@ var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
2346
2776
 
2347
2777
  function clearVisualEditorLocalStorage() {
2348
2778
  try {
2349
- for (var i = localStorage.length - 1; i >= 0; i--) {
2350
- var k = localStorage.key(i);
2351
- if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2352
- localStorage.removeItem(k);
2779
+ var stores = [];
2780
+ try { stores.push(localStorage); } catch(_) {}
2781
+ try { stores.push(sessionStorage); } catch(_) {}
2782
+ for (var si = 0; si < stores.length; si++) {
2783
+ var store = stores[si];
2784
+ if (!store) continue;
2785
+ for (var i = store.length - 1; i >= 0; i--) {
2786
+ var k = store.key(i);
2787
+ if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2788
+ store.removeItem(k);
2789
+ }
2353
2790
  }
2354
2791
  }
2355
2792
  } catch(_) {}
2356
2793
  }
2357
2794
 
2795
+ function clearPersistedActiveVariationForData(data) {
2796
+ try {
2797
+ var sk = activeVariationStorageKeyFromPayload(data);
2798
+ if (sk && sk !== VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') {
2799
+ try { localStorage.removeItem(sk); } catch(_) {}
2800
+ try { sessionStorage.removeItem(sk); } catch(_) {}
2801
+ }
2802
+ } catch(_) {}
2803
+ }
2804
+
2358
2805
  function activeVariationStorageKeyFromPayload(data) {
2359
2806
  return (
2360
2807
  VVE_LOCAL_STORAGE_PREFIX +
@@ -2393,22 +2840,41 @@ function writePersistedActiveVariationId(varId) {
2393
2840
  * @param allowPrevMemory when true, keep in-session activeVarId if still valid (skip-reload path).
2394
2841
  */
2395
2842
  function pickActiveVariationIdForLoad(data, variationsArr, prevMemoryId, allowPrevMemory) {
2396
- var baseline = variationsArr.find(function(v) { return v.baseline; });
2397
- var fallback = (baseline || variationsArr[0] || {})._id || null;
2843
+ var firstNonBaseline = variationsArr.find(function(v) { return !v.baseline; });
2844
+ var fallback = (firstNonBaseline || variationsArr[0] || {})._id || null;
2398
2845
  if (!variationsArr.length) return null;
2399
2846
  if (allowPrevMemory && prevMemoryId && variationsArr.some(function(v) { return v._id === prevMemoryId; })) {
2400
2847
  return prevMemoryId;
2401
2848
  }
2402
2849
  var stored = readPersistedActiveVariationId(data);
2403
2850
  if (stored && variationsArr.some(function(v) { return v._id === stored; })) {
2851
+ var storedVar = variationsArr.find(function(v) { return v._id === stored; });
2852
+ if (storedVar && storedVar.baseline && firstNonBaseline && firstNonBaseline._id) {
2853
+ return firstNonBaseline._id;
2854
+ }
2404
2855
  return stored;
2405
2856
  }
2406
2857
  return fallback;
2407
2858
  }
2408
2859
 
2860
+ function updateExperimentNameLabel(data) {
2861
+ var el = document.getElementById('tb-exp-name');
2862
+ if (!el) return;
2863
+ var name = '';
2864
+ try {
2865
+ name = data && data.name != null ? String(data.name).trim() : '';
2866
+ } catch(_) {
2867
+ name = '';
2868
+ }
2869
+ if (!name) name = 'Visual Editor';
2870
+ el.textContent = name;
2871
+ el.title = name;
2872
+ }
2873
+
2409
2874
  // \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
2410
2875
  function handleLoadExperiment(data) {
2411
2876
  clearPendingGranularChangesets();
2877
+ updateExperimentNameLabel(data);
2412
2878
  var prevKey = experimentData
2413
2879
  ? String(experimentData.experimentId || '') + '|' + String(experimentData.pageUrl || '')
2414
2880
  : '';
@@ -2624,7 +3090,13 @@ function granularAnySelectorMatches(doc, cs) {
2624
3090
 
2625
3091
  /** Bust bfcache / same-URL no-op reloads so the iframe actually re-parses (loading \u2192 interactive). */
2626
3092
  function appendIframeReloadBust(url) {
2627
- return url;
3093
+ try {
3094
+ var u = new URL(String(url || ''), window.location.href);
3095
+ u.searchParams.set('_vve_reload', String(Date.now()));
3096
+ return u.toString();
3097
+ } catch(_) {
3098
+ return url;
3099
+ }
2628
3100
  }
2629
3101
 
2630
3102
  // True when the iframe contentDocument belongs to the current iframe.src navigation.
@@ -3002,11 +3474,55 @@ function appendSessionStructuralChainRow(varId, row) {
3002
3474
  sessionStructuralChainRowsByVarId[varId].push(row);
3003
3475
  }
3004
3476
 
3477
+ function markStructuralRowApplied(row) {
3478
+ try {
3479
+ var k = structuralChangesetDedupKey(row);
3480
+ if (k) appliedStructuralChangesetKeys[k] = true;
3481
+ } catch(_) {}
3482
+ }
3483
+
3484
+ function logStructuralStateChange(row, label, value, targetEl) {
3485
+ if (!row || !row.selector) return;
3486
+ stateChanges.push({
3487
+ selector: row.selector,
3488
+ inputId: 'vve-struct',
3489
+ label: label || 'Structure change',
3490
+ cssProp: null,
3491
+ value: value != null ? String(value) : String(row.type || ''),
3492
+ targetEl: targetEl || null,
3493
+ originalValue: '',
3494
+ vveTs: row.vveTs || nextHistoryTimestamp(),
3495
+ isStructuralLive: true,
3496
+ structuralVarId: activeVarId || null,
3497
+ structuralType: row.type || '',
3498
+ });
3499
+ if (currentMainTab === 'history') renderHistoryTab();
3500
+ commitStateChangesForActiveVariation();
3501
+ }
3502
+
3503
+ function removeSessionStructuralRowByTimestamp(varId, ts) {
3504
+ if (!varId || !ts) return;
3505
+ var arr = sessionStructuralChainRowsByVarId[varId];
3506
+ if (!arr || !arr.length) return;
3507
+ for (var i = arr.length - 1; i >= 0; i--) {
3508
+ if (arr[i] && arr[i].vveTs === ts) {
3509
+ arr.splice(i, 1);
3510
+ return;
3511
+ }
3512
+ }
3513
+ }
3514
+
3005
3515
  /** One States-tab row -> Conversion.io chain-set shape (matches applyChangesetEntry). */
3006
3516
  function stateChangeToChainSet(c) {
3007
3517
  if (!c || !c.selector) return null;
3008
3518
  if (c.cssProp) {
3009
- return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
3519
+ return {
3520
+ selector: c.selector,
3521
+ type: 'style',
3522
+ property: c.cssProp,
3523
+ value: normalizeCssValueForProperty(c.cssProp, c.value),
3524
+ vveTs: c.vveTs || nextHistoryTimestamp()
3525
+ };
3010
3526
  }
3011
3527
  switch (c.inputId) {
3012
3528
  case 'pp-text':
@@ -3178,7 +3694,7 @@ function buildPersistentStyleRulesForActiveVariation() {
3178
3694
  if (value == null || value === '') return;
3179
3695
  var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
3180
3696
  var pr = String(prop).trim();
3181
- var val = String(value).trim();
3697
+ var val = normalizeCssValueForProperty(pr, value);
3182
3698
  if (!sel || !pr || !val) return;
3183
3699
  var k = sel + '__vve_sep__' + pr;
3184
3700
  if (!map[k]) order.push(k);
@@ -3216,7 +3732,8 @@ function buildPersistentStyleRulesForActiveVariation() {
3216
3732
  var lines = [];
3217
3733
  for (var oi = 0; oi < order.length; oi++) {
3218
3734
  var row = map[order[oi]];
3219
- lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
3735
+ var outVal = normalizeCssValueForProperty(row.property, row.value);
3736
+ lines.push(row.selector + ' { ' + row.property + ': ' + outVal + ' !important; }');
3220
3737
  }
3221
3738
  return lines.join('\\n');
3222
3739
  }
@@ -3367,7 +3884,9 @@ function buildPersistedChainSetsForVariation(v) {
3367
3884
  var row = stateChangeToChainSet(sourceStateChanges[si]);
3368
3885
  if (row) overlay.push(row);
3369
3886
  }
3370
- return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
3887
+ var merged = mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
3888
+ for (var mi = 0; mi < merged.length; mi++) normalizeChainSetRowUnits(merged[mi]);
3889
+ return merged;
3371
3890
  }
3372
3891
 
3373
3892
  /**
@@ -3696,12 +4215,15 @@ function duplicateSelectedEl() {
3696
4215
  clone.setAttribute('data-vve-instance', generateVveInstanceId());
3697
4216
  } catch(_) {}
3698
4217
  if (activeVarId) {
3699
- appendSessionStructuralChainRow(activeVarId, {
4218
+ var dupRow = {
3700
4219
  selector: anchorSel,
3701
4220
  type: 'insert',
3702
4221
  action: 'after',
3703
4222
  html: clone.outerHTML,
3704
- });
4223
+ };
4224
+ appendSessionStructuralChainRow(activeVarId, dupRow);
4225
+ markStructuralRowApplied(dupRow);
4226
+ logStructuralStateChange(dupRow, 'Duplicated via toolbar', 'Duplicate inserted', selectedEl);
3705
4227
  }
3706
4228
  selectedEl.parentNode.insertBefore(clone, selectedEl.nextSibling);
3707
4229
  saveCurrentVariationHtml();
@@ -3717,23 +4239,27 @@ function toggleHideSelectedEl() {
3717
4239
  selectedEl.style.visibility = '';
3718
4240
  selectedEl.removeAttribute('data-vve-hidden');
3719
4241
  if (activeVarId) {
3720
- appendSessionStructuralChainRow(activeVarId, {
4242
+ var showRow = {
3721
4243
  selector: hidSel,
3722
4244
  type: 'style',
3723
4245
  property: 'visibility',
3724
4246
  value: '',
3725
- });
4247
+ };
4248
+ appendSessionStructuralChainRow(activeVarId, showRow);
4249
+ logStructuralStateChange(showRow, 'Shown via toolbar', 'Visibility restored', selectedEl);
3726
4250
  }
3727
4251
  } else {
3728
4252
  selectedEl.style.visibility = 'hidden';
3729
4253
  selectedEl.setAttribute('data-vve-hidden', '1');
3730
4254
  if (activeVarId) {
3731
- appendSessionStructuralChainRow(activeVarId, {
4255
+ var hideRow = {
3732
4256
  selector: hidSel,
3733
4257
  type: 'style',
3734
4258
  property: 'visibility',
3735
4259
  value: 'hidden',
3736
- });
4260
+ };
4261
+ appendSessionStructuralChainRow(activeVarId, hideRow);
4262
+ logStructuralStateChange(hideRow, 'Hidden via toolbar', 'Visibility set to hidden', selectedEl);
3737
4263
  }
3738
4264
  }
3739
4265
  saveCurrentVariationHtml();
@@ -3744,7 +4270,11 @@ function deleteSelectedEl() {
3744
4270
  if (!selectedEl || !selectedEl.parentNode) return;
3745
4271
  var delSel = buildSelector(selectedEl);
3746
4272
  selectedEl.remove();
3747
- if (activeVarId) appendSessionStructuralChainRow(activeVarId, { selector: delSel, type: 'remove' });
4273
+ if (activeVarId) {
4274
+ var delRow = { selector: delSel, type: 'remove' };
4275
+ appendSessionStructuralChainRow(activeVarId, delRow);
4276
+ logStructuralStateChange(delRow, 'Deleted via toolbar', 'Element removed', null);
4277
+ }
3748
4278
  saveCurrentVariationHtml();
3749
4279
  recomputeEditorDirty();
3750
4280
  deselectElement();
@@ -4645,19 +5175,25 @@ function recordReorderAfterDrag(movedEl) {
4645
5175
  var prev = movedEl.previousElementSibling;
4646
5176
  var next = movedEl.nextElementSibling;
4647
5177
  if (prev) {
4648
- appendSessionStructuralChainRow(activeVarId, {
5178
+ var reorderAfterRow = {
4649
5179
  selector: buildSelector(movedEl),
4650
5180
  type: 'reorder',
4651
5181
  targetSelector: buildSelector(prev),
4652
5182
  action: 'after',
4653
- });
5183
+ };
5184
+ appendSessionStructuralChainRow(activeVarId, reorderAfterRow);
5185
+ markStructuralRowApplied(reorderAfterRow);
5186
+ logStructuralStateChange(reorderAfterRow, 'Reordered via drag', 'Moved after sibling', movedEl);
4654
5187
  } else if (next) {
4655
- appendSessionStructuralChainRow(activeVarId, {
5188
+ var reorderBeforeRow = {
4656
5189
  selector: buildSelector(movedEl),
4657
5190
  type: 'reorder',
4658
5191
  targetSelector: buildSelector(next),
4659
5192
  action: 'before',
4660
- });
5193
+ };
5194
+ appendSessionStructuralChainRow(activeVarId, reorderBeforeRow);
5195
+ markStructuralRowApplied(reorderBeforeRow);
5196
+ logStructuralStateChange(reorderBeforeRow, 'Reordered via drag', 'Moved before sibling', movedEl);
4661
5197
  }
4662
5198
  }
4663
5199
 
@@ -4938,12 +5474,15 @@ function insertHtml(html) {
4938
5474
  }
4939
5475
  if (firstEl) selectElement(firstEl);
4940
5476
  if (activeVarId) {
4941
- appendSessionStructuralChainRow(activeVarId, {
5477
+ var insertRow = {
4942
5478
  selector: anchorSel,
4943
5479
  type: 'insert',
4944
5480
  action: 'after',
4945
5481
  html: htmlStr,
4946
- });
5482
+ };
5483
+ appendSessionStructuralChainRow(activeVarId, insertRow);
5484
+ markStructuralRowApplied(insertRow);
5485
+ logStructuralStateChange(insertRow, 'Added component/section', 'Inserted new element', firstEl || selectedEl);
4947
5486
  }
4948
5487
  saveCurrentVariationHtml();
4949
5488
  recomputeEditorDirty();
@@ -5033,6 +5572,14 @@ document.getElementById('comp-search').addEventListener('input', function() {
5033
5572
  renderSidebar(this.value);
5034
5573
  }
5035
5574
  });
5575
+ var btnAddElement = document.getElementById('btn-add-element');
5576
+ if (btnAddElement) {
5577
+ btnAddElement.addEventListener('click', function(e) {
5578
+ e.preventDefault();
5579
+ e.stopPropagation();
5580
+ toggleSectionComponentsPanel();
5581
+ });
5582
+ }
5036
5583
 
5037
5584
  // \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
5038
5585
  document.getElementById('btn-save').addEventListener('click', handleSave);
@@ -5073,11 +5620,21 @@ function handleSave() {
5073
5620
  }
5074
5621
 
5075
5622
  function handleClose() {
5623
+ clearPersistedActiveVariationForData(experimentData);
5076
5624
  clearVisualEditorLocalStorage();
5077
5625
  // Unsaved-changes UX lives in the parent (PlatformVisualEditorV2); avoid double confirm here.
5078
5626
  send('close-editor', {});
5079
5627
  }
5080
5628
 
5629
+ // Defensive cleanup: if parent closes/unmounts the editor shell without
5630
+ // invoking handleClose(), clear persisted VVE keys on unload as well.
5631
+ window.addEventListener('pagehide', function() {
5632
+ clearVisualEditorLocalStorage();
5633
+ });
5634
+ window.addEventListener('beforeunload', function() {
5635
+ clearVisualEditorLocalStorage();
5636
+ });
5637
+
5081
5638
  // \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
5082
5639
  function isNativeEditableTarget(target) {
5083
5640
  if (!target || target.nodeType !== 1) return false;
@@ -5099,10 +5656,7 @@ document.addEventListener('keydown', function(e) {
5099
5656
  e.preventDefault();
5100
5657
  runEditorUndo();
5101
5658
  }
5102
- if (meta && e.shiftKey && k === 'z') {
5103
- e.preventDefault();
5104
- runEditorRedo();
5105
- }
5659
+ // Redo is intentionally hidden/disabled in this editor flow.
5106
5660
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
5107
5661
  if (e.key === 'Escape') {
5108
5662
  var openTips = document.querySelectorAll('.ve-pl-tip.is-tip-open');
@@ -5122,11 +5676,17 @@ document.addEventListener('keydown', function(e) {
5122
5676
  }
5123
5677
  });
5124
5678
  function runEditorUndo() {
5125
- var target = getLatestHistoryUndoTarget();
5126
- if (target) {
5127
- removeHistoryItem(target.source, target.idx);
5679
+ // Undo only unsaved in-session edits; do not remove persisted history rows.
5680
+ if (!isDirty) return;
5681
+
5682
+ // 1) Prefer live unsaved change stack (stateChanges shown in History tab).
5683
+ var liveTarget = getLatestLiveUndoTarget();
5684
+ if (liveTarget) {
5685
+ removeHistoryItem('live', liveTarget.idx);
5128
5686
  return;
5129
5687
  }
5688
+
5689
+ // 2) Fallback to Vvveb internal undo stack (e.g. structural drag/drop ops).
5130
5690
  if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
5131
5691
  Vvveb.Undo.undo();
5132
5692
  saveCurrentVariationHtml();
@@ -5149,11 +5709,10 @@ document.getElementById('btn-undo').addEventListener('click', function(e) {
5149
5709
  e.stopPropagation();
5150
5710
  runEditorUndo();
5151
5711
  });
5152
- document.getElementById('btn-redo').addEventListener('click', function(e) {
5153
- e.preventDefault();
5154
- e.stopPropagation();
5155
- runEditorRedo();
5156
- });
5712
+ var btnRedo = document.getElementById('btn-redo');
5713
+ if (btnRedo) {
5714
+ btnRedo.style.display = 'none';
5715
+ }
5157
5716
 
5158
5717
  function layoutLoadingTooltip(host) {
5159
5718
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -5229,6 +5788,7 @@ function registerCROSections() {
5229
5788
 
5230
5789
  window.addEventListener('load', function() {
5231
5790
  registerCROSections();
5791
+ bindViewportControls();
5232
5792
  switchSectionComponentsTab(currentSectionComponentsTab);
5233
5793
  renderElementsTree(document.getElementById('comp-search').value);
5234
5794
  vvvebReady = true;
@@ -5936,8 +6496,32 @@ ${runtimePreflightScript}
5936
6496
  var TARGET_ORIGIN=${JSON.stringify(origin)};
5937
6497
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
5938
6498
  var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
5939
- 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("//");}
5940
- 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;}}
6499
+ var _proxyCtx=(function(){try{return new URL(window.location.href);}catch(_){return null;}})();
6500
+ var PROXY_ROOT="/api/conversion-proxy";
6501
+ var PROXY_PASSWORD=_proxyCtx?_proxyCtx.searchParams.get("password")||"":"";
6502
+ var PROXY_BASE_URL=_proxyCtx?_proxyCtx.searchParams.get("conversionProxyBaseUrl")||"":"";
6503
+ var PROXY_TRACKING_MARKERS=_proxyCtx?_proxyCtx.searchParams.get("trackingMarkers")||"":"";
6504
+ var PROXY_STRICT_FREEZE=_proxyCtx?_proxyCtx.searchParams.get("strictObserverFreeze")||"":"";
6505
+ var PROXY_UPSTREAM_MODE=_proxyCtx?_proxyCtx.searchParams.get("proxy")||"":"";
6506
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
6507
+ function toProxyNetworkUrl(raw){
6508
+ if(isSkippable(raw))return raw;
6509
+ try{
6510
+ var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;
6511
+ var abs=new URL(raw,base);
6512
+ if(abs.origin!==TARGET_ORIGIN)return raw;
6513
+ var prox=new URL(PROXY_ROOT,window.location.origin);
6514
+ prox.searchParams.set("password",PROXY_PASSWORD||"");
6515
+ prox.searchParams.set("url",abs.toString());
6516
+ if(PROXY_BASE_URL)prox.searchParams.set("conversionProxyBaseUrl",PROXY_BASE_URL);
6517
+ if(PROXY_TRACKING_MARKERS)prox.searchParams.set("trackingMarkers",PROXY_TRACKING_MARKERS);
6518
+ if(PROXY_STRICT_FREEZE)prox.searchParams.set("strictObserverFreeze",PROXY_STRICT_FREEZE);
6519
+ if(PROXY_UPSTREAM_MODE)prox.searchParams.set("proxy",PROXY_UPSTREAM_MODE);
6520
+ return prox.toString();
6521
+ }catch(_){
6522
+ return raw;
6523
+ }
6524
+ }
5941
6525
  function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
5942
6526
  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;}
5943
6527
  function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
@@ -5950,9 +6534,9 @@ if(window.fetch){
5950
6534
  var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
5951
6535
  if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
5952
6536
  if(typeof input==="string"){
5953
- input=toAbsoluteOriginUrl(input);
6537
+ input=toProxyNetworkUrl(input);
5954
6538
  }else if(input&&input.url){
5955
- var next=toAbsoluteOriginUrl(input.url);
6539
+ var next=toProxyNetworkUrl(input.url);
5956
6540
  if(next!==input.url){input=new Request(next,input);}
5957
6541
  }
5958
6542
  afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
@@ -5962,8 +6546,22 @@ if(window.fetch){
5962
6546
  try{
5963
6547
  var u=afterUrl?resolveUrl(afterUrl):null;
5964
6548
  var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
5965
- var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
5966
- if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
6549
+ var reqMethod=(init&&init.method?String(init.method):(input&&input.method?String(input.method):"GET")).toUpperCase();
6550
+ var isSafeMethod=reqMethod==="GET"||reqMethod==="HEAD";
6551
+ var path=(u&&u.pathname?u.pathname:"").toLowerCase();
6552
+ var qs=(u&&u.search?u.search:"").toLowerCase();
6553
+ var likelyBackgroundEndpoint=!!(u&&(
6554
+ /(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(path)||
6555
+ path==="/cart"||
6556
+ path==="/cart.js"||
6557
+ path.indexOf("/cart/")===0||
6558
+ path.indexOf("/recommendations/")===0||
6559
+ path.indexOf("/search/suggest")===0||
6560
+ qs.indexOf("sections=")!==-1||
6561
+ qs.indexOf("section_id=")!==-1
6562
+ ));
6563
+ var likelyThirdPartyBg=!sameOrigin||likelyBackgroundEndpoint;
6564
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&isSafeMethod&&likelyThirdPartyBg){
5967
6565
  console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
5968
6566
  return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
5969
6567
  }
@@ -5972,7 +6570,7 @@ if(window.fetch){
5972
6570
  });
5973
6571
  };
5974
6572
  }
5975
- 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);};}
6573
+ 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);};}
5976
6574
  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);};}
5977
6575
  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;}}
5978
6576
  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);