@accelerated-agency/visual-editor 0.3.0 → 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/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;
2511
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 });
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();
@@ -4686,6 +4985,101 @@ if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.ser
4686
4985
  } else {
4687
4986
  html = runtimeProxyScript + html;
4688
4987
  }
4988
+ const aiBridgeRuntimeScript = `<script>(function(){try{
4989
+ var AI_CHANNEL="ve-ai-editor";
4990
+ var lastJs="";
4991
+ var aiSheet=null;
4992
+ var lastAiContainerIds=[];
4993
+ function extractAiContainerIds(source){
4994
+ try{
4995
+ var re=/ai-generated-container-[a-zA-Z0-9_-]+/g;
4996
+ var found=String(source||"").match(re)||[];
4997
+ var uniq=[];
4998
+ for(var i=0;i<found.length;i++){
4999
+ if(uniq.indexOf(found[i])===-1) uniq.push(found[i]);
5000
+ }
5001
+ return uniq;
5002
+ }catch(_){
5003
+ return [];
5004
+ }
5005
+ }
5006
+ function cleanupRemovedAiContainers(nextIds){
5007
+ try{
5008
+ for(var i=0;i<lastAiContainerIds.length;i++){
5009
+ var prevId=lastAiContainerIds[i];
5010
+ if(nextIds.indexOf(prevId)!==-1) continue;
5011
+ var el=document.getElementById(prevId);
5012
+ if(!el) continue;
5013
+ var marker=(el.getAttribute("data-ai-generated")||"").toLowerCase();
5014
+ if(marker==="true"||prevId.indexOf("ai-generated-container-")===0){
5015
+ if(el.parentNode) el.parentNode.removeChild(el);
5016
+ }
5017
+ }
5018
+ }catch(_){}
5019
+ }
5020
+ function getProxyPayload(){
5021
+ try{
5022
+ var u=new URL(window.location.href);
5023
+ return {
5024
+ url:u.searchParams.get("url")||undefined,
5025
+ password:u.searchParams.get("password")||undefined
5026
+ };
5027
+ }catch(_){
5028
+ return {};
5029
+ }
5030
+ }
5031
+ function applyAiCode(payload){
5032
+ try{
5033
+ var p=payload||{};
5034
+ var css=typeof p.cssCode==="string"?p.cssCode:"";
5035
+ var js=typeof p.jsCode==="string"?p.jsCode:"";
5036
+ var doc=document;
5037
+ if(typeof CSSStyleSheet!=="undefined"&&"adoptedStyleSheets" in doc){
5038
+ var sheets=doc.adoptedStyleSheets||[];
5039
+ if(css){
5040
+ if(!aiSheet) aiSheet=new CSSStyleSheet();
5041
+ aiSheet.replaceSync(css);
5042
+ if(sheets.indexOf(aiSheet)===-1){
5043
+ doc.adoptedStyleSheets=sheets.concat(aiSheet);
5044
+ }
5045
+ }else if(aiSheet&&sheets.indexOf(aiSheet)!==-1){
5046
+ doc.adoptedStyleSheets=sheets.filter(function(s){return s!==aiSheet;});
5047
+ }
5048
+ }
5049
+ if(js!==lastJs){
5050
+ var nextIds=extractAiContainerIds(js);
5051
+ cleanupRemovedAiContainers(nextIds);
5052
+ if(js) (new Function(js)).call(window);
5053
+ lastJs=js;
5054
+ lastAiContainerIds=nextIds;
5055
+ }
5056
+ try{
5057
+ doc.documentElement.setAttribute("data-ve-ai-applied","1");
5058
+ window.parent&&window.parent.postMessage({
5059
+ channel:AI_CHANNEL,
5060
+ type:"ai-code-applied",
5061
+ payload:{cssLen:css.length,jsLen:js.length}
5062
+ },"*");
5063
+ }catch(_){}
5064
+ }catch(_){}
5065
+ }
5066
+ window.addEventListener("message",function(ev){
5067
+ try{
5068
+ var d=ev&&ev.data;
5069
+ if(!d||d.channel!==AI_CHANNEL||d.type!=="apply-ai-code") return;
5070
+ applyAiCode(d.payload||{});
5071
+ }catch(_){}
5072
+ });
5073
+ try{
5074
+ if(window.parent){
5075
+ window.parent.postMessage({channel:AI_CHANNEL,type:"ai-code-ready"},"*");
5076
+ window.parent.postMessage({channel:AI_CHANNEL,type:"ai-url-changed",payload:getProxyPayload()},"*");
5077
+ }
5078
+ }catch(_){}
5079
+ }catch(_){}})();</script>`;
5080
+ html = html.includes("</head>") ? html.replace("</head>", `${aiBridgeRuntimeScript}
5081
+ </head>`) : `${aiBridgeRuntimeScript}
5082
+ ${html}`;
4689
5083
  const bridgeScript = `<script src="/bridge.js"></script>`;
4690
5084
  html = html.includes("</body>") ? html.replace("</body>", `${bridgeScript}
4691
5085
  </body>`) : html + bridgeScript;