@accelerated-agency/visual-editor 0.1.1
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/README.md +264 -0
- package/dist/bridge-channel-ISTPKGMY.js +10 -0
- package/dist/chunk-WCSTG2IY.js +41 -0
- package/dist/index.js +5052 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# @conversion/visual-editor
|
|
2
|
+
|
|
3
|
+
Reusable visual editor package for embedding Conversion's editing UI inside a host React app.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
This package exports:
|
|
8
|
+
|
|
9
|
+
- `PlatformVisualEditor` (recommended host wrapper)
|
|
10
|
+
- `EditorShell` (low-level editor shell UI)
|
|
11
|
+
- `ToastProvider`, `useToast`
|
|
12
|
+
- `visualEditorProxyPlugin` (Vite dev-server plugin for `/api/proxy` and optional AI API route)
|
|
13
|
+
- Types: `PlatformVisualEditorProps`, `VisualEditorExperiment`, `VisualEditorVariation`, `VisualEditorTab`, plus mutation/message types
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add @conversion/visual-editor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Local development (package + consumer app)
|
|
22
|
+
|
|
23
|
+
Use this setup so changes in this package reflect in your consumer app immediately.
|
|
24
|
+
|
|
25
|
+
### 1) Consumer app dependency
|
|
26
|
+
|
|
27
|
+
In consumer app `package.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
"@conversion/visual-editor": "file:../conversion-visual-editor"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
yarn install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2) Run package in watch mode
|
|
40
|
+
|
|
41
|
+
In `conversion-visual-editor` terminal:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run watch
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3) Consumer app Vite config (local-dev only)
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { defineConfig } from "vite";
|
|
51
|
+
import path from "path";
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
resolve: {
|
|
55
|
+
preserveSymlinks: true, // local-dev only
|
|
56
|
+
},
|
|
57
|
+
optimizeDeps: {
|
|
58
|
+
exclude: ["@conversion/visual-editor"], // local-dev only
|
|
59
|
+
},
|
|
60
|
+
server: {
|
|
61
|
+
fs: {
|
|
62
|
+
allow: [path.resolve(__dirname, "..")], // local-dev only
|
|
63
|
+
},
|
|
64
|
+
watch: {
|
|
65
|
+
ignored: ["!**/node_modules/@conversion/visual-editor/**"], // local-dev only
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Start consumer app:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
yarn dev --force
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If one update is stale, clear cache once:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
rm -rf node_modules/.vite
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Remove these before/after npm publish
|
|
84
|
+
|
|
85
|
+
When you move from local `file:` development to npm package usage, remove/revert in consumer app:
|
|
86
|
+
|
|
87
|
+
- `file:../conversion-visual-editor` dependency (replace with npm version)
|
|
88
|
+
- `resolve.preserveSymlinks: true`
|
|
89
|
+
- `optimizeDeps.exclude: ["@conversion/visual-editor"]` (if no longer needed)
|
|
90
|
+
- `server.fs.allow` override used for local package path
|
|
91
|
+
- `server.watch.ignored` override for package path
|
|
92
|
+
|
|
93
|
+
## Required host setup
|
|
94
|
+
|
|
95
|
+
`PlatformVisualEditor` is designed to run in embedded mode and expects a proxied page-loading flow.
|
|
96
|
+
|
|
97
|
+
### 1) Add the Vite plugin (recommended)
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// vite.config.ts
|
|
101
|
+
import { defineConfig } from "vite";
|
|
102
|
+
import react from "@vitejs/plugin-react";
|
|
103
|
+
import { visualEditorProxyPlugin } from "@conversion/visual-editor";
|
|
104
|
+
|
|
105
|
+
export default defineConfig({
|
|
106
|
+
plugins: [
|
|
107
|
+
react(),
|
|
108
|
+
visualEditorProxyPlugin({
|
|
109
|
+
// optional, only needed for AI test generation endpoint
|
|
110
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
111
|
+
// optional, defaults to true
|
|
112
|
+
enableGenerateTestApi: true,
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
What this plugin provides:
|
|
119
|
+
|
|
120
|
+
- `GET/POST /api/proxy` for loading target storefront pages through your app origin
|
|
121
|
+
- HTML rewriting for proxied pages (asset/action rewriting, popup suppression, bridge script injection)
|
|
122
|
+
- `POST /api/generate-test` endpoint (optional; used by AI panel)
|
|
123
|
+
|
|
124
|
+
### 2) Ensure `/bridge.js` is served by your app
|
|
125
|
+
|
|
126
|
+
The proxy injects:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<script src="/bridge.js"></script>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Your host app must serve this file at `/bridge.js` for iframe/editor communication to work correctly.
|
|
133
|
+
|
|
134
|
+
### 3) Mount `PlatformVisualEditor` in your route/page
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { PlatformVisualEditor } from "@conversion/visual-editor";
|
|
138
|
+
|
|
139
|
+
export function VisualEditorPage() {
|
|
140
|
+
return (
|
|
141
|
+
<PlatformVisualEditor
|
|
142
|
+
experiment={{
|
|
143
|
+
experimentId: "exp_1",
|
|
144
|
+
name: "Homepage Hero Test",
|
|
145
|
+
status: "draft",
|
|
146
|
+
pageUrl: "https://store.example.com",
|
|
147
|
+
editorPassword: "",
|
|
148
|
+
variations: [
|
|
149
|
+
{
|
|
150
|
+
_id: "control",
|
|
151
|
+
iid: 0,
|
|
152
|
+
name: "Control",
|
|
153
|
+
baseline: true,
|
|
154
|
+
traffic_allocation: 50,
|
|
155
|
+
changesets: "[]",
|
|
156
|
+
csscode: "",
|
|
157
|
+
jscode: "",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
}}
|
|
161
|
+
onRequestSave={async ({ experimentId, variations, hash }) => {
|
|
162
|
+
// Save to your backend
|
|
163
|
+
// hash is set for save-and-navigate requests (for example "#code-editor")
|
|
164
|
+
}}
|
|
165
|
+
onNavigateRequested={(hash) => {
|
|
166
|
+
// Move to host tab/route after successful save-and-navigate
|
|
167
|
+
}}
|
|
168
|
+
onEditorUrlChanged={async ({ url, password }) => {
|
|
169
|
+
// Optional: persist latest URL/password selection
|
|
170
|
+
}}
|
|
171
|
+
onClose={() => {
|
|
172
|
+
// Close editor page/modal
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Environment variables
|
|
180
|
+
|
|
181
|
+
### Required
|
|
182
|
+
|
|
183
|
+
- None for core visual-editor functionality.
|
|
184
|
+
|
|
185
|
+
### Optional
|
|
186
|
+
|
|
187
|
+
- `ANTHROPIC_API_KEY`: required only if you use AI test generation (`/api/generate-test`).
|
|
188
|
+
- If missing and AI route is enabled, AI generation requests will fail with a server error.
|
|
189
|
+
- You can disable the route with `visualEditorProxyPlugin({ enableGenerateTestApi: false })`.
|
|
190
|
+
|
|
191
|
+
## Data contracts
|
|
192
|
+
|
|
193
|
+
### `VisualEditorExperiment`
|
|
194
|
+
|
|
195
|
+
- `experimentId?: string`
|
|
196
|
+
- `name?: string`
|
|
197
|
+
- `status?: string`
|
|
198
|
+
- `pageUrl?: string`
|
|
199
|
+
- `editorPassword?: string`
|
|
200
|
+
- `variations?: VisualEditorVariation[]`
|
|
201
|
+
|
|
202
|
+
### `VisualEditorVariation`
|
|
203
|
+
|
|
204
|
+
- `_id: string`
|
|
205
|
+
- `iid?: number`
|
|
206
|
+
- `name: string`
|
|
207
|
+
- `baseline?: boolean`
|
|
208
|
+
- `traffic_allocation?: number`
|
|
209
|
+
- `csscode?: string`
|
|
210
|
+
- `jscode?: string`
|
|
211
|
+
- `changesets?: string` (JSON string)
|
|
212
|
+
|
|
213
|
+
## Save and navigation flow
|
|
214
|
+
|
|
215
|
+
When users click save/finalize in the editor:
|
|
216
|
+
|
|
217
|
+
1. Editor emits `save-experiment` or `save-and-navigate`
|
|
218
|
+
2. `PlatformVisualEditor` calls your `onRequestSave(payload)`
|
|
219
|
+
3. On success:
|
|
220
|
+
- dirty state resets
|
|
221
|
+
- `onSaveSuccess` is called
|
|
222
|
+
- for `save-and-navigate`, `onNavigateRequested(hash)` is called
|
|
223
|
+
4. On failure:
|
|
224
|
+
- `onSaveError(error)` is called
|
|
225
|
+
|
|
226
|
+
## Main props reference (`PlatformVisualEditor`)
|
|
227
|
+
|
|
228
|
+
- `channel?: string` postMessage channel name, default `conversion-platform`
|
|
229
|
+
- `embeddedGlobalKey?: string` global embedded marker, default `__CONVERSION_EMBEDDED__`
|
|
230
|
+
- `className?: string` outer wrapper classes
|
|
231
|
+
- `editorClassName?: string` editor content wrapper classes
|
|
232
|
+
- `showHeader?: boolean` show default header
|
|
233
|
+
- `title?: string` header title override
|
|
234
|
+
- `status?: string` header status badge
|
|
235
|
+
- `tabs?: VisualEditorTab[]` tabs to render in default header
|
|
236
|
+
- `activeTab?: string` active tab label in default header
|
|
237
|
+
- `loading?: boolean` loading state
|
|
238
|
+
- `error?: string | null` error state
|
|
239
|
+
- `showCloseButton?: boolean` toggle default close button
|
|
240
|
+
- `closeLabel?: string` close button text
|
|
241
|
+
- `loadingText?: string` loading text
|
|
242
|
+
- `saveDebounceSkips?: number` mutation-change events to ignore before setting dirty
|
|
243
|
+
- `experiment?: VisualEditorExperiment` input experiment payload
|
|
244
|
+
- `onClose?: () => void` close handler
|
|
245
|
+
- `onTabChange?: (tab: VisualEditorTab) => void` tab click handler
|
|
246
|
+
- `onDirtyChange?: (dirty: boolean) => void` dirty-state callback
|
|
247
|
+
- `onEditorReady?: () => void` called after editor bootstraps
|
|
248
|
+
- `onSaveSuccess?: (payload) => void` called after successful save
|
|
249
|
+
- `onSaveError?: (error: unknown) => void` called on save failure
|
|
250
|
+
- `onRequestSave?: (payload) => Promise<void> | void` required to persist changes
|
|
251
|
+
- `onEditorUrlChanged?: (payload) => Promise<void> | void` URL/password change callback
|
|
252
|
+
- `onNavigateRequested?: (hash: string) => void` post-save navigation callback
|
|
253
|
+
- `onRestoreVersion?: (payload) => void` restore callback
|
|
254
|
+
- `onDiscardDirty?: () => boolean | Promise<boolean>` custom unsaved-changes guard
|
|
255
|
+
- `renderHeader?: (...) => ReactNode` custom header renderer
|
|
256
|
+
- `renderLoading?: () => ReactNode` custom loading renderer
|
|
257
|
+
- `renderError?: (message) => ReactNode` custom error renderer
|
|
258
|
+
|
|
259
|
+
## Known integration notes
|
|
260
|
+
|
|
261
|
+
- Keep the editor component mounted while async save is running.
|
|
262
|
+
- The editor loads target pages via `/api/proxy`; cross-origin requests made by the target page may still need proxying/server-side handling.
|
|
263
|
+
- If you customize `channel`, keep the same value on both host and editor surfaces.
|
|
264
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/lib/bridge-channel.ts
|
|
2
|
+
var CHANNEL = "conversion-editor";
|
|
3
|
+
var iframeRef = null;
|
|
4
|
+
function setIframeRef(el) {
|
|
5
|
+
iframeRef = el;
|
|
6
|
+
}
|
|
7
|
+
function sendToBridge(message) {
|
|
8
|
+
if (!iframeRef?.contentWindow) return;
|
|
9
|
+
iframeRef.contentWindow.postMessage(
|
|
10
|
+
{ channel: CHANNEL, payload: message },
|
|
11
|
+
"*"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function capturePageSnapshot(timeoutMs = 5e3) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (!iframeRef?.contentWindow) {
|
|
17
|
+
reject(new Error("Bridge iframe not available"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
window.removeEventListener("message", handler);
|
|
22
|
+
reject(new Error("Snapshot capture timed out"));
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
function handler(e) {
|
|
25
|
+
const msg = e.data;
|
|
26
|
+
if (!msg || msg.channel !== CHANNEL) return;
|
|
27
|
+
if (msg.payload?.type !== "snapshotCaptured") return;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
window.removeEventListener("message", handler);
|
|
30
|
+
resolve(msg.payload.snapshot);
|
|
31
|
+
}
|
|
32
|
+
window.addEventListener("message", handler);
|
|
33
|
+
sendToBridge({ type: "captureSnapshot" });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
setIframeRef,
|
|
39
|
+
sendToBridge,
|
|
40
|
+
capturePageSnapshot
|
|
41
|
+
};
|