@accelerated-agency/visual-editor 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +115 -113
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,6 +5,11 @@ import { persist } from 'zustand/middleware';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
  import { HexColorPicker } from 'react-colorful';
7
7
 
8
+ var PERSIST_NAME_PREFIX = "conversion-editor-variations-v3-";
9
+ function variationsPersistStorageName(experimentId) {
10
+ const id = experimentId?.trim();
11
+ return `${PERSIST_NAME_PREFIX}${id && id.length > 0 ? id : "standalone"}`;
12
+ }
8
13
  var VARIATION_COLORS = [
9
14
  "#0084D1",
10
15
  "#F54A00",
@@ -77,16 +82,12 @@ var useVariationsStore = create()(
77
82
  }));
78
83
  },
79
84
  setActiveVariation: (id) => set({ activeVariationId: id }),
80
- addMutationToActive: (mutation) => set((s) => {
81
- console.log("addMutationToActive", s.activeVariationId, mutation);
82
- return {
83
- variations: s.variations.map(
84
- (v) => v.id === s.activeVariationId ? { ...v, mutations: [...v.mutations, mutation] } : v
85
- ),
86
- // Clear redo stack for active variation on new mutation
87
- redoStacks: { ...s.redoStacks, [s.activeVariationId]: [] }
88
- };
89
- }),
85
+ addMutationToActive: (mutation) => set((s) => ({
86
+ variations: s.variations.map(
87
+ (v) => v.id === s.activeVariationId ? { ...v, mutations: [...v.mutations, mutation] } : v
88
+ ),
89
+ redoStacks: { ...s.redoStacks, [s.activeVariationId]: [] }
90
+ })),
90
91
  removeLastMutationFromActive: () => set((s) => {
91
92
  const active = s.variations.find((v) => v.id === s.activeVariationId);
92
93
  const removed = active?.mutations[active.mutations.length - 1];
@@ -149,7 +150,8 @@ var useVariationsStore = create()(
149
150
  }
150
151
  }),
151
152
  {
152
- name: "conversion-editor-variations",
153
+ name: variationsPersistStorageName(void 0),
154
+ skipHydration: true,
153
155
  partialize: (state) => ({
154
156
  variations: state.variations,
155
157
  activeVariationId: state.activeVariationId
@@ -157,6 +159,15 @@ var useVariationsStore = create()(
157
159
  }
158
160
  )
159
161
  );
162
+ async function hydrateVariationsFromStorage(experimentId) {
163
+ useVariationsStore.persist.setOptions({
164
+ name: variationsPersistStorageName(experimentId)
165
+ });
166
+ try {
167
+ await useVariationsStore.persist.rehydrate();
168
+ } catch {
169
+ }
170
+ }
160
171
  function TopBar({
161
172
  connectionStatus,
162
173
  onLoadUrl,
@@ -1832,13 +1843,10 @@ function ElementIcon({ tag }) {
1832
1843
  return /* @__PURE__ */ jsx("span", { className: "shrink-0 w-5 h-5 rounded flex items-center justify-center", style: { backgroundColor: bgColor }, children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("rect", { x: "1.5", y: "1.5", width: "9", height: "9", rx: "1.5", stroke: iconColor, strokeWidth: "1.2" }) }) });
1833
1844
  }
1834
1845
  var CHANNEL = "conversion-editor";
1835
- var IFRAME_LOAD_GUARD_MS = 13e4;
1836
1846
  function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong }) {
1837
1847
  const iframeElRef = useRef(null);
1838
- const loadGuardRef = useRef(null);
1839
1848
  const setSelectedElement = useMutationsStore((s) => s.setSelectedElement);
1840
1849
  const addMutationToActive = useVariationsStore((s) => s.addMutationToActive);
1841
- const [loading, setLoading] = useState(false);
1842
1850
  useEffect(() => {
1843
1851
  setIframeRef(iframeElRef.current);
1844
1852
  return () => setIframeRef(null);
@@ -1877,25 +1885,6 @@ function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong
1877
1885
  window.addEventListener("message", handleMessage);
1878
1886
  return () => window.removeEventListener("message", handleMessage);
1879
1887
  }, [handleMessage]);
1880
- useEffect(() => {
1881
- if (!url) return;
1882
- setLoading(true);
1883
- if (loadGuardRef.current) clearTimeout(loadGuardRef.current);
1884
- loadGuardRef.current = setTimeout(() => {
1885
- loadGuardRef.current = null;
1886
- setLoading(false);
1887
- }, IFRAME_LOAD_GUARD_MS);
1888
- return () => {
1889
- if (loadGuardRef.current) clearTimeout(loadGuardRef.current);
1890
- };
1891
- }, [url]);
1892
- const clearLoadGuard = useCallback(() => {
1893
- if (loadGuardRef.current) {
1894
- clearTimeout(loadGuardRef.current);
1895
- loadGuardRef.current = null;
1896
- }
1897
- setLoading(false);
1898
- }, []);
1899
1888
  let resolvedUrl;
1900
1889
  if (url.toLowerCase() === "test") {
1901
1890
  resolvedUrl = "/test";
@@ -1910,27 +1899,16 @@ function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong
1910
1899
  /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-400", children: 'Load a page from the Page dropdown in the top bar, or type "test" for the local test page' })
1911
1900
  ] });
1912
1901
  }
1913
- return /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full", children: [
1914
- loading && /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 z-10 flex flex-col gap-4 p-8", style: { backgroundColor: "#F9FAFB" }, children: [
1915
- /* @__PURE__ */ jsx("div", { className: "h-8 w-3/4 rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } }),
1916
- /* @__PURE__ */ jsx("div", { className: "h-4 w-1/2 rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } }),
1917
- /* @__PURE__ */ jsx("div", { className: "h-48 w-full rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } }),
1918
- /* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } }),
1919
- /* @__PURE__ */ jsx("div", { className: "h-4 w-1/3 rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } }),
1920
- /* @__PURE__ */ jsx("div", { className: "h-32 w-full rounded-md animate-pulse", style: { backgroundColor: "#E5E5E5" } })
1921
- ] }),
1922
- /* @__PURE__ */ jsx(
1923
- "iframe",
1924
- {
1925
- ref: iframeElRef,
1926
- src: resolvedUrl,
1927
- className: "w-full h-full border-0",
1928
- sandbox: "allow-scripts allow-same-origin allow-forms allow-popups",
1929
- onLoad: clearLoadGuard,
1930
- onError: clearLoadGuard
1931
- }
1932
- )
1933
- ] });
1902
+ return /* @__PURE__ */ jsx("div", { className: "relative w-full h-full", children: /* @__PURE__ */ jsx(
1903
+ "iframe",
1904
+ {
1905
+ ref: iframeElRef,
1906
+ src: resolvedUrl,
1907
+ className: "w-full h-full border-0",
1908
+ sandbox: "allow-scripts allow-same-origin allow-forms allow-popups",
1909
+ title: "Visual editor page"
1910
+ }
1911
+ ) });
1934
1912
  }
1935
1913
  function ElementOverlayToolbar() {
1936
1914
  const selectedElement = useMutationsStore((s) => s.selectedElement);
@@ -4078,6 +4056,9 @@ function sendToPlatform(type, payload) {
4078
4056
  data: { channel: PLATFORM_CHANNEL, type, payload }
4079
4057
  }));
4080
4058
  }
4059
+ function experimentIframeContextKey(exp) {
4060
+ return [exp.experimentId ?? "", exp.pageUrl ?? "", exp.editorPassword ?? ""].join("");
4061
+ }
4081
4062
  function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4082
4063
  const [url, setUrl] = useState("");
4083
4064
  const [password, setPassword] = useState("");
@@ -4090,7 +4071,8 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4090
4071
  );
4091
4072
  const [experimentData, setExperimentData] = useState(null);
4092
4073
  const experimentDataRef = useRef(null);
4093
- const lastInitialExperimentKeyRef = useRef("");
4074
+ const lastAppliedRunKeyRef = useRef("");
4075
+ const iframeContextKeyRef = useRef("");
4094
4076
  useEffect(() => {
4095
4077
  experimentDataRef.current = experimentData;
4096
4078
  }, [experimentData]);
@@ -4108,7 +4090,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4108
4090
  const clearAll = useVariationsStore((s) => s.clearAll);
4109
4091
  const setSelectedElement = useMutationsStore((s) => s.setSelectedElement);
4110
4092
  const selectedElement = useMutationsStore((s) => s.selectedElement);
4111
- const activeVariationId = useVariationsStore((s) => s.activeVariationId);
4112
4093
  const variations = useVariationsStore((s) => s.variations);
4113
4094
  const removeLastMutationFromActive = useVariationsStore((s) => s.removeLastMutationFromActive);
4114
4095
  const removeMutationsForSelector = useVariationsStore((s) => s.removeMutationsForSelector);
@@ -4159,7 +4140,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4159
4140
  }, [embedded, experimentData, toast, buildPlatformVariations]);
4160
4141
  const pingIntervalRef = useRef(null);
4161
4142
  const pongTimeoutRef = useRef(null);
4162
- const syncDebounceRef = useRef(null);
4163
4143
  const stopHeartbeat = useCallback(() => {
4164
4144
  if (pingIntervalRef.current) {
4165
4145
  clearInterval(pingIntervalRef.current);
@@ -4193,15 +4173,24 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4193
4173
  },
4194
4174
  [stopHeartbeat, clearAll, setSelectedElement]
4195
4175
  );
4176
+ useEffect(() => {
4177
+ if (initialExperiment) return;
4178
+ let cancelled = false;
4179
+ void (async () => {
4180
+ await hydrateVariationsFromStorage(void 0);
4181
+ if (cancelled) return;
4182
+ })();
4183
+ return () => {
4184
+ cancelled = true;
4185
+ };
4186
+ }, [initialExperiment]);
4196
4187
  useEffect(() => {
4197
4188
  if (!initialExperiment) return;
4198
- const key = JSON.stringify(initialExperiment);
4199
- if (lastInitialExperimentKeyRef.current === key) return;
4200
- lastInitialExperimentKeyRef.current = key;
4201
- setExperimentData(initialExperiment);
4202
- if (initialExperiment.pageUrl) {
4203
- handleLoadUrl(initialExperiment.pageUrl, initialExperiment.editorPassword || void 0);
4204
- }
4189
+ const runKey = JSON.stringify(initialExperiment);
4190
+ if (lastAppliedRunKeyRef.current === runKey) return;
4191
+ const ctx = experimentIframeContextKey(initialExperiment);
4192
+ const sameIframeContext = iframeContextKeyRef.current !== "" && iframeContextKeyRef.current === ctx;
4193
+ let cancelled = false;
4205
4194
  const sourceVariations = Array.isArray(initialExperiment.variations) ? initialExperiment.variations : [];
4206
4195
  const editorVariations = sourceVariations.map((v, idx) => {
4207
4196
  let mutations = [];
@@ -4221,7 +4210,31 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4221
4210
  mutations
4222
4211
  };
4223
4212
  });
4224
- loadExperimentVariations(editorVariations);
4213
+ setExperimentData(initialExperiment);
4214
+ if (sameIframeContext) {
4215
+ void (async () => {
4216
+ await hydrateVariationsFromStorage(initialExperiment.experimentId);
4217
+ if (cancelled) return;
4218
+ loadExperimentVariations(editorVariations);
4219
+ lastAppliedRunKeyRef.current = runKey;
4220
+ })();
4221
+ return () => {
4222
+ cancelled = true;
4223
+ };
4224
+ }
4225
+ if (initialExperiment.pageUrl) {
4226
+ handleLoadUrl(initialExperiment.pageUrl, initialExperiment.editorPassword || void 0);
4227
+ iframeContextKeyRef.current = ctx;
4228
+ }
4229
+ void (async () => {
4230
+ await hydrateVariationsFromStorage(initialExperiment.experimentId);
4231
+ if (cancelled) return;
4232
+ loadExperimentVariations(editorVariations);
4233
+ lastAppliedRunKeyRef.current = runKey;
4234
+ })();
4235
+ return () => {
4236
+ cancelled = true;
4237
+ };
4225
4238
  }, [initialExperiment, handleLoadUrl, loadExperimentVariations]);
4226
4239
  const handleBridgeReady = useCallback(() => {
4227
4240
  setConnectionStatus("connected");
@@ -4251,33 +4264,44 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4251
4264
  switch (msg.type) {
4252
4265
  case "load-experiment": {
4253
4266
  const data = msg.payload;
4254
- setExperimentData(data);
4255
4267
  experimentDataRef.current = data;
4256
- if (data.pageUrl && !msg.payload?.skipUrlReload) {
4257
- handleLoadUrl(data.pageUrl, data.editorPassword || void 0);
4258
- }
4259
- if (Array.isArray(data.variations)) {
4260
- const editorVariations = data.variations.map((v, idx) => {
4261
- let mutations = [];
4262
- try {
4263
- const chainSets = JSON.parse(v.changesets || "[]");
4264
- if (Array.isArray(chainSets) && chainSets.length > 0) {
4265
- mutations = convertChainSetsToMutations(chainSets);
4266
- }
4267
- } catch {
4268
+ const editorVariations = Array.isArray(data.variations) ? data.variations.map((v, idx) => {
4269
+ let mutations = [];
4270
+ try {
4271
+ const chainSets = JSON.parse(v.changesets || "[]");
4272
+ if (Array.isArray(chainSets) && chainSets.length > 0) {
4273
+ mutations = convertChainSetsToMutations(chainSets);
4268
4274
  }
4269
- return {
4270
- id: v._id || `v_${idx}`,
4271
- platformIid: v.iid,
4272
- name: v.name,
4273
- isControl: v.baseline || false,
4274
- traffic_allocation: v.traffic_allocation,
4275
- mutations
4276
- };
4277
- });
4278
- loadExperimentVariations(editorVariations);
4275
+ } catch {
4276
+ }
4277
+ return {
4278
+ id: v._id || `v_${idx}`,
4279
+ platformIid: v.iid,
4280
+ name: v.name,
4281
+ isControl: v.baseline || false,
4282
+ traffic_allocation: v.traffic_allocation,
4283
+ mutations
4284
+ };
4285
+ }) : [];
4286
+ setExperimentData(data);
4287
+ const ctx = experimentIframeContextKey(data);
4288
+ let reloadedIframe = false;
4289
+ if (data.pageUrl && !data.skipUrlReload) {
4290
+ if (iframeContextKeyRef.current !== ctx) {
4291
+ handleLoadUrl(data.pageUrl, data.editorPassword || void 0);
4292
+ iframeContextKeyRef.current = ctx;
4293
+ reloadedIframe = true;
4294
+ }
4279
4295
  }
4280
- toast(`Loaded experiment: ${data.name || "Untitled"}`, "info");
4296
+ void (async () => {
4297
+ await hydrateVariationsFromStorage(data.experimentId);
4298
+ if (Array.isArray(data.variations)) {
4299
+ loadExperimentVariations(editorVariations);
4300
+ }
4301
+ if (reloadedIframe) {
4302
+ toast(`Loaded experiment: ${data.name || "Untitled"}`, "info");
4303
+ }
4304
+ })();
4281
4305
  break;
4282
4306
  }
4283
4307
  case "request-save": {
@@ -4351,27 +4375,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4351
4375
  useEffect(() => {
4352
4376
  sendToBridge({ type: "setMode", mode: interactionMode });
4353
4377
  }, [interactionMode]);
4354
- useEffect(() => {
4355
- if (syncDebounceRef.current) {
4356
- clearTimeout(syncDebounceRef.current);
4357
- syncDebounceRef.current = null;
4358
- }
4359
- if (connectionStatus !== "connected") return;
4360
- syncDebounceRef.current = setTimeout(() => {
4361
- const currentActiveMutations = useVariationsStore.getState().getActiveMutations();
4362
- sendToBridge({ type: "clearAllMutations" });
4363
- if (currentActiveMutations.length > 0) {
4364
- sendToBridge({ type: "applyMutationBatch", mutations: currentActiveMutations });
4365
- }
4366
- syncDebounceRef.current = null;
4367
- }, 350);
4368
- return () => {
4369
- if (syncDebounceRef.current) {
4370
- clearTimeout(syncDebounceRef.current);
4371
- syncDebounceRef.current = null;
4372
- }
4373
- };
4374
- }, [connectionStatus, activeVariationId, experimentData?.experimentId]);
4375
4378
  useEffect(() => {
4376
4379
  const handler = (e) => {
4377
4380
  const meta = e.metaKey || e.ctrlKey;
@@ -4574,7 +4577,7 @@ function PlatformVisualEditor({
4574
4577
  renderLoading,
4575
4578
  renderError
4576
4579
  }) {
4577
- console.log(experiment);
4580
+ console.log("experiment", experiment);
4578
4581
  const [editorReady, setEditorReady] = useState(false);
4579
4582
  const [dirty, setDirty] = useState(false);
4580
4583
  const dirtyRef = useRef(false);
@@ -4788,7 +4791,6 @@ function PlatformVisualEditorV2({
4788
4791
  renderLoading,
4789
4792
  renderError
4790
4793
  }) {
4791
- console.log(experiment);
4792
4794
  const iframeRef = useRef(null);
4793
4795
  const [editorReady, setEditorReady] = useState(false);
4794
4796
  const [dirty, setDirty] = useState(false);
@@ -4978,4 +4980,4 @@ function PlatformVisualEditorV2({
4978
4980
  ] });
4979
4981
  }
4980
4982
 
4981
- export { EditorShell, PlatformVisualEditor, PlatformVisualEditorV2, ToastProvider, useToast };
4983
+ export { EditorShell, PlatformVisualEditor, PlatformVisualEditorV2, ToastProvider, hydrateVariationsFromStorage, useToast, variationsPersistStorageName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",