@accelerated-agency/visual-editor 0.3.1 → 0.3.2

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/index.js CHANGED
@@ -4647,6 +4647,7 @@ function PlatformVisualEditor({
4647
4647
  }, [dirty, onDirtyChange]);
4648
4648
  const loadPayload = useMemo(
4649
4649
  () => ({
4650
+ iid: experiment?.iid,
4650
4651
  experimentId: experiment?.experimentId,
4651
4652
  name: experiment?.name,
4652
4653
  status: experiment?.status,
@@ -4656,6 +4657,7 @@ function PlatformVisualEditor({
4656
4657
  }),
4657
4658
  [experiment]
4658
4659
  );
4660
+ console.log("loadPayload", loadPayload);
4659
4661
  useEffect(() => {
4660
4662
  if (!editorReady) return;
4661
4663
  const payloadKey = JSON.stringify(loadPayload);
@@ -4859,6 +4861,7 @@ function PlatformVisualEditorV2({
4859
4861
  }, []);
4860
4862
  const loadPayload = useMemo(
4861
4863
  () => ({
4864
+ iid: experiment?.iid,
4862
4865
  experimentId: experiment?.experimentId,
4863
4866
  name: experiment?.name,
4864
4867
  status: experiment?.status,
@@ -4868,6 +4871,7 @@ function PlatformVisualEditorV2({
4868
4871
  }),
4869
4872
  [experiment]
4870
4873
  );
4874
+ console.log("loadPayload", loadPayload);
4871
4875
  const editorSrc = useMemo(() => {
4872
4876
  const safeBaseUrl = normalizeProxyBaseUrl(proxyBaseUrl);
4873
4877
  return safeBaseUrl ? `${safeBaseUrl}/vvveb-editor` : "/vvveb-editor";
package/dist/vite.cjs CHANGED
@@ -148,6 +148,17 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
148
148
  .tb-save-txt{font-size:14px;color:#00C951;white-space:nowrap}
149
149
  .tb-save-txt::before{content:'Saved'}
150
150
  #dirty-dot.on~.tb-save-txt::before{content:'Unsaved'}
151
+
152
+ /* \u2500\u2500 In-editor toast notification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
153
+ #ve-notification{
154
+ position:fixed;right:16px;top:64px;z-index:13000;max-width:360px;
155
+ padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.35;
156
+ border:1px solid var(--border);background:#fff;color:var(--text);
157
+ box-shadow:0 10px 24px rgba(2,6,23,.18);display:none
158
+ }
159
+ #ve-notification.show{display:block}
160
+ #ve-notification.error{border-color:#fecaca;background:#fef2f2;color:#991b1b}
161
+ #ve-notification.success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}
151
162
  .tb-save-time{font-size:12px;color:#52525b;white-space:nowrap}
152
163
  #dirty-dot.on~.tb-save-time{display:none}
153
164
  /* Simulate + Finalize buttons */
@@ -455,6 +466,49 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
455
466
  /* \u2500\u2500 Subgroup label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
456
467
  .sub-lbl{font-size:10px;text-transform:uppercase;color:var(--text-3);font-weight:700;letter-spacing:.05em;margin:8px 0 5px}
457
468
  .sub-lbl:first-child{margin-top:0}
469
+ .sub-lbl-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:8px 0 5px}
470
+ .sub-lbl-row .sub-lbl{margin:0}
471
+ .css-expand-btn{
472
+ width:22px;height:22px;border:1px solid var(--border);border-radius:5px;background:#fff;
473
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text-3);
474
+ transition:all .15s
475
+ }
476
+ .css-expand-btn:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
477
+
478
+ /* \u2500\u2500 Custom CSS modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
479
+ #custom-css-modal{
480
+ position:fixed;inset:0;z-index:12000;background:rgba(15,23,42,.5);
481
+ display:none;align-items:center;justify-content:center;padding:20px
482
+ }
483
+ #custom-css-modal.open{display:flex}
484
+ .custom-css-dialog{
485
+ width:min(860px,96vw);max-height:86vh;background:#fff;border:1px solid var(--border);
486
+ border-radius:10px;box-shadow:0 20px 40px rgba(2,6,23,.35);display:flex;flex-direction:column;overflow:hidden
487
+ }
488
+ .custom-css-head{
489
+ display:flex;align-items:center;justify-content:space-between;gap:10px;
490
+ padding:10px 12px;border-bottom:1px solid var(--border);background:#fff
491
+ }
492
+ .custom-css-title{font-size:12px;font-weight:700;color:var(--text)}
493
+ .custom-css-close{
494
+ border:none;background:transparent;color:var(--text-3);cursor:pointer;width:24px;height:24px;border-radius:5px
495
+ }
496
+ .custom-css-close:hover{background:var(--bg-hover);color:var(--text)}
497
+ #custom-css-modal-textarea{
498
+ width:100%;min-height:360px;max-height:58vh;resize:vertical;border:none;outline:none;
499
+ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
500
+ font-size:12px;line-height:1.5;padding:12px;background:#0b1220;color:#e2e8f0
501
+ }
502
+ .custom-css-actions{
503
+ display:flex;justify-content:flex-end;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:#fff
504
+ }
505
+ .custom-css-btn{
506
+ border:1px solid var(--border);background:#fff;color:var(--text-2);border-radius:6px;padding:6px 10px;cursor:pointer;
507
+ font-size:12px;font-weight:600;font-family:inherit
508
+ }
509
+ .custom-css-btn:hover{background:var(--bg-hover)}
510
+ .custom-css-btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
511
+ .custom-css-btn.primary:hover{filter:brightness(.97)}
458
512
 
459
513
  /* \u2500\u2500 Shadow presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
460
514
  .shadow-presets{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
@@ -844,6 +898,23 @@ select.pr-inp{cursor:pointer;background:#fff}
844
898
 
845
899
  </div><!-- #app -->
846
900
 
901
+ <!-- Custom CSS full-screen modal -->
902
+ <div id="custom-css-modal" aria-hidden="true">
903
+ <div class="custom-css-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-css-modal-title">
904
+ <div class="custom-css-head">
905
+ <div class="custom-css-title" id="custom-css-modal-title"><i class="bi bi-code-slash"></i> Custom CSS</div>
906
+ <button type="button" class="custom-css-close" onclick="closeCustomCssModal()" title="Close">
907
+ <i class="bi bi-x-lg"></i>
908
+ </button>
909
+ </div>
910
+ <textarea id="custom-css-modal-textarea" spellcheck="false" placeholder="Type your css here"></textarea>
911
+ <div class="custom-css-actions">
912
+ <button type="button" class="custom-css-btn" onclick="closeCustomCssModal()">Cancel</button>
913
+ <button type="button" class="custom-css-btn primary" onclick="applyCustomCssModal()">Add</button>
914
+ </div>
915
+ </div>
916
+ </div>
917
+
847
918
  <!-- CDN scripts -->
848
919
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
849
920
  <script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/builder.js"></script>
@@ -915,13 +986,37 @@ function send(type, payload) {
915
986
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
916
987
  }
917
988
 
989
+ var notificationTimer = null;
990
+ function showEditorNotification(message, kind, durationMs) {
991
+ var id = 've-notification';
992
+ var el = document.getElementById(id);
993
+ if (!el) {
994
+ el = document.createElement('div');
995
+ el.id = id;
996
+ el.setAttribute('role', 'status');
997
+ el.setAttribute('aria-live', 'polite');
998
+ document.body.appendChild(el);
999
+ }
1000
+ el.className = '';
1001
+ el.classList.add(kind === 'success' ? 'success' : 'error');
1002
+ el.classList.add('show');
1003
+ el.textContent = String(message || 'Something went wrong');
1004
+ if (notificationTimer) clearTimeout(notificationTimer);
1005
+ notificationTimer = setTimeout(function() {
1006
+ var cur = document.getElementById(id);
1007
+ if (!cur) return;
1008
+ cur.classList.remove('show');
1009
+ }, Math.max(1200, durationMs || 2600));
1010
+ }
1011
+
918
1012
  function generatePreviewUrlString(args) {
919
1013
  var baseUrl = (args && args.url) || '';
920
1014
  var test = (args && args.test) || {};
921
1015
  var variation = (args && args.variation) || {};
922
1016
  if (!baseUrl) return '';
923
- var testId = test.iid || test.experimentId || test._id || '';
924
- var variationId = variation.iid || variation._id || '';
1017
+ var testId = test.iid || '';
1018
+ var variationId = variation.iid || '';
1019
+ if (!testId || !variationId) return '';
925
1020
  var cId = String(testId || '') + '_' + String(variationId || '');
926
1021
  var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
927
1022
  return (
@@ -945,6 +1040,10 @@ function simulateExperiment() {
945
1040
  test.pageUrl ||
946
1041
  (test.metadata_1 && test.metadata_1.editor_url) ||
947
1042
  '';
1043
+ if (!test.iid || !activeVariation || !activeVariation.iid) {
1044
+ showEditorNotification('Cannot simulate: missing test.iid or variation.iid.', 'error', 3200);
1045
+ return;
1046
+ }
948
1047
  var url = generatePreviewUrlString({
949
1048
  url: targetUrl,
950
1049
  test: test,
@@ -952,6 +1051,7 @@ function simulateExperiment() {
952
1051
  });
953
1052
  if (!url) {
954
1053
  console.warn('[V2] simulateExperiment: missing target URL');
1054
+ showEditorNotification('Cannot simulate: missing target URL.', 'error', 3200);
955
1055
  return;
956
1056
  }
957
1057
  try {
@@ -969,6 +1069,19 @@ window.addEventListener('message', function(e) {
969
1069
  }
970
1070
  });
971
1071
 
1072
+ document.addEventListener('keydown', function(e) {
1073
+ if (e.key !== 'Escape') return;
1074
+ var modal = document.getElementById('custom-css-modal');
1075
+ if (!modal || !modal.classList.contains('open')) return;
1076
+ closeCustomCssModal();
1077
+ });
1078
+
1079
+ document.addEventListener('click', function(e) {
1080
+ var modal = document.getElementById('custom-css-modal');
1081
+ if (!modal || !modal.classList.contains('open')) return;
1082
+ if (e.target === modal) closeCustomCssModal();
1083
+ });
1084
+
972
1085
  // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
973
1086
  var experimentData = null;
974
1087
  var variations = [];
@@ -1048,14 +1161,17 @@ function endSuppressIframeMutationDirty() {
1048
1161
  function commitStateChangesForActiveVariation() {
1049
1162
  if (!activeVarId) return;
1050
1163
  stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1164
+ refreshPersistentChangesetStyleTagForActiveVariation();
1051
1165
  }
1052
1166
 
1053
1167
  function loadStateChangesForActiveVariation() {
1054
1168
  if (!activeVarId) {
1055
1169
  stateChanges = [];
1170
+ refreshPersistentChangesetStyleTagForActiveVariation();
1056
1171
  return;
1057
1172
  }
1058
1173
  stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1174
+ refreshPersistentChangesetStyleTagForActiveVariation();
1059
1175
  }
1060
1176
 
1061
1177
  function recoverSelectedElement(forceDeselectOnMiss) {
@@ -1535,24 +1651,26 @@ function persistActiveVariationChangesets(arr) {
1535
1651
  }
1536
1652
  }
1537
1653
  }
1654
+ refreshPersistentChangesetStyleTagForActiveVariation();
1538
1655
  }
1539
1656
 
1540
1657
  function entrySnapshotKey(entry) {
1541
1658
  if (!entry || !entry.selector) return '';
1659
+ var SEP = '__vve_sep__';
1542
1660
  var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
1543
1661
  return (
1544
1662
  selKey +
1545
- '\0' +
1663
+ SEP +
1546
1664
  normalizeChangesetType(entry) +
1547
- '\0' +
1665
+ SEP +
1548
1666
  String(entry.property || '') +
1549
- '\0' +
1667
+ SEP +
1550
1668
  String(entry.attribute || '') +
1551
- '\0' +
1669
+ SEP +
1552
1670
  String(entry.action || '') +
1553
- '\0' +
1671
+ SEP +
1554
1672
  String(entry.html != null ? 'h' + String(entry.html).length : '') +
1555
- '\0' +
1673
+ SEP +
1556
1674
  String(entry.value != null ? 'v:' + entry.value : '')
1557
1675
  );
1558
1676
  }
@@ -1602,7 +1720,6 @@ function softReloadEditorIframe() {
1602
1720
  var navGen = nextIframeContentNavGen();
1603
1721
  resetIframeBindings();
1604
1722
  setIframePageLoadingUi(true);
1605
- iframe.src = '';
1606
1723
  iframe.src = appendIframeReloadBust(src);
1607
1724
  startIframeContentApplyWatcher(navGen);
1608
1725
  scheduleDomTreeRefresh();
@@ -1611,6 +1728,7 @@ function softReloadEditorIframe() {
1611
1728
  /** @returns {boolean} true if a full iframe reload was started */
1612
1729
  function revertChangesetEntryOnDom(entry) {
1613
1730
  if (!entry) return false;
1731
+ var styleOnly = isStyleOnlyChangesetEntry(entry);
1614
1732
  if (entry.selector === '__vvveb_body__') {
1615
1733
  var iframeDoc0 = document.getElementById('iframeId').contentDocument;
1616
1734
  if (!iframeDoc0 || !iframeDoc0.body) return false;
@@ -1631,6 +1749,11 @@ function revertChangesetEntryOnDom(entry) {
1631
1749
  var snap = appliedChangesetSnapshots[k];
1632
1750
  var el = querySelectorResolved(iframeDoc, entry.selector);
1633
1751
  if (!snap || !el) {
1752
+ if (styleOnly) {
1753
+ refreshPersistentChangesetStyleTagForActiveVariation();
1754
+ delete appliedChangesetSnapshots[k];
1755
+ return false;
1756
+ }
1634
1757
  softReloadEditorIframe();
1635
1758
  delete appliedChangesetSnapshots[k];
1636
1759
  return true;
@@ -1645,6 +1768,11 @@ function revertChangesetEntryOnDom(entry) {
1645
1768
  else el.setAttribute(snap.name, snap.v);
1646
1769
  } else if (snap.kind === 'display') el.style.display = snap.v;
1647
1770
  else {
1771
+ if (styleOnly) {
1772
+ refreshPersistentChangesetStyleTagForActiveVariation();
1773
+ delete appliedChangesetSnapshots[k];
1774
+ return false;
1775
+ }
1648
1776
  softReloadEditorIframe();
1649
1777
  delete appliedChangesetSnapshots[k];
1650
1778
  return true;
@@ -1704,7 +1832,9 @@ function renderHistoryTab() {
1704
1832
  var lab = historyEntryTypeLabel(item.entry);
1705
1833
  var val = historyEntryValuePreview(item.entry);
1706
1834
  html +=
1707
- '<div class="state-item">' +
1835
+ '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1836
+ item.idx +
1837
+ ')">' +
1708
1838
  '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1709
1839
  item.idx +
1710
1840
  '</span>' +
@@ -1720,7 +1850,7 @@ function renderHistoryTab() {
1720
1850
  item.idx +
1721
1851
  ')" onclick="removeHistoryChangeset(' +
1722
1852
  item.idx +
1723
- ')">&#x2715;</button>' +
1853
+ ', event)">&#x2715;</button>' +
1724
1854
  '</div>';
1725
1855
  });
1726
1856
  html += '</div>';
@@ -1738,7 +1868,38 @@ function changesetListHasStructural(arr) {
1738
1868
  return false;
1739
1869
  }
1740
1870
 
1741
- function removeHistoryChangeset(idx) {
1871
+ function isStyleOnlyChangesetEntry(entry) {
1872
+ if (!entry) return false;
1873
+ var t = normalizeChangesetType(entry);
1874
+ if (t === 'style') return true;
1875
+ if (t === 'attribute' && String(entry.attribute || '').toLowerCase() === 'style') return true;
1876
+ return false;
1877
+ }
1878
+
1879
+ function focusHistoryChangeset(idx) {
1880
+ var v = getActiveVariationForHistory();
1881
+ if (!v) return;
1882
+ var arr = parseVariationChangesets(v);
1883
+ if (idx < 0 || idx >= arr.length) return;
1884
+ var entry = arr[idx];
1885
+ if (!entry || !entry.selector || entry.selector === '__vvveb_body__') return;
1886
+ try {
1887
+ var iframe = document.getElementById('iframeId');
1888
+ var iframeDoc = iframe && iframe.contentDocument;
1889
+ if (!iframeDoc) return;
1890
+ var el = querySelectorResolved(iframeDoc, entry.selector);
1891
+ if (!el) return;
1892
+ selectElement(el);
1893
+ try {
1894
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
1895
+ } catch(_) {
1896
+ el.scrollIntoView();
1897
+ }
1898
+ } catch(_) {}
1899
+ }
1900
+
1901
+ function removeHistoryChangeset(idx, evt) {
1902
+ if (evt && evt.stopPropagation) evt.stopPropagation();
1742
1903
  var v = getActiveVariationForHistory();
1743
1904
  if (!v) return;
1744
1905
  var arr = parseVariationChangesets(v);
@@ -1746,6 +1907,16 @@ function removeHistoryChangeset(idx) {
1746
1907
  var removed = arr[idx];
1747
1908
  arr.splice(idx, 1);
1748
1909
  persistActiveVariationChangesets(arr);
1910
+ if (isStyleOnlyChangesetEntry(removed)) {
1911
+ try {
1912
+ refreshPersistentChangesetStyleTagForActiveVariation();
1913
+ saveCurrentVariationHtml();
1914
+ } catch(_) {}
1915
+ if (currentMainTab === 'history') renderHistoryTab();
1916
+ recomputeEditorDirty();
1917
+ scheduleDomTreeRefresh();
1918
+ return;
1919
+ }
1749
1920
  var didReload = revertChangesetEntryOnDom(removed);
1750
1921
  try {
1751
1922
  delete varHtmlCache[activeVarId];
@@ -1753,15 +1924,19 @@ function removeHistoryChangeset(idx) {
1753
1924
  // Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
1754
1925
  // structural row remains or was removed (revert may already have started a reload for insert/body).
1755
1926
  var removedType = normalizeChangesetType(removed);
1756
- var needsStructuralReload =
1757
- !didReload &&
1758
- (removedType === 'insert' ||
1759
- removedType === 'reorder' ||
1760
- changesetListHasStructural(arr));
1927
+ var hasStructuralRemaining = changesetListHasStructural(arr);
1928
+ var removedIsStructural = removedType === 'insert' || removedType === 'reorder';
1761
1929
  if (didReload) {
1762
1930
  /* revertChangesetEntryOnDom already kicked off iframe reload */
1763
- } else if (needsStructuralReload) {
1931
+ } else if (removedIsStructural) {
1764
1932
  softReloadEditorIframe();
1933
+ } else if (hasStructuralRemaining) {
1934
+ // Keep current DOM state (already reverted for removed row) and only refresh style layer.
1935
+ // Avoid full reload and avoid re-applying all rows, which can duplicate structural insert/reorder entries.
1936
+ try {
1937
+ refreshPersistentChangesetStyleTagForActiveVariation();
1938
+ saveCurrentVariationHtml();
1939
+ } catch(_) {}
1765
1940
  } else {
1766
1941
  try {
1767
1942
  appliedStructuralChangesetKeys = {};
@@ -2129,7 +2304,7 @@ function runConsistencyReconcile() {
2129
2304
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2130
2305
  var granular = filterGranularChangesetEntries(cs);
2131
2306
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
2132
- if (unresolved > 0 || changesetListHasStructural(cs)) {
2307
+ if (unresolved > 0 || hasUnappliedStructuralChangesets(cs)) {
2133
2308
  reapplyActiveVariationGranular(doc);
2134
2309
  registerPendingGranularChangesets(cs, doc);
2135
2310
  }
@@ -2499,24 +2674,136 @@ function flushPendingGranularChangesets() {
2499
2674
  function structuralChangesetDedupKey(entry) {
2500
2675
  var nt = normalizeChangesetType(entry);
2501
2676
  if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
2677
+ var SEP = '__vve_sep__';
2502
2678
  var vid = activeVarId || '';
2503
2679
  try {
2504
2680
  return (
2505
2681
  vid +
2506
- '\0' +
2682
+ SEP +
2507
2683
  nt +
2508
- '\0' +
2684
+ SEP +
2509
2685
  entry.selector +
2510
- '\0' +
2686
+ SEP +
2511
2687
  String(entry.action || '') +
2512
- '\0' +
2688
+ SEP +
2513
2689
  String(entry.html != null ? entry.html : '').slice(0, 240) +
2514
- '\0' +
2690
+ SEP +
2515
2691
  String(entry.targetSelector || '')
2516
2692
  );
2517
2693
  } catch(_) {
2518
- return vid + '\0' + nt + '\0' + entry.selector;
2694
+ return vid + SEP + nt + SEP + entry.selector;
2695
+ }
2696
+ }
2697
+
2698
+ function hasUnappliedStructuralChangesets(cs) {
2699
+ if (!cs || !cs.length) return false;
2700
+ for (var i = 0; i < cs.length; i++) {
2701
+ var e = cs[i];
2702
+ var t = normalizeChangesetType(e);
2703
+ if (t !== 'insert' && t !== 'reorder') continue;
2704
+ var k = structuralChangesetDedupKey(e);
2705
+ if (!k) return true;
2706
+ if (!appliedStructuralChangesetKeys[k]) return true;
2707
+ }
2708
+ return false;
2709
+ }
2710
+
2711
+ function parseInlineStyleDeclarations(styleText) {
2712
+ var out = [];
2713
+ if (styleText == null) return out;
2714
+ var s = String(styleText);
2715
+ if (!s.trim()) return out;
2716
+ var parts = s.split(';');
2717
+ for (var i = 0; i < parts.length; i++) {
2718
+ var seg = parts[i];
2719
+ if (!seg) continue;
2720
+ var idx = seg.indexOf(':');
2721
+ if (idx <= 0) continue;
2722
+ var prop = seg.slice(0, idx).trim();
2723
+ var value = seg.slice(idx + 1).trim();
2724
+ if (!prop || !value) continue;
2725
+ out.push({ prop: prop, value: value });
2519
2726
  }
2727
+ return out;
2728
+ }
2729
+
2730
+ function buildPersistentStyleRulesForActiveVariation() {
2731
+ if (!activeVarId) return '';
2732
+ var v = variations.find(function(x) { return x && x._id === activeVarId; });
2733
+ var parsed = parseVariationChangesets(v);
2734
+ var map = {};
2735
+ var order = [];
2736
+ function put(selector, prop, value) {
2737
+ if (!selector || !prop) return;
2738
+ if (value == null || value === '') return;
2739
+ var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
2740
+ var pr = String(prop).trim();
2741
+ var val = String(value).trim();
2742
+ if (!sel || !pr || !val) return;
2743
+ var k = sel + '__vve_sep__' + pr;
2744
+ if (!map[k]) order.push(k);
2745
+ map[k] = { selector: sel, property: pr, value: val };
2746
+ }
2747
+ for (var i = 0; i < parsed.length; i++) {
2748
+ var e = parsed[i];
2749
+ if (!e) continue;
2750
+ var t = normalizeChangesetType(e);
2751
+ if (t === 'style') {
2752
+ put(e.selector, e.property || e.cssProp, e.value);
2753
+ continue;
2754
+ }
2755
+ if (t === 'attribute' && String(e.attribute || '').toLowerCase() === 'style') {
2756
+ var decls = parseInlineStyleDeclarations(e.value);
2757
+ for (var di = 0; di < decls.length; di++) {
2758
+ put(e.selector, decls[di].prop, decls[di].value);
2759
+ }
2760
+ }
2761
+ }
2762
+ for (var j = 0; j < stateChanges.length; j++) {
2763
+ var c = stateChanges[j];
2764
+ if (!c) continue;
2765
+ if (c.cssProp) {
2766
+ put(c.selector, c.cssProp, c.value);
2767
+ continue;
2768
+ }
2769
+ if (c.inputId === 'pp-css') {
2770
+ var liveDecls = parseInlineStyleDeclarations(c.value);
2771
+ for (var ldi = 0; ldi < liveDecls.length; ldi++) {
2772
+ put(c.selector, liveDecls[ldi].prop, liveDecls[ldi].value);
2773
+ }
2774
+ }
2775
+ }
2776
+ var lines = [];
2777
+ for (var oi = 0; oi < order.length; oi++) {
2778
+ var row = map[order[oi]];
2779
+ lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
2780
+ }
2781
+ return lines.join('\\n');
2782
+ }
2783
+
2784
+ function upsertPersistentChangesetStyleTag(iframeDoc, rulesText) {
2785
+ if (!iframeDoc) return;
2786
+ var STYLE_ID = '__vve_persist_changesets_style__';
2787
+ var prev = iframeDoc.getElementById(STYLE_ID);
2788
+ if (!rulesText) {
2789
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
2790
+ return;
2791
+ }
2792
+ var head = iframeDoc.head || iframeDoc.getElementsByTagName('head')[0];
2793
+ if (!head) return;
2794
+ var styleEl = prev || iframeDoc.createElement('style');
2795
+ styleEl.id = STYLE_ID;
2796
+ if (styleEl.textContent !== rulesText) styleEl.textContent = rulesText;
2797
+ if (!styleEl.parentNode) head.appendChild(styleEl);
2798
+ }
2799
+
2800
+ function refreshPersistentChangesetStyleTagForActiveVariation() {
2801
+ try {
2802
+ var iframe = document.getElementById('iframeId');
2803
+ var iframeDoc = iframe && iframe.contentDocument;
2804
+ if (!iframeDoc) return;
2805
+ upsertPersistentChangesetStyleTag(iframeDoc, buildPersistentStyleRulesForActiveVariation());
2806
+ } catch(_) {}
2520
2807
  }
2521
2808
 
2522
2809
  /**
@@ -2553,23 +2840,11 @@ function applyChangesetEntry(entry, iframeDoc) {
2553
2840
  else if (entry.value != null) el.textContent = entry.value;
2554
2841
  break;
2555
2842
  case 'style':
2556
- if (entry.property) {
2557
- var propKebab = entry.property;
2558
- var cam = camelize(propKebab);
2559
- if (entry.value == null || entry.value === '') {
2560
- try { el.style.removeProperty(propKebab); } catch(_) {}
2561
- try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
2562
- } else {
2563
- try {
2564
- el.style.setProperty(propKebab, entry.value, 'important');
2565
- } catch(_) {
2566
- el.style[cam] = entry.value;
2567
- }
2568
- }
2569
- }
2843
+ // Style changes are applied via persistent stylesheet injection.
2570
2844
  break;
2571
2845
  case 'attribute':
2572
2846
  if (entry.attribute && entry.value != null) {
2847
+ if (String(entry.attribute).toLowerCase() === 'style') break;
2573
2848
  el.setAttribute(entry.attribute, entry.value);
2574
2849
  }
2575
2850
  break;
@@ -2606,6 +2881,7 @@ function applyActiveVariationHtml() {
2606
2881
 
2607
2882
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2608
2883
  var cs = parseVariationChangesets(variation);
2884
+ refreshPersistentChangesetStyleTagForActiveVariation();
2609
2885
 
2610
2886
  beginSuppressIframeMutationDirty();
2611
2887
  try {
@@ -2620,6 +2896,7 @@ function applyActiveVariationHtml() {
2620
2896
  for (var i = 0; i < cs.length; i++) {
2621
2897
  applyChangesetEntry(cs[i], iframeDoc);
2622
2898
  }
2899
+ refreshPersistentChangesetStyleTagForActiveVariation();
2623
2900
  // Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
2624
2901
  registerPendingGranularChangesets(cs, iframeDoc);
2625
2902
  } finally {
@@ -2668,6 +2945,7 @@ function applyVariationGranularOnly(iframeDoc) {
2668
2945
  for (var i = 0; i < cs.length; i++) {
2669
2946
  applyChangesetEntry(cs[i], iframeDoc);
2670
2947
  }
2948
+ refreshPersistentChangesetStyleTagForActiveVariation();
2671
2949
  registerPendingGranularChangesets(cs, iframeDoc);
2672
2950
  } finally {
2673
2951
  endSuppressIframeMutationDirty();
@@ -2686,6 +2964,7 @@ function reapplyActiveVariationGranular(iframeDoc) {
2686
2964
  for (var i = 0; i < cs.length; i++) {
2687
2965
  applyChangesetEntry(cs[i], iframeDoc);
2688
2966
  }
2967
+ refreshPersistentChangesetStyleTagForActiveVariation();
2689
2968
  } finally {
2690
2969
  endSuppressIframeMutationDirty();
2691
2970
  }
@@ -3174,6 +3453,40 @@ function pr2(l1, i1, l2, i2) {
3174
3453
  '<div class="pr2-item"><div class="pr2-lbl">'+l2+'</div>'+i2+'</div></div>';
3175
3454
  }
3176
3455
  function subLbl(text) { return '<div class="sub-lbl">'+text+'</div>'; }
3456
+ function openCustomCssModal() {
3457
+ var modal = document.getElementById('custom-css-modal');
3458
+ var ta = document.getElementById('custom-css-modal-textarea');
3459
+ if (!modal || !ta) return;
3460
+ var inp = document.getElementById('pp-css');
3461
+ var v = inp ? inp.value : (selectedEl && selectedEl.getAttribute ? (selectedEl.getAttribute('style') || '') : '');
3462
+ ta.value = v || '';
3463
+ modal.classList.add('open');
3464
+ modal.setAttribute('aria-hidden', 'false');
3465
+ setTimeout(function() {
3466
+ try { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } catch(_) {}
3467
+ }, 0);
3468
+ }
3469
+ function closeCustomCssModal() {
3470
+ var modal = document.getElementById('custom-css-modal');
3471
+ if (!modal) return;
3472
+ modal.classList.remove('open');
3473
+ modal.setAttribute('aria-hidden', 'true');
3474
+ }
3475
+ function applyCustomCssModal() {
3476
+ var ta = document.getElementById('custom-css-modal-textarea');
3477
+ var inp = document.getElementById('pp-css');
3478
+ if (!ta || !inp) {
3479
+ closeCustomCssModal();
3480
+ return;
3481
+ }
3482
+ inp.value = ta.value || '';
3483
+ try {
3484
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
3485
+ } catch(_) {
3486
+ if (typeof inp.oninput === 'function') inp.oninput();
3487
+ }
3488
+ closeCustomCssModal();
3489
+ }
3177
3490
  function weightOpts(cur) {
3178
3491
  return [['100','Thin'],['200','Extra Light'],['300','Light'],['400','Normal'],['500','Medium'],
3179
3492
  ['600','Semi Bold'],['700','Bold'],['800','Extra Bold'],['900','Black']]
@@ -3454,7 +3767,12 @@ function renderRightPanel(el) {
3454
3767
  // \u2500\u2500 CSS and Classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3455
3768
  document.getElementById('acc-body-css').innerHTML =
3456
3769
  pr('Classes', '<input class="pr-inp" id="pp-cls" type="text" value="'+esc(el.className||'')+'" placeholder="class1 class2">') +
3457
- subLbl('Custom CSS') +
3770
+ '<div class="sub-lbl-row">' +
3771
+ '<div class="sub-lbl">Custom CSS</div>' +
3772
+ '<button type="button" class="css-expand-btn" title="Open full-screen editor" onclick="openCustomCssModal()">' +
3773
+ '<i class="bi bi-fullscreen"></i>' +
3774
+ '</button>' +
3775
+ '</div>' +
3458
3776
  '<textarea class="pr-inp" id="pp-css" style="width:100%;min-height:80px;font-family:monospace;font-size:11px" placeholder="color: red; font-size: 16px;">'+esc(el.getAttribute('style')||'')+'</textarea>';
3459
3777
 
3460
3778
  // \u2500\u2500 Attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -3818,26 +4136,6 @@ function attachChangeObserver() {
3818
4136
  }
3819
4137
  changeObserver = new MutationObserver(function(mutations) {
3820
4138
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3821
- var bodyReplaced = false;
3822
- for (var mi = 0; mi < mutations.length; mi++) {
3823
- var m = mutations[mi];
3824
- if (
3825
- m &&
3826
- m.type === 'childList' &&
3827
- m.target === doc.body &&
3828
- m.addedNodes &&
3829
- m.removedNodes &&
3830
- m.addedNodes.length > 0 &&
3831
- m.removedNodes.length > 0
3832
- ) {
3833
- bodyReplaced = true;
3834
- break;
3835
- }
3836
- }
3837
- if (bodyReplaced) {
3838
- // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3839
- appliedStructuralChangesetKeys = {};
3840
- }
3841
4139
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3842
4140
  // Keep selection sticky by re-resolving from fingerprint.
3843
4141
  recoverSelectedElement(false);
@@ -3871,6 +4169,7 @@ function syncIframeInteractions(reason) {
3871
4169
  }
3872
4170
  showNoUrl(false);
3873
4171
  injectIframeSelectionStyles(doc);
4172
+ refreshPersistentChangesetStyleTagForActiveVariation();
3874
4173
  attachClickHandler();
3875
4174
  attachDragReposition();
3876
4175
  attachChangeObserver();
package/dist/vite.js CHANGED
@@ -140,6 +140,17 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
140
140
  .tb-save-txt{font-size:14px;color:#00C951;white-space:nowrap}
141
141
  .tb-save-txt::before{content:'Saved'}
142
142
  #dirty-dot.on~.tb-save-txt::before{content:'Unsaved'}
143
+
144
+ /* \u2500\u2500 In-editor toast notification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
145
+ #ve-notification{
146
+ position:fixed;right:16px;top:64px;z-index:13000;max-width:360px;
147
+ padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.35;
148
+ border:1px solid var(--border);background:#fff;color:var(--text);
149
+ box-shadow:0 10px 24px rgba(2,6,23,.18);display:none
150
+ }
151
+ #ve-notification.show{display:block}
152
+ #ve-notification.error{border-color:#fecaca;background:#fef2f2;color:#991b1b}
153
+ #ve-notification.success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}
143
154
  .tb-save-time{font-size:12px;color:#52525b;white-space:nowrap}
144
155
  #dirty-dot.on~.tb-save-time{display:none}
145
156
  /* Simulate + Finalize buttons */
@@ -447,6 +458,49 @@ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFo
447
458
  /* \u2500\u2500 Subgroup label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
448
459
  .sub-lbl{font-size:10px;text-transform:uppercase;color:var(--text-3);font-weight:700;letter-spacing:.05em;margin:8px 0 5px}
449
460
  .sub-lbl:first-child{margin-top:0}
461
+ .sub-lbl-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:8px 0 5px}
462
+ .sub-lbl-row .sub-lbl{margin:0}
463
+ .css-expand-btn{
464
+ width:22px;height:22px;border:1px solid var(--border);border-radius:5px;background:#fff;
465
+ display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--text-3);
466
+ transition:all .15s
467
+ }
468
+ .css-expand-btn:hover{border-color:var(--accent);color:var(--accent-txt);background:var(--accent-bg)}
469
+
470
+ /* \u2500\u2500 Custom CSS modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
471
+ #custom-css-modal{
472
+ position:fixed;inset:0;z-index:12000;background:rgba(15,23,42,.5);
473
+ display:none;align-items:center;justify-content:center;padding:20px
474
+ }
475
+ #custom-css-modal.open{display:flex}
476
+ .custom-css-dialog{
477
+ width:min(860px,96vw);max-height:86vh;background:#fff;border:1px solid var(--border);
478
+ border-radius:10px;box-shadow:0 20px 40px rgba(2,6,23,.35);display:flex;flex-direction:column;overflow:hidden
479
+ }
480
+ .custom-css-head{
481
+ display:flex;align-items:center;justify-content:space-between;gap:10px;
482
+ padding:10px 12px;border-bottom:1px solid var(--border);background:#fff
483
+ }
484
+ .custom-css-title{font-size:12px;font-weight:700;color:var(--text)}
485
+ .custom-css-close{
486
+ border:none;background:transparent;color:var(--text-3);cursor:pointer;width:24px;height:24px;border-radius:5px
487
+ }
488
+ .custom-css-close:hover{background:var(--bg-hover);color:var(--text)}
489
+ #custom-css-modal-textarea{
490
+ width:100%;min-height:360px;max-height:58vh;resize:vertical;border:none;outline:none;
491
+ font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
492
+ font-size:12px;line-height:1.5;padding:12px;background:#0b1220;color:#e2e8f0
493
+ }
494
+ .custom-css-actions{
495
+ display:flex;justify-content:flex-end;gap:8px;padding:10px 12px;border-top:1px solid var(--border);background:#fff
496
+ }
497
+ .custom-css-btn{
498
+ border:1px solid var(--border);background:#fff;color:var(--text-2);border-radius:6px;padding:6px 10px;cursor:pointer;
499
+ font-size:12px;font-weight:600;font-family:inherit
500
+ }
501
+ .custom-css-btn:hover{background:var(--bg-hover)}
502
+ .custom-css-btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
503
+ .custom-css-btn.primary:hover{filter:brightness(.97)}
450
504
 
451
505
  /* \u2500\u2500 Shadow presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
452
506
  .shadow-presets{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
@@ -836,6 +890,23 @@ select.pr-inp{cursor:pointer;background:#fff}
836
890
 
837
891
  </div><!-- #app -->
838
892
 
893
+ <!-- Custom CSS full-screen modal -->
894
+ <div id="custom-css-modal" aria-hidden="true">
895
+ <div class="custom-css-dialog" role="dialog" aria-modal="true" aria-labelledby="custom-css-modal-title">
896
+ <div class="custom-css-head">
897
+ <div class="custom-css-title" id="custom-css-modal-title"><i class="bi bi-code-slash"></i> Custom CSS</div>
898
+ <button type="button" class="custom-css-close" onclick="closeCustomCssModal()" title="Close">
899
+ <i class="bi bi-x-lg"></i>
900
+ </button>
901
+ </div>
902
+ <textarea id="custom-css-modal-textarea" spellcheck="false" placeholder="Type your css here"></textarea>
903
+ <div class="custom-css-actions">
904
+ <button type="button" class="custom-css-btn" onclick="closeCustomCssModal()">Cancel</button>
905
+ <button type="button" class="custom-css-btn primary" onclick="applyCustomCssModal()">Add</button>
906
+ </div>
907
+ </div>
908
+ </div>
909
+
839
910
  <!-- CDN scripts -->
840
911
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
841
912
  <script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/builder.js"></script>
@@ -907,13 +978,37 @@ function send(type, payload) {
907
978
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
908
979
  }
909
980
 
981
+ var notificationTimer = null;
982
+ function showEditorNotification(message, kind, durationMs) {
983
+ var id = 've-notification';
984
+ var el = document.getElementById(id);
985
+ if (!el) {
986
+ el = document.createElement('div');
987
+ el.id = id;
988
+ el.setAttribute('role', 'status');
989
+ el.setAttribute('aria-live', 'polite');
990
+ document.body.appendChild(el);
991
+ }
992
+ el.className = '';
993
+ el.classList.add(kind === 'success' ? 'success' : 'error');
994
+ el.classList.add('show');
995
+ el.textContent = String(message || 'Something went wrong');
996
+ if (notificationTimer) clearTimeout(notificationTimer);
997
+ notificationTimer = setTimeout(function() {
998
+ var cur = document.getElementById(id);
999
+ if (!cur) return;
1000
+ cur.classList.remove('show');
1001
+ }, Math.max(1200, durationMs || 2600));
1002
+ }
1003
+
910
1004
  function generatePreviewUrlString(args) {
911
1005
  var baseUrl = (args && args.url) || '';
912
1006
  var test = (args && args.test) || {};
913
1007
  var variation = (args && args.variation) || {};
914
1008
  if (!baseUrl) return '';
915
- var testId = test.iid || test.experimentId || test._id || '';
916
- var variationId = variation.iid || variation._id || '';
1009
+ var testId = test.iid || '';
1010
+ var variationId = variation.iid || '';
1011
+ if (!testId || !variationId) return '';
917
1012
  var cId = String(testId || '') + '_' + String(variationId || '');
918
1013
  var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
919
1014
  return (
@@ -937,6 +1032,10 @@ function simulateExperiment() {
937
1032
  test.pageUrl ||
938
1033
  (test.metadata_1 && test.metadata_1.editor_url) ||
939
1034
  '';
1035
+ if (!test.iid || !activeVariation || !activeVariation.iid) {
1036
+ showEditorNotification('Cannot simulate: missing test.iid or variation.iid.', 'error', 3200);
1037
+ return;
1038
+ }
940
1039
  var url = generatePreviewUrlString({
941
1040
  url: targetUrl,
942
1041
  test: test,
@@ -944,6 +1043,7 @@ function simulateExperiment() {
944
1043
  });
945
1044
  if (!url) {
946
1045
  console.warn('[V2] simulateExperiment: missing target URL');
1046
+ showEditorNotification('Cannot simulate: missing target URL.', 'error', 3200);
947
1047
  return;
948
1048
  }
949
1049
  try {
@@ -961,6 +1061,19 @@ window.addEventListener('message', function(e) {
961
1061
  }
962
1062
  });
963
1063
 
1064
+ document.addEventListener('keydown', function(e) {
1065
+ if (e.key !== 'Escape') return;
1066
+ var modal = document.getElementById('custom-css-modal');
1067
+ if (!modal || !modal.classList.contains('open')) return;
1068
+ closeCustomCssModal();
1069
+ });
1070
+
1071
+ document.addEventListener('click', function(e) {
1072
+ var modal = document.getElementById('custom-css-modal');
1073
+ if (!modal || !modal.classList.contains('open')) return;
1074
+ if (e.target === modal) closeCustomCssModal();
1075
+ });
1076
+
964
1077
  // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
965
1078
  var experimentData = null;
966
1079
  var variations = [];
@@ -1040,14 +1153,17 @@ function endSuppressIframeMutationDirty() {
1040
1153
  function commitStateChangesForActiveVariation() {
1041
1154
  if (!activeVarId) return;
1042
1155
  stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1156
+ refreshPersistentChangesetStyleTagForActiveVariation();
1043
1157
  }
1044
1158
 
1045
1159
  function loadStateChangesForActiveVariation() {
1046
1160
  if (!activeVarId) {
1047
1161
  stateChanges = [];
1162
+ refreshPersistentChangesetStyleTagForActiveVariation();
1048
1163
  return;
1049
1164
  }
1050
1165
  stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1166
+ refreshPersistentChangesetStyleTagForActiveVariation();
1051
1167
  }
1052
1168
 
1053
1169
  function recoverSelectedElement(forceDeselectOnMiss) {
@@ -1527,24 +1643,26 @@ function persistActiveVariationChangesets(arr) {
1527
1643
  }
1528
1644
  }
1529
1645
  }
1646
+ refreshPersistentChangesetStyleTagForActiveVariation();
1530
1647
  }
1531
1648
 
1532
1649
  function entrySnapshotKey(entry) {
1533
1650
  if (!entry || !entry.selector) return '';
1651
+ var SEP = '__vve_sep__';
1534
1652
  var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
1535
1653
  return (
1536
1654
  selKey +
1537
- '\0' +
1655
+ SEP +
1538
1656
  normalizeChangesetType(entry) +
1539
- '\0' +
1657
+ SEP +
1540
1658
  String(entry.property || '') +
1541
- '\0' +
1659
+ SEP +
1542
1660
  String(entry.attribute || '') +
1543
- '\0' +
1661
+ SEP +
1544
1662
  String(entry.action || '') +
1545
- '\0' +
1663
+ SEP +
1546
1664
  String(entry.html != null ? 'h' + String(entry.html).length : '') +
1547
- '\0' +
1665
+ SEP +
1548
1666
  String(entry.value != null ? 'v:' + entry.value : '')
1549
1667
  );
1550
1668
  }
@@ -1594,7 +1712,6 @@ function softReloadEditorIframe() {
1594
1712
  var navGen = nextIframeContentNavGen();
1595
1713
  resetIframeBindings();
1596
1714
  setIframePageLoadingUi(true);
1597
- iframe.src = '';
1598
1715
  iframe.src = appendIframeReloadBust(src);
1599
1716
  startIframeContentApplyWatcher(navGen);
1600
1717
  scheduleDomTreeRefresh();
@@ -1603,6 +1720,7 @@ function softReloadEditorIframe() {
1603
1720
  /** @returns {boolean} true if a full iframe reload was started */
1604
1721
  function revertChangesetEntryOnDom(entry) {
1605
1722
  if (!entry) return false;
1723
+ var styleOnly = isStyleOnlyChangesetEntry(entry);
1606
1724
  if (entry.selector === '__vvveb_body__') {
1607
1725
  var iframeDoc0 = document.getElementById('iframeId').contentDocument;
1608
1726
  if (!iframeDoc0 || !iframeDoc0.body) return false;
@@ -1623,6 +1741,11 @@ function revertChangesetEntryOnDom(entry) {
1623
1741
  var snap = appliedChangesetSnapshots[k];
1624
1742
  var el = querySelectorResolved(iframeDoc, entry.selector);
1625
1743
  if (!snap || !el) {
1744
+ if (styleOnly) {
1745
+ refreshPersistentChangesetStyleTagForActiveVariation();
1746
+ delete appliedChangesetSnapshots[k];
1747
+ return false;
1748
+ }
1626
1749
  softReloadEditorIframe();
1627
1750
  delete appliedChangesetSnapshots[k];
1628
1751
  return true;
@@ -1637,6 +1760,11 @@ function revertChangesetEntryOnDom(entry) {
1637
1760
  else el.setAttribute(snap.name, snap.v);
1638
1761
  } else if (snap.kind === 'display') el.style.display = snap.v;
1639
1762
  else {
1763
+ if (styleOnly) {
1764
+ refreshPersistentChangesetStyleTagForActiveVariation();
1765
+ delete appliedChangesetSnapshots[k];
1766
+ return false;
1767
+ }
1640
1768
  softReloadEditorIframe();
1641
1769
  delete appliedChangesetSnapshots[k];
1642
1770
  return true;
@@ -1696,7 +1824,9 @@ function renderHistoryTab() {
1696
1824
  var lab = historyEntryTypeLabel(item.entry);
1697
1825
  var val = historyEntryValuePreview(item.entry);
1698
1826
  html +=
1699
- '<div class="state-item">' +
1827
+ '<div class="state-item" role="button" tabindex="0" title="Jump to element in iframe" onclick="focusHistoryChangeset(' +
1828
+ item.idx +
1829
+ ')">' +
1700
1830
  '<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
1701
1831
  item.idx +
1702
1832
  '</span>' +
@@ -1712,7 +1842,7 @@ function renderHistoryTab() {
1712
1842
  item.idx +
1713
1843
  ')" onclick="removeHistoryChangeset(' +
1714
1844
  item.idx +
1715
- ')">&#x2715;</button>' +
1845
+ ', event)">&#x2715;</button>' +
1716
1846
  '</div>';
1717
1847
  });
1718
1848
  html += '</div>';
@@ -1730,7 +1860,38 @@ function changesetListHasStructural(arr) {
1730
1860
  return false;
1731
1861
  }
1732
1862
 
1733
- function removeHistoryChangeset(idx) {
1863
+ function isStyleOnlyChangesetEntry(entry) {
1864
+ if (!entry) return false;
1865
+ var t = normalizeChangesetType(entry);
1866
+ if (t === 'style') return true;
1867
+ if (t === 'attribute' && String(entry.attribute || '').toLowerCase() === 'style') return true;
1868
+ return false;
1869
+ }
1870
+
1871
+ function focusHistoryChangeset(idx) {
1872
+ var v = getActiveVariationForHistory();
1873
+ if (!v) return;
1874
+ var arr = parseVariationChangesets(v);
1875
+ if (idx < 0 || idx >= arr.length) return;
1876
+ var entry = arr[idx];
1877
+ if (!entry || !entry.selector || entry.selector === '__vvveb_body__') return;
1878
+ try {
1879
+ var iframe = document.getElementById('iframeId');
1880
+ var iframeDoc = iframe && iframe.contentDocument;
1881
+ if (!iframeDoc) return;
1882
+ var el = querySelectorResolved(iframeDoc, entry.selector);
1883
+ if (!el) return;
1884
+ selectElement(el);
1885
+ try {
1886
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
1887
+ } catch(_) {
1888
+ el.scrollIntoView();
1889
+ }
1890
+ } catch(_) {}
1891
+ }
1892
+
1893
+ function removeHistoryChangeset(idx, evt) {
1894
+ if (evt && evt.stopPropagation) evt.stopPropagation();
1734
1895
  var v = getActiveVariationForHistory();
1735
1896
  if (!v) return;
1736
1897
  var arr = parseVariationChangesets(v);
@@ -1738,6 +1899,16 @@ function removeHistoryChangeset(idx) {
1738
1899
  var removed = arr[idx];
1739
1900
  arr.splice(idx, 1);
1740
1901
  persistActiveVariationChangesets(arr);
1902
+ if (isStyleOnlyChangesetEntry(removed)) {
1903
+ try {
1904
+ refreshPersistentChangesetStyleTagForActiveVariation();
1905
+ saveCurrentVariationHtml();
1906
+ } catch(_) {}
1907
+ if (currentMainTab === 'history') renderHistoryTab();
1908
+ recomputeEditorDirty();
1909
+ scheduleDomTreeRefresh();
1910
+ return;
1911
+ }
1741
1912
  var didReload = revertChangesetEntryOnDom(removed);
1742
1913
  try {
1743
1914
  delete varHtmlCache[activeVarId];
@@ -1745,15 +1916,19 @@ function removeHistoryChangeset(idx) {
1745
1916
  // Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
1746
1917
  // structural row remains or was removed (revert may already have started a reload for insert/body).
1747
1918
  var removedType = normalizeChangesetType(removed);
1748
- var needsStructuralReload =
1749
- !didReload &&
1750
- (removedType === 'insert' ||
1751
- removedType === 'reorder' ||
1752
- changesetListHasStructural(arr));
1919
+ var hasStructuralRemaining = changesetListHasStructural(arr);
1920
+ var removedIsStructural = removedType === 'insert' || removedType === 'reorder';
1753
1921
  if (didReload) {
1754
1922
  /* revertChangesetEntryOnDom already kicked off iframe reload */
1755
- } else if (needsStructuralReload) {
1923
+ } else if (removedIsStructural) {
1756
1924
  softReloadEditorIframe();
1925
+ } else if (hasStructuralRemaining) {
1926
+ // Keep current DOM state (already reverted for removed row) and only refresh style layer.
1927
+ // Avoid full reload and avoid re-applying all rows, which can duplicate structural insert/reorder entries.
1928
+ try {
1929
+ refreshPersistentChangesetStyleTagForActiveVariation();
1930
+ saveCurrentVariationHtml();
1931
+ } catch(_) {}
1757
1932
  } else {
1758
1933
  try {
1759
1934
  appliedStructuralChangesetKeys = {};
@@ -2121,7 +2296,7 @@ function runConsistencyReconcile() {
2121
2296
  if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2122
2297
  var granular = filterGranularChangesetEntries(cs);
2123
2298
  var unresolved = countUnresolvedGranularSelectors(doc, granular);
2124
- if (unresolved > 0 || changesetListHasStructural(cs)) {
2299
+ if (unresolved > 0 || hasUnappliedStructuralChangesets(cs)) {
2125
2300
  reapplyActiveVariationGranular(doc);
2126
2301
  registerPendingGranularChangesets(cs, doc);
2127
2302
  }
@@ -2491,24 +2666,136 @@ function flushPendingGranularChangesets() {
2491
2666
  function structuralChangesetDedupKey(entry) {
2492
2667
  var nt = normalizeChangesetType(entry);
2493
2668
  if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
2669
+ var SEP = '__vve_sep__';
2494
2670
  var vid = activeVarId || '';
2495
2671
  try {
2496
2672
  return (
2497
2673
  vid +
2498
- '\0' +
2674
+ SEP +
2499
2675
  nt +
2500
- '\0' +
2676
+ SEP +
2501
2677
  entry.selector +
2502
- '\0' +
2678
+ SEP +
2503
2679
  String(entry.action || '') +
2504
- '\0' +
2680
+ SEP +
2505
2681
  String(entry.html != null ? entry.html : '').slice(0, 240) +
2506
- '\0' +
2682
+ SEP +
2507
2683
  String(entry.targetSelector || '')
2508
2684
  );
2509
2685
  } catch(_) {
2510
- return vid + '\0' + nt + '\0' + entry.selector;
2686
+ return vid + SEP + nt + SEP + entry.selector;
2687
+ }
2688
+ }
2689
+
2690
+ function hasUnappliedStructuralChangesets(cs) {
2691
+ if (!cs || !cs.length) return false;
2692
+ for (var i = 0; i < cs.length; i++) {
2693
+ var e = cs[i];
2694
+ var t = normalizeChangesetType(e);
2695
+ if (t !== 'insert' && t !== 'reorder') continue;
2696
+ var k = structuralChangesetDedupKey(e);
2697
+ if (!k) return true;
2698
+ if (!appliedStructuralChangesetKeys[k]) return true;
2699
+ }
2700
+ return false;
2701
+ }
2702
+
2703
+ function parseInlineStyleDeclarations(styleText) {
2704
+ var out = [];
2705
+ if (styleText == null) return out;
2706
+ var s = String(styleText);
2707
+ if (!s.trim()) return out;
2708
+ var parts = s.split(';');
2709
+ for (var i = 0; i < parts.length; i++) {
2710
+ var seg = parts[i];
2711
+ if (!seg) continue;
2712
+ var idx = seg.indexOf(':');
2713
+ if (idx <= 0) continue;
2714
+ var prop = seg.slice(0, idx).trim();
2715
+ var value = seg.slice(idx + 1).trim();
2716
+ if (!prop || !value) continue;
2717
+ out.push({ prop: prop, value: value });
2511
2718
  }
2719
+ return out;
2720
+ }
2721
+
2722
+ function buildPersistentStyleRulesForActiveVariation() {
2723
+ if (!activeVarId) return '';
2724
+ var v = variations.find(function(x) { return x && x._id === activeVarId; });
2725
+ var parsed = parseVariationChangesets(v);
2726
+ var map = {};
2727
+ var order = [];
2728
+ function put(selector, prop, value) {
2729
+ if (!selector || !prop) return;
2730
+ if (value == null || value === '') return;
2731
+ var sel = sanitizeSelectorForMatch(String(selector)) || String(selector);
2732
+ var pr = String(prop).trim();
2733
+ var val = String(value).trim();
2734
+ if (!sel || !pr || !val) return;
2735
+ var k = sel + '__vve_sep__' + pr;
2736
+ if (!map[k]) order.push(k);
2737
+ map[k] = { selector: sel, property: pr, value: val };
2738
+ }
2739
+ for (var i = 0; i < parsed.length; i++) {
2740
+ var e = parsed[i];
2741
+ if (!e) continue;
2742
+ var t = normalizeChangesetType(e);
2743
+ if (t === 'style') {
2744
+ put(e.selector, e.property || e.cssProp, e.value);
2745
+ continue;
2746
+ }
2747
+ if (t === 'attribute' && String(e.attribute || '').toLowerCase() === 'style') {
2748
+ var decls = parseInlineStyleDeclarations(e.value);
2749
+ for (var di = 0; di < decls.length; di++) {
2750
+ put(e.selector, decls[di].prop, decls[di].value);
2751
+ }
2752
+ }
2753
+ }
2754
+ for (var j = 0; j < stateChanges.length; j++) {
2755
+ var c = stateChanges[j];
2756
+ if (!c) continue;
2757
+ if (c.cssProp) {
2758
+ put(c.selector, c.cssProp, c.value);
2759
+ continue;
2760
+ }
2761
+ if (c.inputId === 'pp-css') {
2762
+ var liveDecls = parseInlineStyleDeclarations(c.value);
2763
+ for (var ldi = 0; ldi < liveDecls.length; ldi++) {
2764
+ put(c.selector, liveDecls[ldi].prop, liveDecls[ldi].value);
2765
+ }
2766
+ }
2767
+ }
2768
+ var lines = [];
2769
+ for (var oi = 0; oi < order.length; oi++) {
2770
+ var row = map[order[oi]];
2771
+ lines.push(row.selector + ' { ' + row.property + ': ' + row.value + ' !important; }');
2772
+ }
2773
+ return lines.join('\\n');
2774
+ }
2775
+
2776
+ function upsertPersistentChangesetStyleTag(iframeDoc, rulesText) {
2777
+ if (!iframeDoc) return;
2778
+ var STYLE_ID = '__vve_persist_changesets_style__';
2779
+ var prev = iframeDoc.getElementById(STYLE_ID);
2780
+ if (!rulesText) {
2781
+ if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
2782
+ return;
2783
+ }
2784
+ var head = iframeDoc.head || iframeDoc.getElementsByTagName('head')[0];
2785
+ if (!head) return;
2786
+ var styleEl = prev || iframeDoc.createElement('style');
2787
+ styleEl.id = STYLE_ID;
2788
+ if (styleEl.textContent !== rulesText) styleEl.textContent = rulesText;
2789
+ if (!styleEl.parentNode) head.appendChild(styleEl);
2790
+ }
2791
+
2792
+ function refreshPersistentChangesetStyleTagForActiveVariation() {
2793
+ try {
2794
+ var iframe = document.getElementById('iframeId');
2795
+ var iframeDoc = iframe && iframe.contentDocument;
2796
+ if (!iframeDoc) return;
2797
+ upsertPersistentChangesetStyleTag(iframeDoc, buildPersistentStyleRulesForActiveVariation());
2798
+ } catch(_) {}
2512
2799
  }
2513
2800
 
2514
2801
  /**
@@ -2545,23 +2832,11 @@ function applyChangesetEntry(entry, iframeDoc) {
2545
2832
  else if (entry.value != null) el.textContent = entry.value;
2546
2833
  break;
2547
2834
  case 'style':
2548
- if (entry.property) {
2549
- var propKebab = entry.property;
2550
- var cam = camelize(propKebab);
2551
- if (entry.value == null || entry.value === '') {
2552
- try { el.style.removeProperty(propKebab); } catch(_) {}
2553
- try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
2554
- } else {
2555
- try {
2556
- el.style.setProperty(propKebab, entry.value, 'important');
2557
- } catch(_) {
2558
- el.style[cam] = entry.value;
2559
- }
2560
- }
2561
- }
2835
+ // Style changes are applied via persistent stylesheet injection.
2562
2836
  break;
2563
2837
  case 'attribute':
2564
2838
  if (entry.attribute && entry.value != null) {
2839
+ if (String(entry.attribute).toLowerCase() === 'style') break;
2565
2840
  el.setAttribute(entry.attribute, entry.value);
2566
2841
  }
2567
2842
  break;
@@ -2598,6 +2873,7 @@ function applyActiveVariationHtml() {
2598
2873
 
2599
2874
  var variation = variations.find(function(v) { return v._id === activeVarId; });
2600
2875
  var cs = parseVariationChangesets(variation);
2876
+ refreshPersistentChangesetStyleTagForActiveVariation();
2601
2877
 
2602
2878
  beginSuppressIframeMutationDirty();
2603
2879
  try {
@@ -2612,6 +2888,7 @@ function applyActiveVariationHtml() {
2612
2888
  for (var i = 0; i < cs.length; i++) {
2613
2889
  applyChangesetEntry(cs[i], iframeDoc);
2614
2890
  }
2891
+ refreshPersistentChangesetStyleTagForActiveVariation();
2615
2892
  // Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
2616
2893
  registerPendingGranularChangesets(cs, iframeDoc);
2617
2894
  } finally {
@@ -2660,6 +2937,7 @@ function applyVariationGranularOnly(iframeDoc) {
2660
2937
  for (var i = 0; i < cs.length; i++) {
2661
2938
  applyChangesetEntry(cs[i], iframeDoc);
2662
2939
  }
2940
+ refreshPersistentChangesetStyleTagForActiveVariation();
2663
2941
  registerPendingGranularChangesets(cs, iframeDoc);
2664
2942
  } finally {
2665
2943
  endSuppressIframeMutationDirty();
@@ -2678,6 +2956,7 @@ function reapplyActiveVariationGranular(iframeDoc) {
2678
2956
  for (var i = 0; i < cs.length; i++) {
2679
2957
  applyChangesetEntry(cs[i], iframeDoc);
2680
2958
  }
2959
+ refreshPersistentChangesetStyleTagForActiveVariation();
2681
2960
  } finally {
2682
2961
  endSuppressIframeMutationDirty();
2683
2962
  }
@@ -3166,6 +3445,40 @@ function pr2(l1, i1, l2, i2) {
3166
3445
  '<div class="pr2-item"><div class="pr2-lbl">'+l2+'</div>'+i2+'</div></div>';
3167
3446
  }
3168
3447
  function subLbl(text) { return '<div class="sub-lbl">'+text+'</div>'; }
3448
+ function openCustomCssModal() {
3449
+ var modal = document.getElementById('custom-css-modal');
3450
+ var ta = document.getElementById('custom-css-modal-textarea');
3451
+ if (!modal || !ta) return;
3452
+ var inp = document.getElementById('pp-css');
3453
+ var v = inp ? inp.value : (selectedEl && selectedEl.getAttribute ? (selectedEl.getAttribute('style') || '') : '');
3454
+ ta.value = v || '';
3455
+ modal.classList.add('open');
3456
+ modal.setAttribute('aria-hidden', 'false');
3457
+ setTimeout(function() {
3458
+ try { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } catch(_) {}
3459
+ }, 0);
3460
+ }
3461
+ function closeCustomCssModal() {
3462
+ var modal = document.getElementById('custom-css-modal');
3463
+ if (!modal) return;
3464
+ modal.classList.remove('open');
3465
+ modal.setAttribute('aria-hidden', 'true');
3466
+ }
3467
+ function applyCustomCssModal() {
3468
+ var ta = document.getElementById('custom-css-modal-textarea');
3469
+ var inp = document.getElementById('pp-css');
3470
+ if (!ta || !inp) {
3471
+ closeCustomCssModal();
3472
+ return;
3473
+ }
3474
+ inp.value = ta.value || '';
3475
+ try {
3476
+ inp.dispatchEvent(new Event('input', { bubbles: true }));
3477
+ } catch(_) {
3478
+ if (typeof inp.oninput === 'function') inp.oninput();
3479
+ }
3480
+ closeCustomCssModal();
3481
+ }
3169
3482
  function weightOpts(cur) {
3170
3483
  return [['100','Thin'],['200','Extra Light'],['300','Light'],['400','Normal'],['500','Medium'],
3171
3484
  ['600','Semi Bold'],['700','Bold'],['800','Extra Bold'],['900','Black']]
@@ -3446,7 +3759,12 @@ function renderRightPanel(el) {
3446
3759
  // \u2500\u2500 CSS and Classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3447
3760
  document.getElementById('acc-body-css').innerHTML =
3448
3761
  pr('Classes', '<input class="pr-inp" id="pp-cls" type="text" value="'+esc(el.className||'')+'" placeholder="class1 class2">') +
3449
- subLbl('Custom CSS') +
3762
+ '<div class="sub-lbl-row">' +
3763
+ '<div class="sub-lbl">Custom CSS</div>' +
3764
+ '<button type="button" class="css-expand-btn" title="Open full-screen editor" onclick="openCustomCssModal()">' +
3765
+ '<i class="bi bi-fullscreen"></i>' +
3766
+ '</button>' +
3767
+ '</div>' +
3450
3768
  '<textarea class="pr-inp" id="pp-css" style="width:100%;min-height:80px;font-family:monospace;font-size:11px" placeholder="color: red; font-size: 16px;">'+esc(el.getAttribute('style')||'')+'</textarea>';
3451
3769
 
3452
3770
  // \u2500\u2500 Attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -3810,26 +4128,6 @@ function attachChangeObserver() {
3810
4128
  }
3811
4129
  changeObserver = new MutationObserver(function(mutations) {
3812
4130
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3813
- var bodyReplaced = false;
3814
- for (var mi = 0; mi < mutations.length; mi++) {
3815
- var m = mutations[mi];
3816
- if (
3817
- m &&
3818
- m.type === 'childList' &&
3819
- m.target === doc.body &&
3820
- m.addedNodes &&
3821
- m.removedNodes &&
3822
- m.addedNodes.length > 0 &&
3823
- m.removedNodes.length > 0
3824
- ) {
3825
- bodyReplaced = true;
3826
- break;
3827
- }
3828
- }
3829
- if (bodyReplaced) {
3830
- // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3831
- appliedStructuralChangesetKeys = {};
3832
- }
3833
4131
  // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3834
4132
  // Keep selection sticky by re-resolving from fingerprint.
3835
4133
  recoverSelectedElement(false);
@@ -3863,6 +4161,7 @@ function syncIframeInteractions(reason) {
3863
4161
  }
3864
4162
  showNoUrl(false);
3865
4163
  injectIframeSelectionStyles(doc);
4164
+ refreshPersistentChangesetStyleTagForActiveVariation();
3866
4165
  attachClickHandler();
3867
4166
  attachDragReposition();
3868
4167
  attachChangeObserver();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",