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