@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.js CHANGED
@@ -347,21 +347,21 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
347
347
  .lp-sec{border-bottom:1px solid var(--border);flex-shrink:0;padding: 24px;}
348
348
  .lp-sec-no-border{border-bottom:none!important}
349
349
  .lp-sec-hd{
350
- padding:10px 12px 8px;font-size:14px;font-weight:600;
350
+ margin-bottom: 12px;
351
+ font-size:14px;font-weight:600;
351
352
  color:#404040;display:flex;align-items:center;justify-content:space-between;gap:5px
352
353
  }
353
354
  .lp-sec-hd-left{display:flex;align-items:center;gap:5px}
354
355
  #active-var-label{display:none;color:var(--accent-txt);font-size:10px;font-weight:500}
355
356
  .lp-info-icon{font-size:11px;color:var(--text-3);cursor:default;opacity:.7}
356
357
  .lp-add-btn{
357
- display:none;
358
358
  background:none;border:none;color:var(--text);font-size:14px;font-weight:500;
359
359
  cursor:pointer;padding:2px 5px;border-radius:4px;transition:all .12s;flex-shrink:0
360
360
  }
361
361
  .lp-add-btn:hover{background:var(--bg-hover);color:var(--accent-txt)}
362
362
 
363
363
  /* \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 */
364
- #variation-tabs{padding:2px 0 6px;display:flex;flex-direction:column; gap:8px;}
364
+ #variation-tabs{display:flex;flex-direction:column; gap:8px;}
365
365
  .var-tab{
366
366
  border-radius: 4px;
367
367
  border: 1px solid #e5e7eb;
@@ -392,7 +392,7 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
392
392
  #comp-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,102,241,.12)}
393
393
 
394
394
  /* \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 */
395
- .lp-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;background:#fff}
395
+ .lp-tabs, .section-components-tabs{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;background:#fff}
396
396
  .lp-tab{
397
397
  flex:1;padding:8px 2px;text-align:center;font-size:10px;color:var(--text-3);
398
398
  cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;font-weight:600;line-height:1.15
@@ -402,10 +402,10 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
402
402
  .future-hidden{display:none!important}
403
403
 
404
404
  /* \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 */
405
- .lp-body{flex:1;overflow-y:auto}
406
- .lp-body::-webkit-scrollbar{width:3px}
407
- .lp-body::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
408
- .tab-pane{display:none}.tab-pane.active{display:block}
405
+ .lp-body, .section-components-body{flex:1;overflow-y:auto}
406
+ .lp-body::-webkit-scrollbar, .section-components-body::-webkit-scrollbar{width:3px}
407
+ .lp-body::-webkit-scrollbar-thumb, .section-components-body::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
408
+ .tab-pane, .section-components-tab-pane{display:none}.tab-pane.active, .section-components-tab-pane.active{display:block}
409
409
 
410
410
  /* \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 */
411
411
  .cg-hdr{padding:8px 10px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)}
@@ -617,12 +617,83 @@ select.pr-inp{cursor:pointer;background:#fff}
617
617
  }
618
618
  #states-clear:hover{border-color:#fca5a5;color:#ef4444;background:#fef2f2}
619
619
  #history-clear{
620
- display:block;width:calc(100% - 24px);margin:10px 12px;
620
+ display:block;width:calc(100% - 24px);margin:10px 12px 8px;
621
621
  background:none;border:1px solid var(--border);border-radius:6px;
622
622
  padding:6px;font-size:11px;color:var(--text-2);cursor:pointer;font-family:inherit;
623
623
  transition:all .15s
624
624
  }
625
625
  #history-clear:hover{border-color:#fca5a5;color:#ef4444;background:#fef2f2}
626
+ .history-timeline{
627
+ position:relative;
628
+ margin:8px 0 12px;
629
+ padding:0 12px 0 36px;
630
+ }
631
+ .history-timeline::before{
632
+ content:'';
633
+ position:absolute;
634
+ left:20px;
635
+ top:4px;
636
+ bottom:4px;
637
+ width:2px;
638
+ background:#eceff3;
639
+ border-radius:2px;
640
+ }
641
+ .history-item{
642
+ position:relative;
643
+ display:flex;
644
+ align-items:flex-start;
645
+ gap:10px;
646
+ padding:10px 8px 10px 0;
647
+ cursor:pointer;
648
+ }
649
+ .history-dot{
650
+ position:absolute;
651
+ left:-20px;
652
+ top:18px;
653
+ width:10px;
654
+ height:10px;
655
+ border-radius:50%;
656
+ background:#fff;
657
+ border:2px solid #d7dde6;
658
+ }
659
+ .history-card{
660
+ flex:1;
661
+ min-width:0;
662
+ }
663
+ .history-title{
664
+ font-size:14px;
665
+ font-weight:600;
666
+ color:var(--text);
667
+ line-height:1.3;
668
+ }
669
+ .history-meta{
670
+ margin-top:4px;
671
+ display:flex;
672
+ align-items:center;
673
+ gap:6px;
674
+ color:var(--text-2);
675
+ font-size:11px;
676
+ }
677
+ .history-avatar{
678
+ width:18px;
679
+ height:18px;
680
+ border-radius:50%;
681
+ background:#e9e5ff;
682
+ color:#5b47d6;
683
+ display:flex;
684
+ align-items:center;
685
+ justify-content:center;
686
+ font-size:10px;
687
+ font-weight:700;
688
+ }
689
+ .history-time{
690
+ margin-top:3px;
691
+ font-size:11px;
692
+ color:var(--text-3);
693
+ }
694
+ .history-remove{
695
+ margin-top:2px;
696
+ }
626
697
  </style>
627
698
  </head>
628
699
  <body class="mode-editor">
@@ -704,7 +775,7 @@ select.pr-inp{cursor:pointer;background:#fff}
704
775
  <div class="lp-sec">
705
776
  <div class="lp-sec-hd">
706
777
  <span class="lp-sec-hd-left">Variations <span id="active-var-label"></span></span>
707
- <button class="lp-add-btn" title="Add variation">+ Add</button>
778
+ <button class="lp-add-btn" style="display:none" title="Add variation">+ Add</button>
708
779
  </div>
709
780
  <div id="variation-tabs"></div>
710
781
  </div>
@@ -729,27 +800,39 @@ select.pr-inp{cursor:pointer;background:#fff}
729
800
  <span class="lp-sec-hd-left">Elements <i class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
730
801
  <button class="lp-add-btn" title="Add element">+ Add</button>
731
802
  </div>
732
- </div>
733
803
 
734
- <!-- Search (hidden, kept for JS) -->
735
- <div style="display:none">
804
+ <!-- Search (hidden, kept for JS) -->
805
+ <div>
736
806
  <input type="search" id="comp-search" placeholder="Search layers\u2026" autocomplete="off">
737
807
  </div>
738
808
 
809
+
739
810
  <!-- Tabs (hidden, kept for JS) -->
740
- <div class="lp-tabs" style="display:none">
741
- <div class="lp-tab active" onclick="switchLeftTab('elements')">Elements</div>
742
- <div class="lp-tab future-hidden" onclick="switchLeftTab('components')">Components</div>
743
- <div class="lp-tab future-hidden" onclick="switchLeftTab('sections')">Sections</div>
811
+ <div class="lp-tabs" >
812
+ <div class="lp-tab active" onclick="switchLeftTab('elements')">Elements</div>
813
+ <div class="lp-tab" onclick="switchLeftTab('dom-tree')">DOM Tree</div>
814
+ </div>
815
+
744
816
  </div>
745
817
 
746
818
  <!-- Tab content -->
747
819
  <div class="lp-body">
748
- <div id="tab-elements" class="tab-pane active"><div id="dom-tree-root" class="dt-tree"></div></div>
749
- <div id="tab-components" class="tab-pane future-hidden"></div>
750
- <div id="tab-sections" class="tab-pane future-hidden"></div>
820
+ <div id="tab-dom-tree" class="tab-pane">
821
+ <div id="dom-tree-root" class="dt-tree">
822
+ </div>
823
+ </div>
824
+ <div id="tab-elements" class="tab-pane active">
825
+ <div id="elements-root" class="elements-tree">
826
+ </div>
827
+ </div>
751
828
  </div>
752
829
 
830
+
831
+
832
+
833
+
834
+
835
+
753
836
  </div><!-- #left-panel -->
754
837
 
755
838
  <!-- Center / iframe panel -->
@@ -757,7 +840,8 @@ select.pr-inp{cursor:pointer;background:#fff}
757
840
 
758
841
  <!-- Floating toolbar for selected element (positioned over iframe) -->
759
842
  <div id="selection-floater" aria-label="Selection actions">
760
- <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>
843
+ <button type="button" class="sf-btn" id="sf-move-up" title="Move up"><i class="bi bi-arrow-up"></i></button>
844
+ <button type="button" class="sf-btn" id="sf-move-down" title="Move down"><i class="bi bi-arrow-down"></i></button>
761
845
  <span class="sf-sep"></span>
762
846
  <button type="button" class="sf-btn" id="sf-resize" disabled title="Resize (coming soon)"><i class="bi bi-arrows-angle-expand"></i></button>
763
847
  <button type="button" class="sf-btn" id="sf-rotate" disabled title="Rotate (coming soon)"><i class="bi bi-arrow-repeat"></i></button>
@@ -784,13 +868,20 @@ select.pr-inp{cursor:pointer;background:#fff}
784
868
 
785
869
  <!-- Right panel -->
786
870
  <div id="right-panel">
787
-
871
+ <!-- Left-tab controls moved here -->
872
+ <div class="section-components-tabs">
873
+ <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
874
+ <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
875
+ </div>
876
+ <div class="lp-body">
877
+ <div id="tab-components" class="tab-pane"></div>
878
+ <div id="tab-sections" class="tab-pane"></div>
879
+ </div>
788
880
  <!-- Element badge (hidden until selection) -->
789
881
  <div id="el-info" style="display:none">
790
882
  <div id="el-info-tag"></div>
791
883
  <div id="el-info-sel"></div>
792
884
  </div>
793
-
794
885
  <!-- \u2500\u2500 3 main tabs \u2500\u2500 -->
795
886
  <div id="main-tabs">
796
887
  <button class="main-tab active" onclick="switchMainTab('design')">Design</button>
@@ -872,12 +963,7 @@ select.pr-inp{cursor:pointer;background:#fff}
872
963
 
873
964
  <!-- \u2500\u2500 States pane \u2500\u2500 -->
874
965
  <div id="tab-states" class="rp-pane">
875
- <div id="states-list">
876
- <div class="states-empty">
877
- <i class="bi bi-layers"></i>
878
- No changes yet \u2014 edit elements on the page to see states here
879
- </div>
880
- </div>
966
+ <div id="states-list"></div>
881
967
  </div><!-- #tab-states -->
882
968
 
883
969
  <!-- \u2500\u2500 History pane (saved DB changesets for active variation) \u2500\u2500 -->
@@ -1125,6 +1211,7 @@ var suppressClickUntil = 0;
1125
1211
  var dragAttachDoc = null;
1126
1212
  var currentMainTab = 'design';
1127
1213
  var currentLeftTab = 'elements';
1214
+ var currentSectionComponentsTab = 'components';
1128
1215
  var dragHandleActive = false;
1129
1216
  var domTreeCollapsed = {};
1130
1217
  var domTreeRefreshTimer = null;
@@ -1156,6 +1243,13 @@ var stateChangesByVarId = {};
1156
1243
  var appliedChangesetSnapshots = {};
1157
1244
  /** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
1158
1245
  var baselineChangesetsByVarId = {};
1246
+ /** Monotonic timestamp key for ordering mixed live + saved history rows. */
1247
+ var vveHistorySeq = 0;
1248
+
1249
+ function nextHistoryTimestamp() {
1250
+ vveHistorySeq += 1;
1251
+ return Date.now() * 1000 + vveHistorySeq;
1252
+ }
1159
1253
 
1160
1254
  // \u2500\u2500 Dirty tracking (compare DB baseline + session stateChanges vs current export) \u2500\u2500
1161
1255
  function beginSuppressIframeMutationDirty() {
@@ -1371,27 +1465,45 @@ function setDevice(device) {
1371
1465
 
1372
1466
  // \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
1373
1467
  function switchLeftTab(tab) {
1468
+ if (tab !== 'elements' && tab !== 'dom-tree') return;
1374
1469
  currentLeftTab = tab;
1375
- var tabs = document.querySelectorAll('.lp-tab');
1376
- tabs[0].classList.toggle('active', tab === 'elements');
1377
- tabs[1].classList.toggle('active', tab === 'components');
1378
- tabs[2].classList.toggle('active', tab === 'sections');
1379
- document.getElementById('tab-elements').classList.toggle('active', tab === 'elements');
1380
- document.getElementById('tab-components').classList.toggle('active', tab === 'components');
1381
- document.getElementById('tab-sections').classList.toggle('active', tab === 'sections');
1470
+ var tabs = document.querySelectorAll('.lp-tabs .lp-tab');
1471
+ for (var i = 0; i < tabs.length; i++) {
1472
+ var oc = tabs[i].getAttribute('onclick') || '';
1473
+ tabs[i].classList.toggle('active', oc.indexOf("switchLeftTab('" + tab + "')") >= 0);
1474
+ }
1475
+ var paneNames = ['elements', 'dom-tree'];
1476
+ for (var p = 0; p < paneNames.length; p++) {
1477
+ var pane = document.getElementById('tab-' + paneNames[p]);
1478
+ if (pane) pane.classList.toggle('active', paneNames[p] === tab);
1479
+ }
1382
1480
  var inp = document.getElementById('comp-search');
1383
1481
  if (tab === 'elements') {
1482
+ inp.placeholder = 'Search elements\u2026';
1483
+ renderElementsTree(inp.value);
1484
+ } else if (tab === 'dom-tree') {
1384
1485
  inp.placeholder = 'Search layers\u2026';
1385
1486
  renderDomTree(inp.value);
1386
- } else if (tab === 'sections') {
1387
- inp.placeholder = 'Search sections\u2026';
1388
- renderSidebar(inp.value);
1389
- } else {
1390
- inp.placeholder = 'Search components\u2026';
1391
- renderSidebar(inp.value);
1392
1487
  }
1393
1488
  }
1394
1489
 
1490
+ function switchSectionComponentsTab(tab) {
1491
+ if (tab !== 'components' && tab !== 'sections') return;
1492
+ currentSectionComponentsTab = tab;
1493
+ var tabs = document.querySelectorAll('.section-components-tabs .lp-tab');
1494
+ for (var i = 0; i < tabs.length; i++) {
1495
+ var oc = tabs[i].getAttribute('onclick') || '';
1496
+ tabs[i].classList.toggle('active', oc.indexOf("switchSectionComponentsTab('" + tab + "')") >= 0);
1497
+ }
1498
+ var compPane = document.getElementById('tab-components');
1499
+ var secPane = document.getElementById('tab-sections');
1500
+ if (compPane) compPane.classList.toggle('active', tab === 'components');
1501
+ if (secPane) secPane.classList.toggle('active', tab === 'sections');
1502
+ var inp = document.getElementById('comp-search');
1503
+ if (inp) inp.placeholder = tab === 'sections' ? 'Search sections\u2026' : 'Search components\u2026';
1504
+ renderSidebar(inp ? inp.value : '');
1505
+ }
1506
+
1395
1507
  // \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
1396
1508
  function toggleAcc(name) {
1397
1509
  var sec = document.getElementById('acc-' + name);
@@ -1514,42 +1626,20 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
1514
1626
  : (originalValue != null ? originalValue : '');
1515
1627
  var entry = {
1516
1628
  selector: selector, inputId: inputId, label: meta.label,
1517
- cssProp: meta.cssProp, value: value, targetEl: targetEl, originalValue: orig
1629
+ cssProp: meta.cssProp, value: value, targetEl: targetEl, originalValue: orig, vveTs: nextHistoryTimestamp()
1518
1630
  };
1519
1631
  if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
1520
1632
  }
1521
1633
  if (currentMainTab === 'states') renderStatesTab();
1634
+ if (currentMainTab === 'history') renderHistoryTab();
1522
1635
  commitStateChangesForActiveVariation();
1523
1636
  recomputeEditorDirty();
1524
1637
  }
1525
1638
 
1526
1639
  function renderStatesTab() {
1527
1640
  var container = document.getElementById('states-list');
1528
- if (!stateChanges.length) {
1529
- 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>';
1530
- return;
1531
- }
1532
- // Group by selector
1533
- var groups = {};
1534
- var order = [];
1535
- stateChanges.forEach(function(c) {
1536
- if (!groups[c.selector]) { groups[c.selector] = []; order.push(c.selector); }
1537
- groups[c.selector].push(c);
1538
- });
1539
- var html = '<button id="states-clear" onclick="clearAllStates()"><i class="bi bi-trash3"></i> Clear all changes</button>';
1540
- order.forEach(function(sel) {
1541
- html += '<div class="state-group"><div class="state-group-sel">'+esc(sel)+'</div>';
1542
- groups[sel].forEach(function(c) {
1543
- var idx = stateChanges.indexOf(c);
1544
- html += '<div class="state-item">' +
1545
- '<span class="state-item-label">'+esc(c.label)+'</span>' +
1546
- '<span class="state-item-val" title="'+esc(c.value)+'">'+esc(c.value)+'</span>' +
1547
- '<button class="state-remove" title="Remove this change" onclick="removeStateChange('+idx+')">&#x2715;</button>' +
1548
- '</div>';
1549
- });
1550
- html += '</div>';
1551
- });
1552
- container.innerHTML = html;
1641
+ if (!container) return;
1642
+ container.innerHTML = '';
1553
1643
  }
1554
1644
 
1555
1645
  // Resolve a live DOM element for a state-change entry.
@@ -1628,7 +1718,9 @@ function removeStateChange(idx) {
1628
1718
  stateChanges.splice(idx, 1);
1629
1719
  commitStateChangesForActiveVariation();
1630
1720
  renderStatesTab();
1721
+ if (currentMainTab === 'history') renderHistoryTab();
1631
1722
  recomputeEditorDirty();
1723
+ scheduleDomTreeRefresh();
1632
1724
  }
1633
1725
 
1634
1726
  function clearAllStates() {
@@ -1639,7 +1731,9 @@ function clearAllStates() {
1639
1731
  stateChanges = [];
1640
1732
  commitStateChangesForActiveVariation();
1641
1733
  renderStatesTab();
1734
+ if (currentMainTab === 'history') renderHistoryTab();
1642
1735
  recomputeEditorDirty();
1736
+ scheduleDomTreeRefresh();
1643
1737
  }
1644
1738
 
1645
1739
  // \u2500\u2500 History tab (saved changesets from DB for active variation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -1812,61 +1906,169 @@ function historyEntryValuePreview(entry) {
1812
1906
  return '';
1813
1907
  }
1814
1908
 
1909
+ function getHistoryTimestampValue(raw, fallback) {
1910
+ var n = Number(raw);
1911
+ if (Number.isFinite(n) && n > 0) return n;
1912
+ return fallback;
1913
+ }
1914
+
1915
+ function historyTimestampForChangeset(entry, idx) {
1916
+ var base = idx + 1;
1917
+ if (!entry) return base;
1918
+ return getHistoryTimestampValue(
1919
+ entry.vveTs != null ? entry.vveTs : (entry.timestamp != null ? entry.timestamp : entry.ts),
1920
+ base,
1921
+ );
1922
+ }
1923
+
1924
+ function historyTimestampForStateChange(change, idx) {
1925
+ return getHistoryTimestampValue(change && change.vveTs, idx + 1);
1926
+ }
1927
+
1928
+ function formatHistoryTimestamp(ts) {
1929
+ if (!Number.isFinite(ts) || ts <= 0) return '';
1930
+ var ms = ts > 9999999999999 ? Math.floor(ts / 1000) : ts;
1931
+ var d = new Date(ms);
1932
+ if (isNaN(d.getTime())) return '';
1933
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1934
+ }
1935
+
1936
+ function formatHistoryRelativeTime(ts) {
1937
+ if (!Number.isFinite(ts) || ts <= 0) return '';
1938
+ var ms = ts > 9999999999999 ? Math.floor(ts / 1000) : ts;
1939
+ var diff = Math.max(0, Date.now() - ms);
1940
+ var sec = Math.floor(diff / 1000);
1941
+ if (sec < 5) return 'just now';
1942
+ if (sec < 60) return sec + ' seconds ago';
1943
+ var min = Math.floor(sec / 60);
1944
+ if (min < 60) return min + ' minute' + (min === 1 ? '' : 's') + ' ago';
1945
+ var hr = Math.floor(min / 60);
1946
+ if (hr < 24) return hr + ' hour' + (hr === 1 ? '' : 's') + ' ago';
1947
+ var day = Math.floor(hr / 24);
1948
+ return day + ' day' + (day === 1 ? '' : 's') + ' ago';
1949
+ }
1950
+
1951
+ function getUnifiedHistoryItems() {
1952
+ var out = [];
1953
+ var v = getActiveVariationForHistory();
1954
+ var saved = v ? parseVariationChangesets(v) : [];
1955
+ for (var i = 0; i < saved.length; i++) {
1956
+ var e = saved[i];
1957
+ out.push({
1958
+ source: 'saved',
1959
+ idx: i,
1960
+ selector: (e && e.selector) || '(unknown)',
1961
+ label: historyEntryTypeLabel(e),
1962
+ value: historyEntryValuePreview(e),
1963
+ ts: historyTimestampForChangeset(e, i),
1964
+ tsLabel: formatHistoryTimestamp(historyTimestampForChangeset(e, i)),
1965
+ actor: 'Saved changeset',
1966
+ });
1967
+ }
1968
+ var live = stateChanges || [];
1969
+ for (var j = 0; j < live.length; j++) {
1970
+ var c = live[j];
1971
+ if (!c) continue;
1972
+ var ts = historyTimestampForStateChange(c, j + saved.length);
1973
+ out.push({
1974
+ source: 'live',
1975
+ idx: j,
1976
+ selector: c.selector || '(unknown)',
1977
+ label: c.label || 'Live change',
1978
+ value: c.value != null ? String(c.value).slice(0, 120) : '',
1979
+ ts: ts,
1980
+ tsLabel: formatHistoryTimestamp(ts),
1981
+ actor: 'You',
1982
+ });
1983
+ }
1984
+ out.sort(function(a, b) {
1985
+ if (b.ts !== a.ts) return b.ts - a.ts;
1986
+ if (a.source !== b.source) return a.source === 'live' ? -1 : 1;
1987
+ return b.idx - a.idx;
1988
+ });
1989
+ return out;
1990
+ }
1991
+
1815
1992
  function renderHistoryTab() {
1816
1993
  var container = document.getElementById('history-list');
1817
1994
  if (!container) return;
1818
- var v = getActiveVariationForHistory();
1819
- var arr = v ? parseVariationChangesets(v) : [];
1820
- if (!arr.length) {
1995
+ var items = getUnifiedHistoryItems();
1996
+ if (!items.length) {
1821
1997
  container.innerHTML =
1822
- '<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>';
1998
+ '<div class="states-empty"><i class="bi bi-clock-history"></i>No changes yet</div>';
1823
1999
  return;
1824
2000
  }
1825
- var groups = {};
1826
- var order = [];
1827
- for (var gi = 0; gi < arr.length; gi++) {
1828
- var entry = arr[gi];
1829
- var sel = entry.selector || '(unknown)';
1830
- if (!groups[sel]) {
1831
- groups[sel] = [];
1832
- order.push(sel);
1833
- }
1834
- groups[sel].push({ entry: entry, idx: gi });
1835
- }
1836
2001
  var html =
1837
- '<button type="button" id="history-clear" onclick="clearAllHistoryChangesets()"><i class="bi bi-trash3"></i> Clear all saved changes</button>';
1838
- order.forEach(function(sel) {
1839
- html += '<div class="state-group"><div class="state-group-sel">' + esc(sel) + '</div>';
1840
- groups[sel].forEach(function(item) {
1841
- var lab = historyEntryTypeLabel(item.entry);
1842
- var val = historyEntryValuePreview(item.entry);
1843
- html +=
1844
- '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1845
- item.idx +
1846
- ')">' +
1847
- '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1848
- item.idx +
1849
- '</span>' +
1850
- '<span class="state-item-label">' +
1851
- esc(lab) +
1852
- '</span>' +
1853
- '<span class="state-item-val" title="' +
1854
- esc(val) +
1855
- '">' +
1856
- esc(val) +
1857
- '</span>' +
1858
- '<button type="button" class="state-remove" title="Remove this saved row (#' +
1859
- item.idx +
1860
- ')" onclick="removeHistoryChangeset(' +
1861
- item.idx +
1862
- ', event)">&#x2715;</button>' +
1863
- '</div>';
1864
- });
1865
- html += '</div>';
1866
- });
2002
+ '<button type="button" id="history-clear" onclick="clearAllUnifiedHistory()"><i class="bi bi-trash3"></i> Clear all changes</button>';
2003
+ html += '<div class="history-timeline">';
2004
+ for (var i = 0; i < items.length; i++) {
2005
+ var it = items[i];
2006
+ var title = 'Edit - ' + (it.label || 'Change');
2007
+ var avatarLabel = it.source === 'live' ? 'Y' : 'S';
2008
+ var timeText = formatHistoryRelativeTime(it.ts) || (it.tsLabel || '');
2009
+ html +=
2010
+ '<div class="history-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryItem(&quot;' +
2011
+ esc(it.source) +
2012
+ '&quot;,' +
2013
+ it.idx +
2014
+ ')">' +
2015
+ '<span class="history-dot"></span>' +
2016
+ '<div class="history-card">' +
2017
+ '<div class="history-title">' + esc(title) + '</div>' +
2018
+ '<div class="history-meta">' +
2019
+ '<span class="history-avatar">' + esc(avatarLabel) + '</span>' +
2020
+ '<span>' + esc(it.actor || 'Editor') + '</span>' +
2021
+ '</div>' +
2022
+ '<div class="history-time">' + esc(timeText || 'n/a') + '</div>' +
2023
+ '</div>' +
2024
+ '<button type="button" class="state-remove history-remove" title="Remove this change" onclick="removeHistoryItem(&quot;' +
2025
+ esc(it.source) +
2026
+ '&quot;,' +
2027
+ it.idx +
2028
+ ', event)">&#x2715;</button>' +
2029
+ '</div>';
2030
+ }
2031
+ html += '</div>';
1867
2032
  container.innerHTML = html;
1868
2033
  }
1869
2034
 
2035
+ function focusHistoryItem(source, idx) {
2036
+ if (source === 'live') {
2037
+ var change = stateChanges[idx];
2038
+ if (!change || !change.selector) return;
2039
+ try {
2040
+ var iframe = document.getElementById('iframeId');
2041
+ var iframeDoc = iframe && iframe.contentDocument;
2042
+ if (!iframeDoc) return;
2043
+ var el = querySelectorResolved(iframeDoc, change.selector);
2044
+ if (!el) return;
2045
+ selectElement(el);
2046
+ try {
2047
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
2048
+ } catch(_) {
2049
+ el.scrollIntoView();
2050
+ }
2051
+ } catch(_) {}
2052
+ return;
2053
+ }
2054
+ focusHistoryChangeset(idx);
2055
+ }
2056
+
2057
+ function removeHistoryItem(source, idx, evt) {
2058
+ if (source === 'live') {
2059
+ if (evt && evt.stopPropagation) evt.stopPropagation();
2060
+ removeStateChange(idx);
2061
+ if (currentMainTab === 'history') renderHistoryTab();
2062
+ return;
2063
+ }
2064
+ removeHistoryChangeset(idx, evt);
2065
+ }
2066
+
2067
+ function getLatestHistoryUndoTarget() {
2068
+ var list = getUnifiedHistoryItems();
2069
+ return list.length ? list[0] : null;
2070
+ }
2071
+
1870
2072
  function changesetListHasStructural(arr) {
1871
2073
  if (!arr || !arr.length) return false;
1872
2074
  for (var i = 0; i < arr.length; i++) {
@@ -1978,6 +2180,12 @@ function clearAllHistoryChangesets() {
1978
2180
  softReloadEditorIframe();
1979
2181
  }
1980
2182
 
2183
+ function clearAllUnifiedHistory() {
2184
+ clearAllStates();
2185
+ clearAllHistoryChangesets();
2186
+ if (currentMainTab === 'history') renderHistoryTab();
2187
+ }
2188
+
1981
2189
  // \u2500\u2500 Persisted active variation (survives iframe / full page reload) \u2500\u2500\u2500\u2500\u2500\u2500\u2500
1982
2190
  /** All Visual Editor iframe keys in localStorage use this prefix \u2014 cleared on close. */
1983
2191
  var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
@@ -2058,7 +2266,8 @@ function handleLoadExperiment(data) {
2058
2266
  return;
2059
2267
  }
2060
2268
  var proxyUrl = '/api/conversion-proxy?password=' + encodeURIComponent(data.editorPassword || '') +
2061
- '&url=' + encodeURIComponent(pageUrl);
2269
+ '&url=' + encodeURIComponent(pageUrl) +
2270
+ '&strictObserverFreeze=' + encodeURIComponent(data && data.strictObserverFreeze ? '1' : '0');
2062
2271
 
2063
2272
  // Parent often re-posts load-experiment when React re-renders (new object identity) or
2064
2273
  // after mutations-changed. Reloading the iframe again wipes variant changesets mid-session.
@@ -2330,7 +2539,7 @@ function runConsistencyReconcile() {
2330
2539
  var doc = iframe && iframe.contentDocument;
2331
2540
  if (!doc || !doc.body) return;
2332
2541
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2333
- var cs = parseVariationChangesets(variation);
2542
+ var cs = buildPersistedChainSetsForVariation(variation);
2334
2543
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2335
2544
  var granular = filterGranularChangesetEntries(cs);
2336
2545
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
@@ -2590,6 +2799,7 @@ function mergeGranularChainSets(baseList, overlayList) {
2590
2799
  function appendSessionStructuralChainRow(varId, row) {
2591
2800
  if (!varId || !row) return;
2592
2801
  if (!sessionStructuralChainRowsByVarId[varId]) sessionStructuralChainRowsByVarId[varId] = [];
2802
+ if (row.vveTs == null) row.vveTs = nextHistoryTimestamp();
2593
2803
  sessionStructuralChainRowsByVarId[varId].push(row);
2594
2804
  }
2595
2805
 
@@ -2597,33 +2807,33 @@ function appendSessionStructuralChainRow(varId, row) {
2597
2807
  function stateChangeToChainSet(c) {
2598
2808
  if (!c || !c.selector) return null;
2599
2809
  if (c.cssProp) {
2600
- return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value };
2810
+ return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2601
2811
  }
2602
2812
  switch (c.inputId) {
2603
2813
  case 'pp-text':
2604
- return { selector: c.selector, type: 'content', value: c.value };
2814
+ return { selector: c.selector, type: 'content', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2605
2815
  case 'pp-html':
2606
- return { selector: c.selector, type: 'content', html: c.value };
2816
+ return { selector: c.selector, type: 'content', html: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2607
2817
  case 'pp-cls':
2608
- return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value };
2818
+ return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2609
2819
  case 'pp-id':
2610
- return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value };
2820
+ return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2611
2821
  case 'pp-href':
2612
- return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value };
2822
+ return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2613
2823
  case 'pp-target':
2614
- return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value };
2824
+ return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2615
2825
  case 'pp-src':
2616
- return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value };
2826
+ return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2617
2827
  case 'pp-alt':
2618
- return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value };
2828
+ return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2619
2829
  case 'pp-ph':
2620
- return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value };
2830
+ return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2621
2831
  case 'pp-css':
2622
- return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value };
2832
+ return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2623
2833
  case 'pp-mob-css':
2624
- return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value };
2834
+ return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2625
2835
  case 'pp-tab-css':
2626
- return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value };
2836
+ return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2627
2837
  default:
2628
2838
  return null;
2629
2839
  }
@@ -2911,7 +3121,7 @@ function applyActiveVariationHtml() {
2911
3121
  if (!iframeDoc || !iframeDoc.body) return;
2912
3122
 
2913
3123
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2914
- var cs = parseVariationChangesets(variation);
3124
+ var cs = buildPersistedChainSetsForVariation(variation);
2915
3125
  refreshPersistentChangesetStyleTagForActiveVariation();
2916
3126
 
2917
3127
  beginSuppressIframeMutationDirty();
@@ -2969,7 +3179,7 @@ function applyVariationGranularOnly(iframeDoc) {
2969
3179
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2970
3180
  if (varHtmlCache[activeVarId]) return;
2971
3181
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2972
- var cs = parseVariationChangesets(variation);
3182
+ var cs = buildPersistedChainSetsForVariation(variation);
2973
3183
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2974
3184
  beginSuppressIframeMutationDirty();
2975
3185
  try {
@@ -2988,7 +3198,7 @@ function reapplyActiveVariationGranular(iframeDoc) {
2988
3198
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2989
3199
  if (varHtmlCache[activeVarId]) return;
2990
3200
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2991
- var cs = parseVariationChangesets(variation);
3201
+ var cs = buildPersistedChainSetsForVariation(variation);
2992
3202
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2993
3203
  beginSuppressIframeMutationDirty();
2994
3204
  try {
@@ -3032,7 +3242,7 @@ function startIframeContentApplyWatcher(navGen, prevDocRef) {
3032
3242
 
3033
3243
  if (doc.readyState === 'loading') {
3034
3244
  var variation = variations.find(function(v) { return v._id === activeVarId; });
3035
- var cs0 = parseVariationChangesets(variation);
3245
+ var cs0 = buildPersistedChainSetsForVariation(variation);
3036
3246
  if (!cs0.length || changesetsHaveBodySnapshot(cs0)) return;
3037
3247
  var granular = filterGranularChangesetEntries(cs0);
3038
3248
  if (!granular.length) return;
@@ -3078,8 +3288,9 @@ function selectElement(el) {
3078
3288
  document.getElementById('no-sel').style.display = 'none';
3079
3289
  renderRightPanel(el);
3080
3290
  updateSelectionToolbar();
3081
- if (currentLeftTab === 'elements') {
3082
- var dr = document.getElementById('dom-tree-root');
3291
+ if (currentLeftTab === 'elements' || currentLeftTab === 'dom-tree') {
3292
+ var treeRootId = currentLeftTab === 'elements' ? 'elements-root' : 'dom-tree-root';
3293
+ var dr = document.getElementById(treeRootId);
3083
3294
  if (dr && dr.querySelector('.dt-row')) syncDomTreeSelection();
3084
3295
  else scheduleDomTreeRefresh();
3085
3296
  }
@@ -3342,27 +3553,32 @@ function deleteSelectedEl() {
3342
3553
  }
3343
3554
 
3344
3555
  function syncDomTreeSelection() {
3345
- var root = document.getElementById('dom-tree-root');
3346
- if (!root) return;
3347
- var rows = root.querySelectorAll('.dt-row');
3348
- for (var i = 0; i < rows.length; i++) {
3349
- rows[i].classList.toggle('dt-selected', !!(selectedEl && rows[i]._dtEl === selectedEl));
3350
- }
3351
- if (!selectedEl) return;
3352
- var found = null;
3353
- for (var j = 0; j < rows.length; j++) {
3354
- if (rows[j]._dtEl === selectedEl) { found = rows[j]; break; }
3556
+ var roots = ['dom-tree-root', 'elements-root'];
3557
+ for (var r = 0; r < roots.length; r++) {
3558
+ var root = document.getElementById(roots[r]);
3559
+ if (!root) continue;
3560
+ var rows = root.querySelectorAll('.dt-row');
3561
+ for (var i = 0; i < rows.length; i++) {
3562
+ rows[i].classList.toggle('dt-selected', !!(selectedEl && rows[i]._dtEl === selectedEl));
3563
+ }
3564
+ if (!selectedEl) continue;
3565
+ var found = null;
3566
+ for (var j = 0; j < rows.length; j++) {
3567
+ if (rows[j]._dtEl === selectedEl) { found = rows[j]; break; }
3568
+ }
3569
+ if (found) found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3355
3570
  }
3356
- if (found) found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3357
3571
  }
3358
3572
 
3359
3573
  function scheduleDomTreeRefresh() {
3360
- if (currentLeftTab !== 'elements') return;
3574
+ if (currentLeftTab !== 'elements' && currentLeftTab !== 'dom-tree') return;
3361
3575
  if (domTreeRefreshTimer) clearTimeout(domTreeRefreshTimer);
3362
3576
  domTreeRefreshTimer = setTimeout(function() {
3363
3577
  domTreeRefreshTimer = null;
3364
3578
  var inp = document.getElementById('comp-search');
3365
- renderDomTree(inp ? inp.value : '');
3579
+ var q = inp ? inp.value : '';
3580
+ if (currentLeftTab === 'elements') renderElementsTree(q);
3581
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
3366
3582
  }, 150);
3367
3583
  }
3368
3584
 
@@ -3385,6 +3601,129 @@ function domTreePathSegment(el) {
3385
3601
  return tag + '[' + idx + ']';
3386
3602
  }
3387
3603
 
3604
+ function renderElementsTree(filterRaw) {
3605
+ var filterText = (filterRaw || '').toLowerCase().trim();
3606
+ var root = document.getElementById('elements-root');
3607
+ if (!root) return;
3608
+ var iframe = document.getElementById('iframeId');
3609
+ var doc = iframe && iframe.contentDocument;
3610
+ if (!isIframeDomReady(iframe, doc)) {
3611
+ root.innerHTML = '<div class="dt-muted">Load a page to see the elements.</div>';
3612
+ return;
3613
+ }
3614
+
3615
+ function skippable(el) {
3616
+ return isDomTreeSkippableTagName(el.tagName);
3617
+ }
3618
+
3619
+ function nodeIcon(tag) {
3620
+ tag = (tag || '').toLowerCase();
3621
+ if (/^h[1-6]$/.test(tag)) return 'bi bi-type-h1';
3622
+ if (tag === 'a') return 'bi bi-link-45deg';
3623
+ if (tag === 'img') return 'bi bi-image';
3624
+ if (tag === 'section' || tag === 'main' || tag === 'article' || tag === 'header' || tag === 'footer' || tag === 'nav') return 'bi bi-layout-three-columns';
3625
+ if (tag === 'button' || tag === 'input' || tag === 'select' || tag === 'textarea') return 'bi bi-ui-radios';
3626
+ if (tag === 'ul' || tag === 'ol') return 'bi bi-list-ul';
3627
+ if (tag === 'li') return 'bi bi-dot';
3628
+ if (tag === 'svg') return 'bi bi-bezier2';
3629
+ if (tag === 'p' || tag === 'span') return 'bi bi-text-left';
3630
+ return 'bi bi-square';
3631
+ }
3632
+
3633
+ function labelFor(el) {
3634
+ var tag = (el.tagName || '').toLowerCase();
3635
+ return tag.toUpperCase();
3636
+ }
3637
+
3638
+ function isListableNode(el) {
3639
+ if (!el || el.nodeType !== 1) return false;
3640
+ if (skippable(el)) return false;
3641
+ var tag = (el.tagName || '').toLowerCase();
3642
+ if (tag !== 'svg') {
3643
+ var p = el.parentElement;
3644
+ while (p) {
3645
+ if ((p.tagName || '').toLowerCase() === 'svg') return false;
3646
+ p = p.parentElement;
3647
+ }
3648
+ }
3649
+ return true;
3650
+ }
3651
+
3652
+ var nodes = [];
3653
+ var i, cursor;
3654
+ try {
3655
+ cursor = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, null);
3656
+ } catch(_) {
3657
+ cursor = null;
3658
+ }
3659
+ if (cursor) {
3660
+ while (cursor.nextNode()) {
3661
+ var node = cursor.currentNode;
3662
+ if (isListableNode(node)) nodes.push(node);
3663
+ if (nodes.length > 4000) break;
3664
+ }
3665
+ } else {
3666
+ function collectFlat(el) {
3667
+ if (!el || !el.children) return;
3668
+ for (var j = 0; j < el.children.length; j++) {
3669
+ var c = el.children[j];
3670
+ if (!isListableNode(c)) continue;
3671
+ nodes.push(c);
3672
+ if (nodes.length > 4000) return;
3673
+ collectFlat(c);
3674
+ if (nodes.length > 4000) return;
3675
+ }
3676
+ }
3677
+ collectFlat(doc.body);
3678
+ }
3679
+
3680
+ root.innerHTML = '';
3681
+ for (i = 0; i < nodes.length; i++) {
3682
+ var el = nodes[i];
3683
+ var lblText = labelFor(el);
3684
+ if (filterText && lblText.toLowerCase().indexOf(filterText) < 0) continue;
3685
+
3686
+ var row = document.createElement('div');
3687
+ row.className = 'dt-row';
3688
+ row._dtEl = el;
3689
+ if (el === selectedEl) row.classList.add('dt-selected');
3690
+ row.style.paddingLeft = '4px';
3691
+
3692
+ var spacer = document.createElement('button');
3693
+ spacer.type = 'button';
3694
+ spacer.className = 'dt-chev dt-spacer';
3695
+
3696
+ var ico = document.createElement('div');
3697
+ ico.className = 'dt-ico';
3698
+ ico.innerHTML = '<i class="' + nodeIcon(el.tagName) + '"></i>';
3699
+
3700
+ var lbl = document.createElement('div');
3701
+ lbl.className = 'dt-lbl';
3702
+ lbl.textContent = lblText;
3703
+ lbl.title = buildSelector(el);
3704
+
3705
+ row.appendChild(spacer);
3706
+ row.appendChild(ico);
3707
+ row.appendChild(lbl);
3708
+ row.onclick = (function(targetEl) {
3709
+ return function() { selectElementFromTree(targetEl); };
3710
+ })(el);
3711
+ row.onmouseenter = (function(targetEl) {
3712
+ return function() { setTreeHoverHighlight(targetEl); };
3713
+ })(el);
3714
+ root.appendChild(row);
3715
+ }
3716
+
3717
+ if (!root.querySelector('.dt-row')) {
3718
+ root.innerHTML = filterText
3719
+ ? '<div class="dt-muted">No elements match your search.</div>'
3720
+ : '<div class="dt-muted">No elements found.</div>';
3721
+ }
3722
+ root.onmouseleave = function() {
3723
+ clearTreeHoverHighlight();
3724
+ };
3725
+ }
3726
+
3388
3727
  function renderDomTree(filterRaw) {
3389
3728
  var filterText = (filterRaw || '').toLowerCase().trim();
3390
3729
  var root = document.getElementById('dom-tree-root');
@@ -4123,6 +4462,20 @@ function recordReorderAfterDrag(movedEl) {
4123
4462
  }
4124
4463
  }
4125
4464
 
4465
+ function moveSelectedElByDirection(direction) {
4466
+ if (!selectedEl || !selectedEl.parentElement) return;
4467
+ var p = selectedEl.parentElement;
4468
+ var sibling = direction < 0 ? selectedEl.previousElementSibling : selectedEl.nextElementSibling;
4469
+ if (!sibling) return;
4470
+ if (direction < 0) p.insertBefore(selectedEl, sibling);
4471
+ else p.insertBefore(sibling, selectedEl);
4472
+ recordReorderAfterDrag(selectedEl);
4473
+ saveCurrentVariationHtml();
4474
+ recomputeEditorDirty();
4475
+ scheduleDomTreeRefresh();
4476
+ updateSelectionToolbar();
4477
+ }
4478
+
4126
4479
  function attachDragReposition() {
4127
4480
  try {
4128
4481
  var iframe = document.getElementById('iframeId');
@@ -4309,7 +4662,9 @@ function syncIframeInteractions(reason) {
4309
4662
  scheduleConsistencyReconcile();
4310
4663
  bindSelectionToolbarScroll();
4311
4664
  var inp = document.getElementById('comp-search');
4312
- renderDomTree(inp ? inp.value : '');
4665
+ var q = inp ? inp.value : '';
4666
+ if (currentLeftTab === 'elements') renderElementsTree(q);
4667
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
4313
4668
  updateSelectionToolbar();
4314
4669
  recomputeEditorDirty();
4315
4670
  } catch(_) {}
@@ -4473,8 +4828,11 @@ function renderSidebar(filter) {
4473
4828
  }
4474
4829
 
4475
4830
  document.getElementById('comp-search').addEventListener('input', function() {
4476
- if (currentLeftTab === 'elements') renderDomTree(this.value);
4477
- else renderSidebar(this.value);
4831
+ if (currentLeftTab === 'elements') renderElementsTree(this.value);
4832
+ else if (currentLeftTab === 'dom-tree') renderDomTree(this.value);
4833
+ if (currentSectionComponentsTab === 'components' || currentSectionComponentsTab === 'sections') {
4834
+ renderSidebar(this.value);
4835
+ }
4478
4836
  });
4479
4837
 
4480
4838
  // \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
@@ -4540,19 +4898,11 @@ document.addEventListener('keydown', function(e) {
4540
4898
  var k = (e.key || '').toLowerCase();
4541
4899
  if (meta && !e.shiftKey && k === 'z') {
4542
4900
  e.preventDefault();
4543
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4544
- Vvveb.Undo.undo();
4545
- saveCurrentVariationHtml();
4546
- recomputeEditorDirty();
4547
- }
4901
+ runEditorUndo();
4548
4902
  }
4549
4903
  if (meta && e.shiftKey && k === 'z') {
4550
4904
  e.preventDefault();
4551
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4552
- Vvveb.Undo.redo();
4553
- saveCurrentVariationHtml();
4554
- recomputeEditorDirty();
4555
- }
4905
+ runEditorRedo();
4556
4906
  }
4557
4907
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
4558
4908
  if (e.key === 'Escape') {
@@ -4572,8 +4922,39 @@ document.addEventListener('keydown', function(e) {
4572
4922
  if (selectedEl) deselectElement();
4573
4923
  }
4574
4924
  });
4575
- document.getElementById('btn-undo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.undo(); });
4576
- document.getElementById('btn-redo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.redo(); });
4925
+ function runEditorUndo() {
4926
+ var target = getLatestHistoryUndoTarget();
4927
+ if (target) {
4928
+ removeHistoryItem(target.source, target.idx);
4929
+ return;
4930
+ }
4931
+ if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
4932
+ Vvveb.Undo.undo();
4933
+ saveCurrentVariationHtml();
4934
+ recomputeEditorDirty();
4935
+ scheduleDomTreeRefresh();
4936
+ updateSelectionToolbar();
4937
+ }
4938
+
4939
+ function runEditorRedo() {
4940
+ if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
4941
+ Vvveb.Undo.redo();
4942
+ saveCurrentVariationHtml();
4943
+ recomputeEditorDirty();
4944
+ scheduleDomTreeRefresh();
4945
+ updateSelectionToolbar();
4946
+ }
4947
+
4948
+ document.getElementById('btn-undo').addEventListener('click', function(e) {
4949
+ e.preventDefault();
4950
+ e.stopPropagation();
4951
+ runEditorUndo();
4952
+ });
4953
+ document.getElementById('btn-redo').addEventListener('click', function(e) {
4954
+ e.preventDefault();
4955
+ e.stopPropagation();
4956
+ runEditorRedo();
4957
+ });
4577
4958
 
4578
4959
  function layoutLoadingTooltip(host) {
4579
4960
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -4649,8 +5030,8 @@ function registerCROSections() {
4649
5030
 
4650
5031
  window.addEventListener('load', function() {
4651
5032
  registerCROSections();
4652
- renderSidebar();
4653
- renderDomTree(document.getElementById('comp-search').value);
5033
+ switchSectionComponentsTab(currentSectionComponentsTab);
5034
+ renderElementsTree(document.getElementById('comp-search').value);
4654
5035
  vvvebReady = true;
4655
5036
  bindLoadingTooltipPositioning();
4656
5037
 
@@ -4703,12 +5084,22 @@ window.addEventListener('load', function() {
4703
5084
  syncIframeInteractions('iframe-load');
4704
5085
  });
4705
5086
 
4706
- document.getElementById('sf-drag').addEventListener('click', function(e) {
4707
- e.preventDefault();
4708
- e.stopPropagation();
4709
- if (!selectedEl) return;
4710
- setDragHandleActive(!dragHandleActive);
4711
- });
5087
+ var sfMoveUp = document.getElementById('sf-move-up');
5088
+ if (sfMoveUp) {
5089
+ sfMoveUp.addEventListener('click', function(e) {
5090
+ e.preventDefault();
5091
+ e.stopPropagation();
5092
+ moveSelectedElByDirection(-1);
5093
+ });
5094
+ }
5095
+ var sfMoveDown = document.getElementById('sf-move-down');
5096
+ if (sfMoveDown) {
5097
+ sfMoveDown.addEventListener('click', function(e) {
5098
+ e.preventDefault();
5099
+ e.stopPropagation();
5100
+ moveSelectedElByDirection(1);
5101
+ });
5102
+ }
4712
5103
  document.getElementById('sf-dup').addEventListener('click', function(e) {
4713
5104
  e.preventDefault();
4714
5105
  e.stopPropagation();
@@ -4754,6 +5145,7 @@ var getDefaultAnthropicApiKey = () => {
4754
5145
  function createVisualEditorMiddleware(options) {
4755
5146
  const anthropicApiKey = options?.anthropicApiKey || getDefaultAnthropicApiKey();
4756
5147
  const enableGenerateTestApi = options?.enableGenerateTestApi ?? true;
5148
+ const strictObserverFreeze = options?.strictObserverFreeze === true;
4757
5149
  const allowedFrameOrigins = options?.allowedFrameOrigins ?? ["*"];
4758
5150
  function setFrameHeaders(req, res) {
4759
5151
  res.removeHeader("X-Frame-Options");
@@ -4921,6 +5313,8 @@ function createVisualEditorMiddleware(options) {
4921
5313
  const url = new URL(req.url || "", "http://localhost");
4922
5314
  const targetUrl = url.searchParams.get("url");
4923
5315
  const password = url.searchParams.get("password") || "";
5316
+ const strictFreezeParam = (url.searchParams.get("strictObserverFreeze") || "").toLowerCase();
5317
+ const strictObserverFreezeForRequest = strictFreezeParam === "1" || strictFreezeParam === "true" || strictFreezeParam === "yes" ? true : strictFreezeParam === "0" || strictFreezeParam === "false" || strictFreezeParam === "no" ? false : strictObserverFreeze;
4924
5318
  if (!targetUrl) {
4925
5319
  res.statusCode = 400;
4926
5320
  res.end(JSON.stringify({ error: "Missing url parameter" }));
@@ -5058,6 +5452,7 @@ ${iframeAlwaysShowCssGuardScript}
5058
5452
  var TARGET_ORIGIN=${JSON.stringify(origin)};
5059
5453
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
5060
5454
  var PROXY_PASSWORD=${JSON.stringify(password)};
5455
+ var STRICT_OBSERVER_FREEZE=${JSON.stringify(strictObserverFreezeForRequest)};
5061
5456
  window.__CONVERSION_EDITOR_ACTIVE__=true;
5062
5457
  function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
5063
5458
  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;}}
@@ -5077,6 +5472,9 @@ try{
5077
5472
  var wrapped=function(list,obs){
5078
5473
  try{
5079
5474
  if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
5475
+ if(STRICT_OBSERVER_FREEZE){
5476
+ return;
5477
+ }
5080
5478
  var now=Date.now();
5081
5479
  if(now-last<120)return;
5082
5480
  last=now;
@@ -5086,6 +5484,11 @@ try{
5086
5484
  return new NativeMO(wrapped);
5087
5485
  };
5088
5486
  window.MutationObserver.prototype=NativeMO.prototype;
5487
+ try{
5488
+ if(STRICT_OBSERVER_FREEZE){
5489
+ console.info("[conversion-proxy] strict MutationObserver freeze active");
5490
+ }
5491
+ }catch(_){}
5089
5492
  }
5090
5493
  }catch(_){}
5091
5494
  }catch(_){}})();</script>`;