@accelerated-agency/visual-editor 0.3.3 → 0.3.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.
package/dist/vite.cjs CHANGED
@@ -355,21 +355,21 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
355
355
  .lp-sec{border-bottom:1px solid var(--border);flex-shrink:0;padding: 24px;}
356
356
  .lp-sec-no-border{border-bottom:none!important}
357
357
  .lp-sec-hd{
358
- padding:10px 12px 8px;font-size:14px;font-weight:600;
358
+ margin-bottom: 12px;
359
+ font-size:14px;font-weight:600;
359
360
  color:#404040;display:flex;align-items:center;justify-content:space-between;gap:5px
360
361
  }
361
362
  .lp-sec-hd-left{display:flex;align-items:center;gap:5px}
362
363
  #active-var-label{display:none;color:var(--accent-txt);font-size:10px;font-weight:500}
363
364
  .lp-info-icon{font-size:11px;color:var(--text-3);cursor:default;opacity:.7}
364
365
  .lp-add-btn{
365
- display:none;
366
366
  background:none;border:none;color:var(--text);font-size:14px;font-weight:500;
367
367
  cursor:pointer;padding:2px 5px;border-radius:4px;transition:all .12s;flex-shrink:0
368
368
  }
369
369
  .lp-add-btn:hover{background:var(--bg-hover);color:var(--accent-txt)}
370
370
 
371
371
  /* \u2500\u2500 Variation tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
372
- #variation-tabs{padding:2px 0 6px;display:flex;flex-direction:column; gap:8px;}
372
+ #variation-tabs{display:flex;flex-direction:column; gap:8px;}
373
373
  .var-tab{
374
374
  border-radius: 4px;
375
375
  border: 1px solid #e5e7eb;
@@ -400,7 +400,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
400
400
  #comp-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,102,241,.12)}
401
401
 
402
402
  /* \u2500\u2500 Left tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
403
- .lp-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;background:#fff}
403
+ .lp-tabs, .section-components-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;background:#fff}
404
404
  .lp-tab{
405
405
  flex:1;padding:8px 2px;text-align:center;font-size:10px;color:var(--text-3);
406
406
  cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;font-weight:600;line-height:1.15
@@ -410,10 +410,10 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
410
410
  .future-hidden{display:none!important}
411
411
 
412
412
  /* \u2500\u2500 Left panel body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
413
- .lp-body{flex:1;overflow-y:auto}
414
- .lp-body::-webkit-scrollbar{width:3px}
415
- .lp-body::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
416
- .tab-pane{display:none}.tab-pane.active{display:block}
413
+ .lp-body, .section-components-body{flex:1;overflow-y:auto}
414
+ .lp-body::-webkit-scrollbar, .section-components-body::-webkit-scrollbar{width:3px}
415
+ .lp-body::-webkit-scrollbar-thumb, .section-components-body::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
416
+ .tab-pane, .section-components-tab-pane{display:none}.tab-pane.active, .section-components-tab-pane.active{display:block}
417
417
 
418
418
  /* \u2500\u2500 Component grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
419
419
  .cg-hdr{padding:8px 10px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)}
@@ -625,12 +625,83 @@ select.pr-inp{cursor:pointer;background:#fff}
625
625
  }
626
626
  #states-clear:hover{border-color:#fca5a5;color:#ef4444;background:#fef2f2}
627
627
  #history-clear{
628
- display:block;width:calc(100% - 24px);margin:10px 12px;
628
+ display:block;width:calc(100% - 24px);margin:10px 12px 8px;
629
629
  background:none;border:1px solid var(--border);border-radius:6px;
630
630
  padding:6px;font-size:11px;color:var(--text-2);cursor:pointer;font-family:inherit;
631
631
  transition:all .15s
632
632
  }
633
633
  #history-clear:hover{border-color:#fca5a5;color:#ef4444;background:#fef2f2}
634
+ .history-timeline{
635
+ position:relative;
636
+ margin:8px 0 12px;
637
+ padding:0 12px 0 36px;
638
+ }
639
+ .history-timeline::before{
640
+ content:'';
641
+ position:absolute;
642
+ left:20px;
643
+ top:4px;
644
+ bottom:4px;
645
+ width:2px;
646
+ background:#eceff3;
647
+ border-radius:2px;
648
+ }
649
+ .history-item{
650
+ position:relative;
651
+ display:flex;
652
+ align-items:flex-start;
653
+ gap:10px;
654
+ padding:10px 8px 10px 0;
655
+ cursor:pointer;
656
+ }
657
+ .history-dot{
658
+ position:absolute;
659
+ left:-20px;
660
+ top:18px;
661
+ width:10px;
662
+ height:10px;
663
+ border-radius:50%;
664
+ background:#fff;
665
+ border:2px solid #d7dde6;
666
+ }
667
+ .history-card{
668
+ flex:1;
669
+ min-width:0;
670
+ }
671
+ .history-title{
672
+ font-size:14px;
673
+ font-weight:600;
674
+ color:var(--text);
675
+ line-height:1.3;
676
+ }
677
+ .history-meta{
678
+ margin-top:4px;
679
+ display:flex;
680
+ align-items:center;
681
+ gap:6px;
682
+ color:var(--text-2);
683
+ font-size:11px;
684
+ }
685
+ .history-avatar{
686
+ width:18px;
687
+ height:18px;
688
+ border-radius:50%;
689
+ background:#e9e5ff;
690
+ color:#5b47d6;
691
+ display:flex;
692
+ align-items:center;
693
+ justify-content:center;
694
+ font-size:10px;
695
+ font-weight:700;
696
+ }
697
+ .history-time{
698
+ margin-top:3px;
699
+ font-size:11px;
700
+ color:var(--text-3);
701
+ }
702
+ .history-remove{
703
+ margin-top:2px;
704
+ }
634
705
  </style>
635
706
  </head>
636
707
  <body class="mode-editor">
@@ -712,7 +783,7 @@ select.pr-inp{cursor:pointer;background:#fff}
712
783
  <div class="lp-sec">
713
784
  <div class="lp-sec-hd">
714
785
  <span class="lp-sec-hd-left">Variations <span id="active-var-label"></span></span>
715
- <button class="lp-add-btn" title="Add variation">+ Add</button>
786
+ <button class="lp-add-btn" style="display:none" title="Add variation">+ Add</button>
716
787
  </div>
717
788
  <div id="variation-tabs"></div>
718
789
  </div>
@@ -737,27 +808,39 @@ select.pr-inp{cursor:pointer;background:#fff}
737
808
  <span class="lp-sec-hd-left">Elements <i class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
738
809
  <button class="lp-add-btn" title="Add element">+ Add</button>
739
810
  </div>
740
- </div>
741
811
 
742
- <!-- Search (hidden, kept for JS) -->
743
- <div style="display:none">
812
+ <!-- Search (hidden, kept for JS) -->
813
+ <div>
744
814
  <input type="search" id="comp-search" placeholder="Search layers\u2026" autocomplete="off">
745
815
  </div>
746
816
 
817
+
747
818
  <!-- Tabs (hidden, kept for JS) -->
748
- <div class="lp-tabs" style="display:none">
749
- <div class="lp-tab active" onclick="switchLeftTab('elements')">Elements</div>
750
- <div class="lp-tab future-hidden" onclick="switchLeftTab('components')">Components</div>
751
- <div class="lp-tab future-hidden" onclick="switchLeftTab('sections')">Sections</div>
819
+ <div class="lp-tabs" >
820
+ <div class="lp-tab active" onclick="switchLeftTab('elements')">Elements</div>
821
+ <div class="lp-tab" onclick="switchLeftTab('dom-tree')">DOM Tree</div>
822
+ </div>
823
+
752
824
  </div>
753
825
 
754
826
  <!-- Tab content -->
755
827
  <div class="lp-body">
756
- <div id="tab-elements" class="tab-pane active"><div id="dom-tree-root" class="dt-tree"></div></div>
757
- <div id="tab-components" class="tab-pane future-hidden"></div>
758
- <div id="tab-sections" class="tab-pane future-hidden"></div>
828
+ <div id="tab-dom-tree" class="tab-pane">
829
+ <div id="dom-tree-root" class="dt-tree">
830
+ </div>
831
+ </div>
832
+ <div id="tab-elements" class="tab-pane active">
833
+ <div id="elements-root" class="elements-tree">
834
+ </div>
835
+ </div>
759
836
  </div>
760
837
 
838
+
839
+
840
+
841
+
842
+
843
+
761
844
  </div><!-- #left-panel -->
762
845
 
763
846
  <!-- Center / iframe panel -->
@@ -765,7 +848,8 @@ select.pr-inp{cursor:pointer;background:#fff}
765
848
 
766
849
  <!-- Floating toolbar for selected element (positioned over iframe) -->
767
850
  <div id="selection-floater" aria-label="Selection actions">
768
- <button type="button" class="sf-btn" id="sf-drag" title="Move up/down (drag on page after activating)"><i class="bi bi-arrows-move"></i></button>
851
+ <button type="button" class="sf-btn" id="sf-move-up" title="Move up"><i class="bi bi-arrow-up"></i></button>
852
+ <button type="button" class="sf-btn" id="sf-move-down" title="Move down"><i class="bi bi-arrow-down"></i></button>
769
853
  <span class="sf-sep"></span>
770
854
  <button type="button" class="sf-btn" id="sf-resize" disabled title="Resize (coming soon)"><i class="bi bi-arrows-angle-expand"></i></button>
771
855
  <button type="button" class="sf-btn" id="sf-rotate" disabled title="Rotate (coming soon)"><i class="bi bi-arrow-repeat"></i></button>
@@ -792,13 +876,20 @@ select.pr-inp{cursor:pointer;background:#fff}
792
876
 
793
877
  <!-- Right panel -->
794
878
  <div id="right-panel">
795
-
879
+ <!-- Left-tab controls moved here -->
880
+ <div class="section-components-tabs">
881
+ <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
882
+ <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
883
+ </div>
884
+ <div class="lp-body">
885
+ <div id="tab-components" class="tab-pane"></div>
886
+ <div id="tab-sections" class="tab-pane"></div>
887
+ </div>
796
888
  <!-- Element badge (hidden until selection) -->
797
889
  <div id="el-info" style="display:none">
798
890
  <div id="el-info-tag"></div>
799
891
  <div id="el-info-sel"></div>
800
892
  </div>
801
-
802
893
  <!-- \u2500\u2500 3 main tabs \u2500\u2500 -->
803
894
  <div id="main-tabs">
804
895
  <button class="main-tab active" onclick="switchMainTab('design')">Design</button>
@@ -880,12 +971,7 @@ select.pr-inp{cursor:pointer;background:#fff}
880
971
 
881
972
  <!-- \u2500\u2500 States pane \u2500\u2500 -->
882
973
  <div id="tab-states" class="rp-pane">
883
- <div id="states-list">
884
- <div class="states-empty">
885
- <i class="bi bi-layers"></i>
886
- No changes yet \u2014 edit elements on the page to see states here
887
- </div>
888
- </div>
974
+ <div id="states-list"></div>
889
975
  </div><!-- #tab-states -->
890
976
 
891
977
  <!-- \u2500\u2500 History pane (saved DB changesets for active variation) \u2500\u2500 -->
@@ -1133,6 +1219,7 @@ var suppressClickUntil = 0;
1133
1219
  var dragAttachDoc = null;
1134
1220
  var currentMainTab = 'design';
1135
1221
  var currentLeftTab = 'elements';
1222
+ var currentSectionComponentsTab = 'components';
1136
1223
  var dragHandleActive = false;
1137
1224
  var domTreeCollapsed = {};
1138
1225
  var domTreeRefreshTimer = null;
@@ -1164,6 +1251,13 @@ var stateChangesByVarId = {};
1164
1251
  var appliedChangesetSnapshots = {};
1165
1252
  /** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
1166
1253
  var baselineChangesetsByVarId = {};
1254
+ /** Monotonic timestamp key for ordering mixed live + saved history rows. */
1255
+ var vveHistorySeq = 0;
1256
+
1257
+ function nextHistoryTimestamp() {
1258
+ vveHistorySeq += 1;
1259
+ return Date.now() * 1000 + vveHistorySeq;
1260
+ }
1167
1261
 
1168
1262
  // \u2500\u2500 Dirty tracking (compare DB baseline + session stateChanges vs current export) \u2500\u2500
1169
1263
  function beginSuppressIframeMutationDirty() {
@@ -1379,27 +1473,45 @@ function setDevice(device) {
1379
1473
 
1380
1474
  // \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
1381
1475
  function switchLeftTab(tab) {
1476
+ if (tab !== 'elements' && tab !== 'dom-tree') return;
1382
1477
  currentLeftTab = tab;
1383
- var tabs = document.querySelectorAll('.lp-tab');
1384
- tabs[0].classList.toggle('active', tab === 'elements');
1385
- tabs[1].classList.toggle('active', tab === 'components');
1386
- tabs[2].classList.toggle('active', tab === 'sections');
1387
- document.getElementById('tab-elements').classList.toggle('active', tab === 'elements');
1388
- document.getElementById('tab-components').classList.toggle('active', tab === 'components');
1389
- document.getElementById('tab-sections').classList.toggle('active', tab === 'sections');
1478
+ var tabs = document.querySelectorAll('.lp-tabs .lp-tab');
1479
+ for (var i = 0; i < tabs.length; i++) {
1480
+ var oc = tabs[i].getAttribute('onclick') || '';
1481
+ tabs[i].classList.toggle('active', oc.indexOf("switchLeftTab('" + tab + "')") >= 0);
1482
+ }
1483
+ var paneNames = ['elements', 'dom-tree'];
1484
+ for (var p = 0; p < paneNames.length; p++) {
1485
+ var pane = document.getElementById('tab-' + paneNames[p]);
1486
+ if (pane) pane.classList.toggle('active', paneNames[p] === tab);
1487
+ }
1390
1488
  var inp = document.getElementById('comp-search');
1391
1489
  if (tab === 'elements') {
1490
+ inp.placeholder = 'Search elements\u2026';
1491
+ renderElementsTree(inp.value);
1492
+ } else if (tab === 'dom-tree') {
1392
1493
  inp.placeholder = 'Search layers\u2026';
1393
1494
  renderDomTree(inp.value);
1394
- } else if (tab === 'sections') {
1395
- inp.placeholder = 'Search sections\u2026';
1396
- renderSidebar(inp.value);
1397
- } else {
1398
- inp.placeholder = 'Search components\u2026';
1399
- renderSidebar(inp.value);
1400
1495
  }
1401
1496
  }
1402
1497
 
1498
+ function switchSectionComponentsTab(tab) {
1499
+ if (tab !== 'components' && tab !== 'sections') return;
1500
+ currentSectionComponentsTab = tab;
1501
+ var tabs = document.querySelectorAll('.section-components-tabs .lp-tab');
1502
+ for (var i = 0; i < tabs.length; i++) {
1503
+ var oc = tabs[i].getAttribute('onclick') || '';
1504
+ tabs[i].classList.toggle('active', oc.indexOf("switchSectionComponentsTab('" + tab + "')") >= 0);
1505
+ }
1506
+ var compPane = document.getElementById('tab-components');
1507
+ var secPane = document.getElementById('tab-sections');
1508
+ if (compPane) compPane.classList.toggle('active', tab === 'components');
1509
+ if (secPane) secPane.classList.toggle('active', tab === 'sections');
1510
+ var inp = document.getElementById('comp-search');
1511
+ if (inp) inp.placeholder = tab === 'sections' ? 'Search sections\u2026' : 'Search components\u2026';
1512
+ renderSidebar(inp ? inp.value : '');
1513
+ }
1514
+
1403
1515
  // \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
1404
1516
  function toggleAcc(name) {
1405
1517
  var sec = document.getElementById('acc-' + name);
@@ -1522,42 +1634,20 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
1522
1634
  : (originalValue != null ? originalValue : '');
1523
1635
  var entry = {
1524
1636
  selector: selector, inputId: inputId, label: meta.label,
1525
- cssProp: meta.cssProp, value: value, targetEl: targetEl, originalValue: orig
1637
+ cssProp: meta.cssProp, value: value, targetEl: targetEl, originalValue: orig, vveTs: nextHistoryTimestamp()
1526
1638
  };
1527
1639
  if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
1528
1640
  }
1529
1641
  if (currentMainTab === 'states') renderStatesTab();
1642
+ if (currentMainTab === 'history') renderHistoryTab();
1530
1643
  commitStateChangesForActiveVariation();
1531
1644
  recomputeEditorDirty();
1532
1645
  }
1533
1646
 
1534
1647
  function renderStatesTab() {
1535
1648
  var container = document.getElementById('states-list');
1536
- if (!stateChanges.length) {
1537
- container.innerHTML = '<div class="states-empty"><i class="bi bi-layers"></i>No changes yet \u2014 edit elements on the page to see states here</div>';
1538
- return;
1539
- }
1540
- // Group by selector
1541
- var groups = {};
1542
- var order = [];
1543
- stateChanges.forEach(function(c) {
1544
- if (!groups[c.selector]) { groups[c.selector] = []; order.push(c.selector); }
1545
- groups[c.selector].push(c);
1546
- });
1547
- var html = '<button id="states-clear" onclick="clearAllStates()"><i class="bi bi-trash3"></i> Clear all changes</button>';
1548
- order.forEach(function(sel) {
1549
- html += '<div class="state-group"><div class="state-group-sel">'+esc(sel)+'</div>';
1550
- groups[sel].forEach(function(c) {
1551
- var idx = stateChanges.indexOf(c);
1552
- html += '<div class="state-item">' +
1553
- '<span class="state-item-label">'+esc(c.label)+'</span>' +
1554
- '<span class="state-item-val" title="'+esc(c.value)+'">'+esc(c.value)+'</span>' +
1555
- '<button class="state-remove" title="Remove this change" onclick="removeStateChange('+idx+')">&#x2715;</button>' +
1556
- '</div>';
1557
- });
1558
- html += '</div>';
1559
- });
1560
- container.innerHTML = html;
1649
+ if (!container) return;
1650
+ container.innerHTML = '';
1561
1651
  }
1562
1652
 
1563
1653
  // Resolve a live DOM element for a state-change entry.
@@ -1636,7 +1726,9 @@ function removeStateChange(idx) {
1636
1726
  stateChanges.splice(idx, 1);
1637
1727
  commitStateChangesForActiveVariation();
1638
1728
  renderStatesTab();
1729
+ if (currentMainTab === 'history') renderHistoryTab();
1639
1730
  recomputeEditorDirty();
1731
+ scheduleDomTreeRefresh();
1640
1732
  }
1641
1733
 
1642
1734
  function clearAllStates() {
@@ -1647,7 +1739,9 @@ function clearAllStates() {
1647
1739
  stateChanges = [];
1648
1740
  commitStateChangesForActiveVariation();
1649
1741
  renderStatesTab();
1742
+ if (currentMainTab === 'history') renderHistoryTab();
1650
1743
  recomputeEditorDirty();
1744
+ scheduleDomTreeRefresh();
1651
1745
  }
1652
1746
 
1653
1747
  // \u2500\u2500 History tab (saved changesets from DB for active variation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -1820,61 +1914,169 @@ function historyEntryValuePreview(entry) {
1820
1914
  return '';
1821
1915
  }
1822
1916
 
1917
+ function getHistoryTimestampValue(raw, fallback) {
1918
+ var n = Number(raw);
1919
+ if (Number.isFinite(n) && n > 0) return n;
1920
+ return fallback;
1921
+ }
1922
+
1923
+ function historyTimestampForChangeset(entry, idx) {
1924
+ var base = idx + 1;
1925
+ if (!entry) return base;
1926
+ return getHistoryTimestampValue(
1927
+ entry.vveTs != null ? entry.vveTs : (entry.timestamp != null ? entry.timestamp : entry.ts),
1928
+ base,
1929
+ );
1930
+ }
1931
+
1932
+ function historyTimestampForStateChange(change, idx) {
1933
+ return getHistoryTimestampValue(change && change.vveTs, idx + 1);
1934
+ }
1935
+
1936
+ function formatHistoryTimestamp(ts) {
1937
+ if (!Number.isFinite(ts) || ts <= 0) return '';
1938
+ var ms = ts > 9999999999999 ? Math.floor(ts / 1000) : ts;
1939
+ var d = new Date(ms);
1940
+ if (isNaN(d.getTime())) return '';
1941
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1942
+ }
1943
+
1944
+ function formatHistoryRelativeTime(ts) {
1945
+ if (!Number.isFinite(ts) || ts <= 0) return '';
1946
+ var ms = ts > 9999999999999 ? Math.floor(ts / 1000) : ts;
1947
+ var diff = Math.max(0, Date.now() - ms);
1948
+ var sec = Math.floor(diff / 1000);
1949
+ if (sec < 5) return 'just now';
1950
+ if (sec < 60) return sec + ' seconds ago';
1951
+ var min = Math.floor(sec / 60);
1952
+ if (min < 60) return min + ' minute' + (min === 1 ? '' : 's') + ' ago';
1953
+ var hr = Math.floor(min / 60);
1954
+ if (hr < 24) return hr + ' hour' + (hr === 1 ? '' : 's') + ' ago';
1955
+ var day = Math.floor(hr / 24);
1956
+ return day + ' day' + (day === 1 ? '' : 's') + ' ago';
1957
+ }
1958
+
1959
+ function getUnifiedHistoryItems() {
1960
+ var out = [];
1961
+ var v = getActiveVariationForHistory();
1962
+ var saved = v ? parseVariationChangesets(v) : [];
1963
+ for (var i = 0; i < saved.length; i++) {
1964
+ var e = saved[i];
1965
+ out.push({
1966
+ source: 'saved',
1967
+ idx: i,
1968
+ selector: (e && e.selector) || '(unknown)',
1969
+ label: historyEntryTypeLabel(e),
1970
+ value: historyEntryValuePreview(e),
1971
+ ts: historyTimestampForChangeset(e, i),
1972
+ tsLabel: formatHistoryTimestamp(historyTimestampForChangeset(e, i)),
1973
+ actor: 'Saved changeset',
1974
+ });
1975
+ }
1976
+ var live = stateChanges || [];
1977
+ for (var j = 0; j < live.length; j++) {
1978
+ var c = live[j];
1979
+ if (!c) continue;
1980
+ var ts = historyTimestampForStateChange(c, j + saved.length);
1981
+ out.push({
1982
+ source: 'live',
1983
+ idx: j,
1984
+ selector: c.selector || '(unknown)',
1985
+ label: c.label || 'Live change',
1986
+ value: c.value != null ? String(c.value).slice(0, 120) : '',
1987
+ ts: ts,
1988
+ tsLabel: formatHistoryTimestamp(ts),
1989
+ actor: 'You',
1990
+ });
1991
+ }
1992
+ out.sort(function(a, b) {
1993
+ if (b.ts !== a.ts) return b.ts - a.ts;
1994
+ if (a.source !== b.source) return a.source === 'live' ? -1 : 1;
1995
+ return b.idx - a.idx;
1996
+ });
1997
+ return out;
1998
+ }
1999
+
1823
2000
  function renderHistoryTab() {
1824
2001
  var container = document.getElementById('history-list');
1825
2002
  if (!container) return;
1826
- var v = getActiveVariationForHistory();
1827
- var arr = v ? parseVariationChangesets(v) : [];
1828
- if (!arr.length) {
2003
+ var items = getUnifiedHistoryItems();
2004
+ if (!items.length) {
1829
2005
  container.innerHTML =
1830
- '<div class="states-empty"><i class="bi bi-clock-history"></i>No saved changesets for this variation \u2014 use Finalize to persist edits to the server</div>';
2006
+ '<div class="states-empty"><i class="bi bi-clock-history"></i>No changes yet</div>';
1831
2007
  return;
1832
2008
  }
1833
- var groups = {};
1834
- var order = [];
1835
- for (var gi = 0; gi < arr.length; gi++) {
1836
- var entry = arr[gi];
1837
- var sel = entry.selector || '(unknown)';
1838
- if (!groups[sel]) {
1839
- groups[sel] = [];
1840
- order.push(sel);
1841
- }
1842
- groups[sel].push({ entry: entry, idx: gi });
1843
- }
1844
2009
  var html =
1845
- '<button type="button" id="history-clear" onclick="clearAllHistoryChangesets()"><i class="bi bi-trash3"></i> Clear all saved changes</button>';
1846
- order.forEach(function(sel) {
1847
- html += '<div class="state-group"><div class="state-group-sel">' + esc(sel) + '</div>';
1848
- groups[sel].forEach(function(item) {
1849
- var lab = historyEntryTypeLabel(item.entry);
1850
- var val = historyEntryValuePreview(item.entry);
1851
- html +=
1852
- '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1853
- item.idx +
1854
- ')">' +
1855
- '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1856
- item.idx +
1857
- '</span>' +
1858
- '<span class="state-item-label">' +
1859
- esc(lab) +
1860
- '</span>' +
1861
- '<span class="state-item-val" title="' +
1862
- esc(val) +
1863
- '">' +
1864
- esc(val) +
1865
- '</span>' +
1866
- '<button type="button" class="state-remove" title="Remove this saved row (#' +
1867
- item.idx +
1868
- ')" onclick="removeHistoryChangeset(' +
1869
- item.idx +
1870
- ', event)">&#x2715;</button>' +
1871
- '</div>';
1872
- });
1873
- html += '</div>';
1874
- });
2010
+ '<button type="button" id="history-clear" onclick="clearAllUnifiedHistory()"><i class="bi bi-trash3"></i> Clear all changes</button>';
2011
+ html += '<div class="history-timeline">';
2012
+ for (var i = 0; i < items.length; i++) {
2013
+ var it = items[i];
2014
+ var title = 'Edit - ' + (it.label || 'Change');
2015
+ var avatarLabel = it.source === 'live' ? 'Y' : 'S';
2016
+ var timeText = formatHistoryRelativeTime(it.ts) || (it.tsLabel || '');
2017
+ html +=
2018
+ '<div class="history-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryItem(&quot;' +
2019
+ esc(it.source) +
2020
+ '&quot;,' +
2021
+ it.idx +
2022
+ ')">' +
2023
+ '<span class="history-dot"></span>' +
2024
+ '<div class="history-card">' +
2025
+ '<div class="history-title">' + esc(title) + '</div>' +
2026
+ '<div class="history-meta">' +
2027
+ '<span class="history-avatar">' + esc(avatarLabel) + '</span>' +
2028
+ '<span>' + esc(it.actor || 'Editor') + '</span>' +
2029
+ '</div>' +
2030
+ '<div class="history-time">' + esc(timeText || 'n/a') + '</div>' +
2031
+ '</div>' +
2032
+ '<button type="button" class="state-remove history-remove" title="Remove this change" onclick="removeHistoryItem(&quot;' +
2033
+ esc(it.source) +
2034
+ '&quot;,' +
2035
+ it.idx +
2036
+ ', event)">&#x2715;</button>' +
2037
+ '</div>';
2038
+ }
2039
+ html += '</div>';
1875
2040
  container.innerHTML = html;
1876
2041
  }
1877
2042
 
2043
+ function focusHistoryItem(source, idx) {
2044
+ if (source === 'live') {
2045
+ var change = stateChanges[idx];
2046
+ if (!change || !change.selector) return;
2047
+ try {
2048
+ var iframe = document.getElementById('iframeId');
2049
+ var iframeDoc = iframe && iframe.contentDocument;
2050
+ if (!iframeDoc) return;
2051
+ var el = querySelectorResolved(iframeDoc, change.selector);
2052
+ if (!el) return;
2053
+ selectElement(el);
2054
+ try {
2055
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
2056
+ } catch(_) {
2057
+ el.scrollIntoView();
2058
+ }
2059
+ } catch(_) {}
2060
+ return;
2061
+ }
2062
+ focusHistoryChangeset(idx);
2063
+ }
2064
+
2065
+ function removeHistoryItem(source, idx, evt) {
2066
+ if (source === 'live') {
2067
+ if (evt && evt.stopPropagation) evt.stopPropagation();
2068
+ removeStateChange(idx);
2069
+ if (currentMainTab === 'history') renderHistoryTab();
2070
+ return;
2071
+ }
2072
+ removeHistoryChangeset(idx, evt);
2073
+ }
2074
+
2075
+ function getLatestHistoryUndoTarget() {
2076
+ var list = getUnifiedHistoryItems();
2077
+ return list.length ? list[0] : null;
2078
+ }
2079
+
1878
2080
  function changesetListHasStructural(arr) {
1879
2081
  if (!arr || !arr.length) return false;
1880
2082
  for (var i = 0; i < arr.length; i++) {
@@ -1986,6 +2188,12 @@ function clearAllHistoryChangesets() {
1986
2188
  softReloadEditorIframe();
1987
2189
  }
1988
2190
 
2191
+ function clearAllUnifiedHistory() {
2192
+ clearAllStates();
2193
+ clearAllHistoryChangesets();
2194
+ if (currentMainTab === 'history') renderHistoryTab();
2195
+ }
2196
+
1989
2197
  // \u2500\u2500 Persisted active variation (survives iframe / full page reload) \u2500\u2500\u2500\u2500\u2500\u2500\u2500
1990
2198
  /** All Visual Editor iframe keys in localStorage use this prefix \u2014 cleared on close. */
1991
2199
  var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
@@ -2066,7 +2274,8 @@ function handleLoadExperiment(data) {
2066
2274
  return;
2067
2275
  }
2068
2276
  var proxyUrl = '/api/conversion-proxy?password=' + encodeURIComponent(data.editorPassword || '') +
2069
- '&url=' + encodeURIComponent(pageUrl);
2277
+ '&url=' + encodeURIComponent(pageUrl) +
2278
+ '&strictObserverFreeze=' + encodeURIComponent(data && data.strictObserverFreeze ? '1' : '0');
2070
2279
 
2071
2280
  // Parent often re-posts load-experiment when React re-renders (new object identity) or
2072
2281
  // after mutations-changed. Reloading the iframe again wipes variant changesets mid-session.
@@ -2338,7 +2547,7 @@ function runConsistencyReconcile() {
2338
2547
  var doc = iframe && iframe.contentDocument;
2339
2548
  if (!doc || !doc.body) return;
2340
2549
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2341
- var cs = parseVariationChangesets(variation);
2550
+ var cs = buildPersistedChainSetsForVariation(variation);
2342
2551
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2343
2552
  var granular = filterGranularChangesetEntries(cs);
2344
2553
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
@@ -2598,6 +2807,7 @@ function mergeGranularChainSets(baseList, overlayList) {
2598
2807
  function appendSessionStructuralChainRow(varId, row) {
2599
2808
  if (!varId || !row) return;
2600
2809
  if (!sessionStructuralChainRowsByVarId[varId]) sessionStructuralChainRowsByVarId[varId] = [];
2810
+ if (row.vveTs == null) row.vveTs = nextHistoryTimestamp();
2601
2811
  sessionStructuralChainRowsByVarId[varId].push(row);
2602
2812
  }
2603
2813
 
@@ -2605,33 +2815,33 @@ function appendSessionStructuralChainRow(varId, row) {
2605
2815
  function stateChangeToChainSet(c) {
2606
2816
  if (!c || !c.selector) return null;
2607
2817
  if (c.cssProp) {
2608
- return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value };
2818
+ return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2609
2819
  }
2610
2820
  switch (c.inputId) {
2611
2821
  case 'pp-text':
2612
- return { selector: c.selector, type: 'content', value: c.value };
2822
+ return { selector: c.selector, type: 'content', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2613
2823
  case 'pp-html':
2614
- return { selector: c.selector, type: 'content', html: c.value };
2824
+ return { selector: c.selector, type: 'content', html: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2615
2825
  case 'pp-cls':
2616
- return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value };
2826
+ return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2617
2827
  case 'pp-id':
2618
- return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value };
2828
+ return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2619
2829
  case 'pp-href':
2620
- return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value };
2830
+ return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2621
2831
  case 'pp-target':
2622
- return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value };
2832
+ return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2623
2833
  case 'pp-src':
2624
- return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value };
2834
+ return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2625
2835
  case 'pp-alt':
2626
- return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value };
2836
+ return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2627
2837
  case 'pp-ph':
2628
- return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value };
2838
+ return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2629
2839
  case 'pp-css':
2630
- return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value };
2840
+ return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2631
2841
  case 'pp-mob-css':
2632
- return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value };
2842
+ return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2633
2843
  case 'pp-tab-css':
2634
- return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value };
2844
+ return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2635
2845
  default:
2636
2846
  return null;
2637
2847
  }
@@ -2919,7 +3129,7 @@ function applyActiveVariationHtml() {
2919
3129
  if (!iframeDoc || !iframeDoc.body) return;
2920
3130
 
2921
3131
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2922
- var cs = parseVariationChangesets(variation);
3132
+ var cs = buildPersistedChainSetsForVariation(variation);
2923
3133
  refreshPersistentChangesetStyleTagForActiveVariation();
2924
3134
 
2925
3135
  beginSuppressIframeMutationDirty();
@@ -2977,7 +3187,7 @@ function applyVariationGranularOnly(iframeDoc) {
2977
3187
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2978
3188
  if (varHtmlCache[activeVarId]) return;
2979
3189
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2980
- var cs = parseVariationChangesets(variation);
3190
+ var cs = buildPersistedChainSetsForVariation(variation);
2981
3191
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2982
3192
  beginSuppressIframeMutationDirty();
2983
3193
  try {
@@ -2996,7 +3206,7 @@ function reapplyActiveVariationGranular(iframeDoc) {
2996
3206
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2997
3207
  if (varHtmlCache[activeVarId]) return;
2998
3208
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2999
- var cs = parseVariationChangesets(variation);
3209
+ var cs = buildPersistedChainSetsForVariation(variation);
3000
3210
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
3001
3211
  beginSuppressIframeMutationDirty();
3002
3212
  try {
@@ -3040,7 +3250,7 @@ function startIframeContentApplyWatcher(navGen, prevDocRef) {
3040
3250
 
3041
3251
  if (doc.readyState === 'loading') {
3042
3252
  var variation = variations.find(function(v) { return v._id === activeVarId; });
3043
- var cs0 = parseVariationChangesets(variation);
3253
+ var cs0 = buildPersistedChainSetsForVariation(variation);
3044
3254
  if (!cs0.length || changesetsHaveBodySnapshot(cs0)) return;
3045
3255
  var granular = filterGranularChangesetEntries(cs0);
3046
3256
  if (!granular.length) return;
@@ -3086,8 +3296,9 @@ function selectElement(el) {
3086
3296
  document.getElementById('no-sel').style.display = 'none';
3087
3297
  renderRightPanel(el);
3088
3298
  updateSelectionToolbar();
3089
- if (currentLeftTab === 'elements') {
3090
- var dr = document.getElementById('dom-tree-root');
3299
+ if (currentLeftTab === 'elements' || currentLeftTab === 'dom-tree') {
3300
+ var treeRootId = currentLeftTab === 'elements' ? 'elements-root' : 'dom-tree-root';
3301
+ var dr = document.getElementById(treeRootId);
3091
3302
  if (dr && dr.querySelector('.dt-row')) syncDomTreeSelection();
3092
3303
  else scheduleDomTreeRefresh();
3093
3304
  }
@@ -3350,27 +3561,32 @@ function deleteSelectedEl() {
3350
3561
  }
3351
3562
 
3352
3563
  function syncDomTreeSelection() {
3353
- var root = document.getElementById('dom-tree-root');
3354
- if (!root) return;
3355
- var rows = root.querySelectorAll('.dt-row');
3356
- for (var i = 0; i < rows.length; i++) {
3357
- rows[i].classList.toggle('dt-selected', !!(selectedEl && rows[i]._dtEl === selectedEl));
3358
- }
3359
- if (!selectedEl) return;
3360
- var found = null;
3361
- for (var j = 0; j < rows.length; j++) {
3362
- if (rows[j]._dtEl === selectedEl) { found = rows[j]; break; }
3564
+ var roots = ['dom-tree-root', 'elements-root'];
3565
+ for (var r = 0; r < roots.length; r++) {
3566
+ var root = document.getElementById(roots[r]);
3567
+ if (!root) continue;
3568
+ var rows = root.querySelectorAll('.dt-row');
3569
+ for (var i = 0; i < rows.length; i++) {
3570
+ rows[i].classList.toggle('dt-selected', !!(selectedEl && rows[i]._dtEl === selectedEl));
3571
+ }
3572
+ if (!selectedEl) continue;
3573
+ var found = null;
3574
+ for (var j = 0; j < rows.length; j++) {
3575
+ if (rows[j]._dtEl === selectedEl) { found = rows[j]; break; }
3576
+ }
3577
+ if (found) found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3363
3578
  }
3364
- if (found) found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3365
3579
  }
3366
3580
 
3367
3581
  function scheduleDomTreeRefresh() {
3368
- if (currentLeftTab !== 'elements') return;
3582
+ if (currentLeftTab !== 'elements' && currentLeftTab !== 'dom-tree') return;
3369
3583
  if (domTreeRefreshTimer) clearTimeout(domTreeRefreshTimer);
3370
3584
  domTreeRefreshTimer = setTimeout(function() {
3371
3585
  domTreeRefreshTimer = null;
3372
3586
  var inp = document.getElementById('comp-search');
3373
- renderDomTree(inp ? inp.value : '');
3587
+ var q = inp ? inp.value : '';
3588
+ if (currentLeftTab === 'elements') renderElementsTree(q);
3589
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
3374
3590
  }, 150);
3375
3591
  }
3376
3592
 
@@ -3393,6 +3609,129 @@ function domTreePathSegment(el) {
3393
3609
  return tag + '[' + idx + ']';
3394
3610
  }
3395
3611
 
3612
+ function renderElementsTree(filterRaw) {
3613
+ var filterText = (filterRaw || '').toLowerCase().trim();
3614
+ var root = document.getElementById('elements-root');
3615
+ if (!root) return;
3616
+ var iframe = document.getElementById('iframeId');
3617
+ var doc = iframe && iframe.contentDocument;
3618
+ if (!isIframeDomReady(iframe, doc)) {
3619
+ root.innerHTML = '<div class="dt-muted">Load a page to see the elements.</div>';
3620
+ return;
3621
+ }
3622
+
3623
+ function skippable(el) {
3624
+ return isDomTreeSkippableTagName(el.tagName);
3625
+ }
3626
+
3627
+ function nodeIcon(tag) {
3628
+ tag = (tag || '').toLowerCase();
3629
+ if (/^h[1-6]$/.test(tag)) return 'bi bi-type-h1';
3630
+ if (tag === 'a') return 'bi bi-link-45deg';
3631
+ if (tag === 'img') return 'bi bi-image';
3632
+ if (tag === 'section' || tag === 'main' || tag === 'article' || tag === 'header' || tag === 'footer' || tag === 'nav') return 'bi bi-layout-three-columns';
3633
+ if (tag === 'button' || tag === 'input' || tag === 'select' || tag === 'textarea') return 'bi bi-ui-radios';
3634
+ if (tag === 'ul' || tag === 'ol') return 'bi bi-list-ul';
3635
+ if (tag === 'li') return 'bi bi-dot';
3636
+ if (tag === 'svg') return 'bi bi-bezier2';
3637
+ if (tag === 'p' || tag === 'span') return 'bi bi-text-left';
3638
+ return 'bi bi-square';
3639
+ }
3640
+
3641
+ function labelFor(el) {
3642
+ var tag = (el.tagName || '').toLowerCase();
3643
+ return tag.toUpperCase();
3644
+ }
3645
+
3646
+ function isListableNode(el) {
3647
+ if (!el || el.nodeType !== 1) return false;
3648
+ if (skippable(el)) return false;
3649
+ var tag = (el.tagName || '').toLowerCase();
3650
+ if (tag !== 'svg') {
3651
+ var p = el.parentElement;
3652
+ while (p) {
3653
+ if ((p.tagName || '').toLowerCase() === 'svg') return false;
3654
+ p = p.parentElement;
3655
+ }
3656
+ }
3657
+ return true;
3658
+ }
3659
+
3660
+ var nodes = [];
3661
+ var i, cursor;
3662
+ try {
3663
+ cursor = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, null);
3664
+ } catch(_) {
3665
+ cursor = null;
3666
+ }
3667
+ if (cursor) {
3668
+ while (cursor.nextNode()) {
3669
+ var node = cursor.currentNode;
3670
+ if (isListableNode(node)) nodes.push(node);
3671
+ if (nodes.length > 4000) break;
3672
+ }
3673
+ } else {
3674
+ function collectFlat(el) {
3675
+ if (!el || !el.children) return;
3676
+ for (var j = 0; j < el.children.length; j++) {
3677
+ var c = el.children[j];
3678
+ if (!isListableNode(c)) continue;
3679
+ nodes.push(c);
3680
+ if (nodes.length > 4000) return;
3681
+ collectFlat(c);
3682
+ if (nodes.length > 4000) return;
3683
+ }
3684
+ }
3685
+ collectFlat(doc.body);
3686
+ }
3687
+
3688
+ root.innerHTML = '';
3689
+ for (i = 0; i < nodes.length; i++) {
3690
+ var el = nodes[i];
3691
+ var lblText = labelFor(el);
3692
+ if (filterText && lblText.toLowerCase().indexOf(filterText) < 0) continue;
3693
+
3694
+ var row = document.createElement('div');
3695
+ row.className = 'dt-row';
3696
+ row._dtEl = el;
3697
+ if (el === selectedEl) row.classList.add('dt-selected');
3698
+ row.style.paddingLeft = '4px';
3699
+
3700
+ var spacer = document.createElement('button');
3701
+ spacer.type = 'button';
3702
+ spacer.className = 'dt-chev dt-spacer';
3703
+
3704
+ var ico = document.createElement('div');
3705
+ ico.className = 'dt-ico';
3706
+ ico.innerHTML = '<i class="' + nodeIcon(el.tagName) + '"></i>';
3707
+
3708
+ var lbl = document.createElement('div');
3709
+ lbl.className = 'dt-lbl';
3710
+ lbl.textContent = lblText;
3711
+ lbl.title = buildSelector(el);
3712
+
3713
+ row.appendChild(spacer);
3714
+ row.appendChild(ico);
3715
+ row.appendChild(lbl);
3716
+ row.onclick = (function(targetEl) {
3717
+ return function() { selectElementFromTree(targetEl); };
3718
+ })(el);
3719
+ row.onmouseenter = (function(targetEl) {
3720
+ return function() { setTreeHoverHighlight(targetEl); };
3721
+ })(el);
3722
+ root.appendChild(row);
3723
+ }
3724
+
3725
+ if (!root.querySelector('.dt-row')) {
3726
+ root.innerHTML = filterText
3727
+ ? '<div class="dt-muted">No elements match your search.</div>'
3728
+ : '<div class="dt-muted">No elements found.</div>';
3729
+ }
3730
+ root.onmouseleave = function() {
3731
+ clearTreeHoverHighlight();
3732
+ };
3733
+ }
3734
+
3396
3735
  function renderDomTree(filterRaw) {
3397
3736
  var filterText = (filterRaw || '').toLowerCase().trim();
3398
3737
  var root = document.getElementById('dom-tree-root');
@@ -4131,6 +4470,20 @@ function recordReorderAfterDrag(movedEl) {
4131
4470
  }
4132
4471
  }
4133
4472
 
4473
+ function moveSelectedElByDirection(direction) {
4474
+ if (!selectedEl || !selectedEl.parentElement) return;
4475
+ var p = selectedEl.parentElement;
4476
+ var sibling = direction < 0 ? selectedEl.previousElementSibling : selectedEl.nextElementSibling;
4477
+ if (!sibling) return;
4478
+ if (direction < 0) p.insertBefore(selectedEl, sibling);
4479
+ else p.insertBefore(sibling, selectedEl);
4480
+ recordReorderAfterDrag(selectedEl);
4481
+ saveCurrentVariationHtml();
4482
+ recomputeEditorDirty();
4483
+ scheduleDomTreeRefresh();
4484
+ updateSelectionToolbar();
4485
+ }
4486
+
4134
4487
  function attachDragReposition() {
4135
4488
  try {
4136
4489
  var iframe = document.getElementById('iframeId');
@@ -4317,7 +4670,9 @@ function syncIframeInteractions(reason) {
4317
4670
  scheduleConsistencyReconcile();
4318
4671
  bindSelectionToolbarScroll();
4319
4672
  var inp = document.getElementById('comp-search');
4320
- renderDomTree(inp ? inp.value : '');
4673
+ var q = inp ? inp.value : '';
4674
+ if (currentLeftTab === 'elements') renderElementsTree(q);
4675
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
4321
4676
  updateSelectionToolbar();
4322
4677
  recomputeEditorDirty();
4323
4678
  } catch(_) {}
@@ -4481,8 +4836,11 @@ function renderSidebar(filter) {
4481
4836
  }
4482
4837
 
4483
4838
  document.getElementById('comp-search').addEventListener('input', function() {
4484
- if (currentLeftTab === 'elements') renderDomTree(this.value);
4485
- else renderSidebar(this.value);
4839
+ if (currentLeftTab === 'elements') renderElementsTree(this.value);
4840
+ else if (currentLeftTab === 'dom-tree') renderDomTree(this.value);
4841
+ if (currentSectionComponentsTab === 'components' || currentSectionComponentsTab === 'sections') {
4842
+ renderSidebar(this.value);
4843
+ }
4486
4844
  });
4487
4845
 
4488
4846
  // \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
@@ -4548,19 +4906,11 @@ document.addEventListener('keydown', function(e) {
4548
4906
  var k = (e.key || '').toLowerCase();
4549
4907
  if (meta && !e.shiftKey && k === 'z') {
4550
4908
  e.preventDefault();
4551
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4552
- Vvveb.Undo.undo();
4553
- saveCurrentVariationHtml();
4554
- recomputeEditorDirty();
4555
- }
4909
+ runEditorUndo();
4556
4910
  }
4557
4911
  if (meta && e.shiftKey && k === 'z') {
4558
4912
  e.preventDefault();
4559
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4560
- Vvveb.Undo.redo();
4561
- saveCurrentVariationHtml();
4562
- recomputeEditorDirty();
4563
- }
4913
+ runEditorRedo();
4564
4914
  }
4565
4915
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
4566
4916
  if (e.key === 'Escape') {
@@ -4580,8 +4930,39 @@ document.addEventListener('keydown', function(e) {
4580
4930
  if (selectedEl) deselectElement();
4581
4931
  }
4582
4932
  });
4583
- document.getElementById('btn-undo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.undo(); });
4584
- document.getElementById('btn-redo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.redo(); });
4933
+ function runEditorUndo() {
4934
+ var target = getLatestHistoryUndoTarget();
4935
+ if (target) {
4936
+ removeHistoryItem(target.source, target.idx);
4937
+ return;
4938
+ }
4939
+ if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
4940
+ Vvveb.Undo.undo();
4941
+ saveCurrentVariationHtml();
4942
+ recomputeEditorDirty();
4943
+ scheduleDomTreeRefresh();
4944
+ updateSelectionToolbar();
4945
+ }
4946
+
4947
+ function runEditorRedo() {
4948
+ if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
4949
+ Vvveb.Undo.redo();
4950
+ saveCurrentVariationHtml();
4951
+ recomputeEditorDirty();
4952
+ scheduleDomTreeRefresh();
4953
+ updateSelectionToolbar();
4954
+ }
4955
+
4956
+ document.getElementById('btn-undo').addEventListener('click', function(e) {
4957
+ e.preventDefault();
4958
+ e.stopPropagation();
4959
+ runEditorUndo();
4960
+ });
4961
+ document.getElementById('btn-redo').addEventListener('click', function(e) {
4962
+ e.preventDefault();
4963
+ e.stopPropagation();
4964
+ runEditorRedo();
4965
+ });
4585
4966
 
4586
4967
  function layoutLoadingTooltip(host) {
4587
4968
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -4657,8 +5038,8 @@ function registerCROSections() {
4657
5038
 
4658
5039
  window.addEventListener('load', function() {
4659
5040
  registerCROSections();
4660
- renderSidebar();
4661
- renderDomTree(document.getElementById('comp-search').value);
5041
+ switchSectionComponentsTab(currentSectionComponentsTab);
5042
+ renderElementsTree(document.getElementById('comp-search').value);
4662
5043
  vvvebReady = true;
4663
5044
  bindLoadingTooltipPositioning();
4664
5045
 
@@ -4711,12 +5092,22 @@ window.addEventListener('load', function() {
4711
5092
  syncIframeInteractions('iframe-load');
4712
5093
  });
4713
5094
 
4714
- document.getElementById('sf-drag').addEventListener('click', function(e) {
4715
- e.preventDefault();
4716
- e.stopPropagation();
4717
- if (!selectedEl) return;
4718
- setDragHandleActive(!dragHandleActive);
4719
- });
5095
+ var sfMoveUp = document.getElementById('sf-move-up');
5096
+ if (sfMoveUp) {
5097
+ sfMoveUp.addEventListener('click', function(e) {
5098
+ e.preventDefault();
5099
+ e.stopPropagation();
5100
+ moveSelectedElByDirection(-1);
5101
+ });
5102
+ }
5103
+ var sfMoveDown = document.getElementById('sf-move-down');
5104
+ if (sfMoveDown) {
5105
+ sfMoveDown.addEventListener('click', function(e) {
5106
+ e.preventDefault();
5107
+ e.stopPropagation();
5108
+ moveSelectedElByDirection(1);
5109
+ });
5110
+ }
4720
5111
  document.getElementById('sf-dup').addEventListener('click', function(e) {
4721
5112
  e.preventDefault();
4722
5113
  e.stopPropagation();
@@ -4762,6 +5153,7 @@ var getDefaultAnthropicApiKey = () => {
4762
5153
  function createVisualEditorMiddleware(options) {
4763
5154
  const anthropicApiKey = options?.anthropicApiKey || getDefaultAnthropicApiKey();
4764
5155
  const enableGenerateTestApi = options?.enableGenerateTestApi ?? true;
5156
+ const strictObserverFreeze = options?.strictObserverFreeze === true;
4765
5157
  const allowedFrameOrigins = options?.allowedFrameOrigins ?? ["*"];
4766
5158
  function setFrameHeaders(req, res) {
4767
5159
  res.removeHeader("X-Frame-Options");
@@ -4929,6 +5321,8 @@ function createVisualEditorMiddleware(options) {
4929
5321
  const url = new URL(req.url || "", "http://localhost");
4930
5322
  const targetUrl = url.searchParams.get("url");
4931
5323
  const password = url.searchParams.get("password") || "";
5324
+ const strictFreezeParam = (url.searchParams.get("strictObserverFreeze") || "").toLowerCase();
5325
+ const strictObserverFreezeForRequest = strictFreezeParam === "1" || strictFreezeParam === "true" || strictFreezeParam === "yes" ? true : strictFreezeParam === "0" || strictFreezeParam === "false" || strictFreezeParam === "no" ? false : strictObserverFreeze;
4932
5326
  if (!targetUrl) {
4933
5327
  res.statusCode = 400;
4934
5328
  res.end(JSON.stringify({ error: "Missing url parameter" }));
@@ -5066,6 +5460,7 @@ ${iframeAlwaysShowCssGuardScript}
5066
5460
  var TARGET_ORIGIN=${JSON.stringify(origin)};
5067
5461
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
5068
5462
  var PROXY_PASSWORD=${JSON.stringify(password)};
5463
+ var STRICT_OBSERVER_FREEZE=${JSON.stringify(strictObserverFreezeForRequest)};
5069
5464
  window.__CONVERSION_EDITOR_ACTIVE__=true;
5070
5465
  function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
5071
5466
  function toAbsolute(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")||raw.startsWith("//")?TARGET_ORIGIN:TARGET_PAGE_URL;return new URL(raw,base).toString();}catch(_){return raw;}}
@@ -5085,6 +5480,9 @@ try{
5085
5480
  var wrapped=function(list,obs){
5086
5481
  try{
5087
5482
  if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
5483
+ if(STRICT_OBSERVER_FREEZE){
5484
+ return;
5485
+ }
5088
5486
  var now=Date.now();
5089
5487
  if(now-last<120)return;
5090
5488
  last=now;
@@ -5094,6 +5492,11 @@ try{
5094
5492
  return new NativeMO(wrapped);
5095
5493
  };
5096
5494
  window.MutationObserver.prototype=NativeMO.prototype;
5495
+ try{
5496
+ if(STRICT_OBSERVER_FREEZE){
5497
+ console.info("[conversion-proxy] strict MutationObserver freeze active");
5498
+ }
5499
+ }catch(_){}
5097
5500
  }
5098
5501
  }catch(_){}
5099
5502
  }catch(_){}})();</script>`;