@accelerated-agency/visual-editor 0.3.4 → 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.
Files changed (3) hide show
  1. package/dist/vite.cjs +559 -169
  2. package/dist/vite.js +559 -169
  3. package/package.json +1 -1
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:';
@@ -2331,7 +2539,7 @@ function runConsistencyReconcile() {
2331
2539
  var doc = iframe && iframe.contentDocument;
2332
2540
  if (!doc || !doc.body) return;
2333
2541
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2334
- var cs = parseVariationChangesets(variation);
2542
+ var cs = buildPersistedChainSetsForVariation(variation);
2335
2543
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2336
2544
  var granular = filterGranularChangesetEntries(cs);
2337
2545
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
@@ -2591,6 +2799,7 @@ function mergeGranularChainSets(baseList, overlayList) {
2591
2799
  function appendSessionStructuralChainRow(varId, row) {
2592
2800
  if (!varId || !row) return;
2593
2801
  if (!sessionStructuralChainRowsByVarId[varId]) sessionStructuralChainRowsByVarId[varId] = [];
2802
+ if (row.vveTs == null) row.vveTs = nextHistoryTimestamp();
2594
2803
  sessionStructuralChainRowsByVarId[varId].push(row);
2595
2804
  }
2596
2805
 
@@ -2598,33 +2807,33 @@ function appendSessionStructuralChainRow(varId, row) {
2598
2807
  function stateChangeToChainSet(c) {
2599
2808
  if (!c || !c.selector) return null;
2600
2809
  if (c.cssProp) {
2601
- 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() };
2602
2811
  }
2603
2812
  switch (c.inputId) {
2604
2813
  case 'pp-text':
2605
- return { selector: c.selector, type: 'content', value: c.value };
2814
+ return { selector: c.selector, type: 'content', value: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2606
2815
  case 'pp-html':
2607
- return { selector: c.selector, type: 'content', html: c.value };
2816
+ return { selector: c.selector, type: 'content', html: c.value, vveTs: c.vveTs || nextHistoryTimestamp() };
2608
2817
  case 'pp-cls':
2609
- 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() };
2610
2819
  case 'pp-id':
2611
- 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() };
2612
2821
  case 'pp-href':
2613
- 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() };
2614
2823
  case 'pp-target':
2615
- 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() };
2616
2825
  case 'pp-src':
2617
- 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() };
2618
2827
  case 'pp-alt':
2619
- 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() };
2620
2829
  case 'pp-ph':
2621
- 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() };
2622
2831
  case 'pp-css':
2623
- 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() };
2624
2833
  case 'pp-mob-css':
2625
- 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() };
2626
2835
  case 'pp-tab-css':
2627
- 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() };
2628
2837
  default:
2629
2838
  return null;
2630
2839
  }
@@ -2912,7 +3121,7 @@ function applyActiveVariationHtml() {
2912
3121
  if (!iframeDoc || !iframeDoc.body) return;
2913
3122
 
2914
3123
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2915
- var cs = parseVariationChangesets(variation);
3124
+ var cs = buildPersistedChainSetsForVariation(variation);
2916
3125
  refreshPersistentChangesetStyleTagForActiveVariation();
2917
3126
 
2918
3127
  beginSuppressIframeMutationDirty();
@@ -2970,7 +3179,7 @@ function applyVariationGranularOnly(iframeDoc) {
2970
3179
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2971
3180
  if (varHtmlCache[activeVarId]) return;
2972
3181
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2973
- var cs = parseVariationChangesets(variation);
3182
+ var cs = buildPersistedChainSetsForVariation(variation);
2974
3183
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2975
3184
  beginSuppressIframeMutationDirty();
2976
3185
  try {
@@ -2989,7 +3198,7 @@ function reapplyActiveVariationGranular(iframeDoc) {
2989
3198
  if (!activeVarId || !iframeDoc || !iframeDoc.body) return;
2990
3199
  if (varHtmlCache[activeVarId]) return;
2991
3200
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2992
- var cs = parseVariationChangesets(variation);
3201
+ var cs = buildPersistedChainSetsForVariation(variation);
2993
3202
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2994
3203
  beginSuppressIframeMutationDirty();
2995
3204
  try {
@@ -3033,7 +3242,7 @@ function startIframeContentApplyWatcher(navGen, prevDocRef) {
3033
3242
 
3034
3243
  if (doc.readyState === 'loading') {
3035
3244
  var variation = variations.find(function(v) { return v._id === activeVarId; });
3036
- var cs0 = parseVariationChangesets(variation);
3245
+ var cs0 = buildPersistedChainSetsForVariation(variation);
3037
3246
  if (!cs0.length || changesetsHaveBodySnapshot(cs0)) return;
3038
3247
  var granular = filterGranularChangesetEntries(cs0);
3039
3248
  if (!granular.length) return;
@@ -3079,8 +3288,9 @@ function selectElement(el) {
3079
3288
  document.getElementById('no-sel').style.display = 'none';
3080
3289
  renderRightPanel(el);
3081
3290
  updateSelectionToolbar();
3082
- if (currentLeftTab === 'elements') {
3083
- 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);
3084
3294
  if (dr && dr.querySelector('.dt-row')) syncDomTreeSelection();
3085
3295
  else scheduleDomTreeRefresh();
3086
3296
  }
@@ -3343,27 +3553,32 @@ function deleteSelectedEl() {
3343
3553
  }
3344
3554
 
3345
3555
  function syncDomTreeSelection() {
3346
- var root = document.getElementById('dom-tree-root');
3347
- if (!root) return;
3348
- var rows = root.querySelectorAll('.dt-row');
3349
- for (var i = 0; i < rows.length; i++) {
3350
- rows[i].classList.toggle('dt-selected', !!(selectedEl && rows[i]._dtEl === selectedEl));
3351
- }
3352
- if (!selectedEl) return;
3353
- var found = null;
3354
- for (var j = 0; j < rows.length; j++) {
3355
- 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' });
3356
3570
  }
3357
- if (found) found.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3358
3571
  }
3359
3572
 
3360
3573
  function scheduleDomTreeRefresh() {
3361
- if (currentLeftTab !== 'elements') return;
3574
+ if (currentLeftTab !== 'elements' && currentLeftTab !== 'dom-tree') return;
3362
3575
  if (domTreeRefreshTimer) clearTimeout(domTreeRefreshTimer);
3363
3576
  domTreeRefreshTimer = setTimeout(function() {
3364
3577
  domTreeRefreshTimer = null;
3365
3578
  var inp = document.getElementById('comp-search');
3366
- renderDomTree(inp ? inp.value : '');
3579
+ var q = inp ? inp.value : '';
3580
+ if (currentLeftTab === 'elements') renderElementsTree(q);
3581
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
3367
3582
  }, 150);
3368
3583
  }
3369
3584
 
@@ -3386,6 +3601,129 @@ function domTreePathSegment(el) {
3386
3601
  return tag + '[' + idx + ']';
3387
3602
  }
3388
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
+
3389
3727
  function renderDomTree(filterRaw) {
3390
3728
  var filterText = (filterRaw || '').toLowerCase().trim();
3391
3729
  var root = document.getElementById('dom-tree-root');
@@ -4124,6 +4462,20 @@ function recordReorderAfterDrag(movedEl) {
4124
4462
  }
4125
4463
  }
4126
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
+
4127
4479
  function attachDragReposition() {
4128
4480
  try {
4129
4481
  var iframe = document.getElementById('iframeId');
@@ -4310,7 +4662,9 @@ function syncIframeInteractions(reason) {
4310
4662
  scheduleConsistencyReconcile();
4311
4663
  bindSelectionToolbarScroll();
4312
4664
  var inp = document.getElementById('comp-search');
4313
- renderDomTree(inp ? inp.value : '');
4665
+ var q = inp ? inp.value : '';
4666
+ if (currentLeftTab === 'elements') renderElementsTree(q);
4667
+ else if (currentLeftTab === 'dom-tree') renderDomTree(q);
4314
4668
  updateSelectionToolbar();
4315
4669
  recomputeEditorDirty();
4316
4670
  } catch(_) {}
@@ -4474,8 +4828,11 @@ function renderSidebar(filter) {
4474
4828
  }
4475
4829
 
4476
4830
  document.getElementById('comp-search').addEventListener('input', function() {
4477
- if (currentLeftTab === 'elements') renderDomTree(this.value);
4478
- 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
+ }
4479
4836
  });
4480
4837
 
4481
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
@@ -4541,19 +4898,11 @@ document.addEventListener('keydown', function(e) {
4541
4898
  var k = (e.key || '').toLowerCase();
4542
4899
  if (meta && !e.shiftKey && k === 'z') {
4543
4900
  e.preventDefault();
4544
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4545
- Vvveb.Undo.undo();
4546
- saveCurrentVariationHtml();
4547
- recomputeEditorDirty();
4548
- }
4901
+ runEditorUndo();
4549
4902
  }
4550
4903
  if (meta && e.shiftKey && k === 'z') {
4551
4904
  e.preventDefault();
4552
- if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
4553
- Vvveb.Undo.redo();
4554
- saveCurrentVariationHtml();
4555
- recomputeEditorDirty();
4556
- }
4905
+ runEditorRedo();
4557
4906
  }
4558
4907
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
4559
4908
  if (e.key === 'Escape') {
@@ -4573,8 +4922,39 @@ document.addEventListener('keydown', function(e) {
4573
4922
  if (selectedEl) deselectElement();
4574
4923
  }
4575
4924
  });
4576
- document.getElementById('btn-undo').addEventListener('click', function() { if (typeof Vvveb !== 'undefined' && Vvveb.Undo) Vvveb.Undo.undo(); });
4577
- 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
+ });
4578
4958
 
4579
4959
  function layoutLoadingTooltip(host) {
4580
4960
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -4650,8 +5030,8 @@ function registerCROSections() {
4650
5030
 
4651
5031
  window.addEventListener('load', function() {
4652
5032
  registerCROSections();
4653
- renderSidebar();
4654
- renderDomTree(document.getElementById('comp-search').value);
5033
+ switchSectionComponentsTab(currentSectionComponentsTab);
5034
+ renderElementsTree(document.getElementById('comp-search').value);
4655
5035
  vvvebReady = true;
4656
5036
  bindLoadingTooltipPositioning();
4657
5037
 
@@ -4704,12 +5084,22 @@ window.addEventListener('load', function() {
4704
5084
  syncIframeInteractions('iframe-load');
4705
5085
  });
4706
5086
 
4707
- document.getElementById('sf-drag').addEventListener('click', function(e) {
4708
- e.preventDefault();
4709
- e.stopPropagation();
4710
- if (!selectedEl) return;
4711
- setDragHandleActive(!dragHandleActive);
4712
- });
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
+ }
4713
5103
  document.getElementById('sf-dup').addEventListener('click', function(e) {
4714
5104
  e.preventDefault();
4715
5105
  e.stopPropagation();