@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 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,10 @@
1
+ import {
2
+ capturePageSnapshot,
3
+ sendToBridge,
4
+ setIframeRef
5
+ } from "./chunk-WCSTG2IY.js";
6
+ export {
7
+ capturePageSnapshot,
8
+ sendToBridge,
9
+ setIframeRef
10
+ };
@@ -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
+ };