@zalify/storefront-kit 0.1.9 → 0.1.12
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/editor/frame.d.ts +6 -2
- package/dist/editor/frame.js +120 -6
- package/dist/editor/host.d.ts +5 -1
- package/dist/editor/host.js +7 -0
- package/dist/react/engine/CssVariables.js +3 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/schemas/bridge.d.ts +41 -3
- package/package.json +1 -1
- package/src/editor/frame.ts +134 -8
- package/src/editor/host.ts +14 -0
- package/src/react/engine/CssVariables.tsx +3 -1
- package/src/react/index.ts +1 -0
- package/src/schemas/bridge.ts +75 -4
package/dist/editor/frame.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
17
17
|
* posted before the pin.
|
|
18
18
|
*/
|
|
19
|
-
import { type Device, type SectionGroupData, type
|
|
19
|
+
import { type Device, type EditorBootstrap, type EditorCapability, type SectionGroupData, type SettingsData, type TemplateData, type ThemeEditorManifest } from '../schemas/index.ts';
|
|
20
20
|
export declare const DATA_HOVERED_ATTR = "data-z-hovered";
|
|
21
21
|
export interface FrameBridgeOptions {
|
|
22
22
|
/** Template currently rendered ("index", "product", …). */
|
|
@@ -25,6 +25,10 @@ export interface FrameBridgeOptions {
|
|
|
25
25
|
hash: string;
|
|
26
26
|
/** Serve the editor manifest (static import or dynamic build). */
|
|
27
27
|
getManifest: () => ThemeEditorManifest | Promise<ThemeEditorManifest>;
|
|
28
|
+
/** Capabilities supported by this deployed storefront runtime. */
|
|
29
|
+
capabilities?: EditorCapability[];
|
|
30
|
+
/** Serve the complete Site Editor bootstrap payload. */
|
|
31
|
+
getBootstrap?: () => EditorBootstrap | Promise<EditorBootstrap>;
|
|
28
32
|
/**
|
|
29
33
|
* Hot-apply an updated template in memory (optimistic edit). Return
|
|
30
34
|
* false (or throw) if the app cannot apply without a reload.
|
|
@@ -33,7 +37,7 @@ export interface FrameBridgeOptions {
|
|
|
33
37
|
templateName: string;
|
|
34
38
|
template: TemplateData;
|
|
35
39
|
groups?: Record<string, SectionGroupData>;
|
|
36
|
-
settingsData?:
|
|
40
|
+
settingsData?: SettingsData;
|
|
37
41
|
}) => boolean | Promise<boolean>;
|
|
38
42
|
onDeviceChange?: (device: Device) => void;
|
|
39
43
|
/** Overrides for tests / non-browser hosts. */
|
package/dist/editor/frame.js
CHANGED
|
@@ -33,6 +33,21 @@ function rectOf(element) {
|
|
|
33
33
|
const rect = element.getBoundingClientRect();
|
|
34
34
|
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
35
35
|
}
|
|
36
|
+
function visibleRectOf(element) {
|
|
37
|
+
const ownRect = rectOf(element);
|
|
38
|
+
if (ownRect.width > 0 && ownRect.height > 0)
|
|
39
|
+
return ownRect;
|
|
40
|
+
const childRects = [...element.children]
|
|
41
|
+
.map(rectOf)
|
|
42
|
+
.filter((rect) => rect.width > 0 && rect.height > 0);
|
|
43
|
+
if (childRects.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const left = Math.min(...childRects.map((rect) => rect.x));
|
|
46
|
+
const top = Math.min(...childRects.map((rect) => rect.y));
|
|
47
|
+
const right = Math.max(...childRects.map((rect) => rect.x + rect.width));
|
|
48
|
+
const bottom = Math.max(...childRects.map((rect) => rect.y + rect.height));
|
|
49
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
50
|
+
}
|
|
36
51
|
function pathNodeOf(target) {
|
|
37
52
|
if (!(target instanceof Element))
|
|
38
53
|
return null;
|
|
@@ -47,6 +62,19 @@ export function mountFrameBridge(options) {
|
|
|
47
62
|
const doc = win.document;
|
|
48
63
|
let editorOrigin = null;
|
|
49
64
|
let lastHeight = 0;
|
|
65
|
+
let selectedPath = null;
|
|
66
|
+
const selectionHighlight = doc.createElement('div');
|
|
67
|
+
selectionHighlight.id = 'zalify-editor-highlight';
|
|
68
|
+
selectionHighlight.setAttribute('aria-hidden', 'true');
|
|
69
|
+
Object.assign(selectionHighlight.style, {
|
|
70
|
+
position: 'fixed',
|
|
71
|
+
display: 'none',
|
|
72
|
+
pointerEvents: 'none',
|
|
73
|
+
boxSizing: 'border-box',
|
|
74
|
+
border: '2px solid #2563eb',
|
|
75
|
+
zIndex: '2147483647',
|
|
76
|
+
});
|
|
77
|
+
(doc.body ?? doc.documentElement).append(selectionHighlight);
|
|
50
78
|
const post = (message) => {
|
|
51
79
|
// bridge:ready is the only pre-pin message; everything else waits.
|
|
52
80
|
const target = editorOrigin ?? '*';
|
|
@@ -63,6 +91,43 @@ export function mountFrameBridge(options) {
|
|
|
63
91
|
const selector = `[${DATA_PATH_ATTR}="${CSS.escape(path)}"]`;
|
|
64
92
|
doc.querySelector(selector)?.setAttribute(attr, '1');
|
|
65
93
|
};
|
|
94
|
+
const syncSelectionHighlight = () => {
|
|
95
|
+
if (selectedPath === null) {
|
|
96
|
+
selectionHighlight.style.display = 'none';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const selector = `[${DATA_PATH_ATTR}="${CSS.escape(selectedPath)}"]`;
|
|
100
|
+
const node = doc.querySelector(selector);
|
|
101
|
+
const rect = node ? visibleRectOf(node) : null;
|
|
102
|
+
if (!rect) {
|
|
103
|
+
selectionHighlight.style.display = 'none';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
Object.assign(selectionHighlight.style, {
|
|
107
|
+
display: 'block',
|
|
108
|
+
left: `${rect.x}px`,
|
|
109
|
+
top: `${rect.y}px`,
|
|
110
|
+
width: `${rect.width}px`,
|
|
111
|
+
height: `${rect.height}px`,
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
const selectionResizeObserver = new win.ResizeObserver(syncSelectionHighlight);
|
|
115
|
+
const setSelection = (path) => {
|
|
116
|
+
setUnique(DATA_SELECTED_ATTR, path);
|
|
117
|
+
selectedPath = path;
|
|
118
|
+
selectionResizeObserver.disconnect();
|
|
119
|
+
if (path !== null) {
|
|
120
|
+
const selector = `[${DATA_PATH_ATTR}="${CSS.escape(path)}"]`;
|
|
121
|
+
const node = doc.querySelector(selector);
|
|
122
|
+
if (node) {
|
|
123
|
+
selectionResizeObserver.observe(node);
|
|
124
|
+
for (const child of node.children) {
|
|
125
|
+
selectionResizeObserver.observe(child);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
syncSelectionHighlight();
|
|
130
|
+
};
|
|
66
131
|
const reportRects = () => {
|
|
67
132
|
const rects = [...doc.querySelectorAll(`[${DATA_PATH_ATTR}]`)].map((node) => ({
|
|
68
133
|
path: node.getAttribute(DATA_PATH_ATTR),
|
|
@@ -95,11 +160,18 @@ export function mountFrameBridge(options) {
|
|
|
95
160
|
editorOrigin = event.origin;
|
|
96
161
|
}
|
|
97
162
|
options.onDeviceChange?.(message.payload.device);
|
|
98
|
-
|
|
163
|
+
setSelection(message.payload.selectedPath);
|
|
164
|
+
// The first measurement can run before bridge:init arrives. It is
|
|
165
|
+
// intentionally not posted until the editor origin is pinned, so
|
|
166
|
+
// invalidate the cached value and publish it now.
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
lastHeight = 0;
|
|
169
|
+
measure();
|
|
170
|
+
});
|
|
99
171
|
break;
|
|
100
172
|
}
|
|
101
173
|
case 'block:select':
|
|
102
|
-
|
|
174
|
+
setSelection(message.payload.path);
|
|
103
175
|
// Rects let the host scroll its full-height canvas to the
|
|
104
176
|
// selection (the frame itself has no scrollbar in that mode).
|
|
105
177
|
reportRects();
|
|
@@ -149,6 +221,21 @@ export function mountFrameBridge(options) {
|
|
|
149
221
|
post({ type: 'manifest:response', payload: { manifest } });
|
|
150
222
|
break;
|
|
151
223
|
}
|
|
224
|
+
case 'editor:bootstrap:request': {
|
|
225
|
+
if (!options.getBootstrap) {
|
|
226
|
+
post({
|
|
227
|
+
type: 'bridge:error',
|
|
228
|
+
payload: {
|
|
229
|
+
code: 'bootstrap-unsupported',
|
|
230
|
+
message: 'frame cannot provide Site Editor bootstrap data',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
const bootstrap = await options.getBootstrap();
|
|
236
|
+
post({ type: 'editor:bootstrap:response', payload: { bootstrap } });
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
152
239
|
}
|
|
153
240
|
};
|
|
154
241
|
const handleClick = (event) => {
|
|
@@ -159,9 +246,26 @@ export function mountFrameBridge(options) {
|
|
|
159
246
|
if (!node)
|
|
160
247
|
return;
|
|
161
248
|
const path = node.getAttribute(DATA_PATH_ATTR);
|
|
162
|
-
|
|
249
|
+
setSelection(path);
|
|
163
250
|
post({ type: 'block:clicked', payload: { path, rect: rectOf(node) } });
|
|
164
251
|
};
|
|
252
|
+
const handleWheel = (event) => {
|
|
253
|
+
// Both previews are fixed device viewports. Let ordinary wheel gestures
|
|
254
|
+
// scroll the storefront; pinch/Ctrl+wheel remains a canvas zoom gesture.
|
|
255
|
+
if (!event.ctrlKey)
|
|
256
|
+
return;
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
post({
|
|
259
|
+
type: 'viewport:wheel',
|
|
260
|
+
payload: {
|
|
261
|
+
deltaX: event.deltaX,
|
|
262
|
+
deltaY: event.deltaY,
|
|
263
|
+
ctrlKey: event.ctrlKey,
|
|
264
|
+
clientX: event.clientX,
|
|
265
|
+
clientY: event.clientY,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
};
|
|
165
269
|
/** Suppress the page's own interactivity (capture phase, doc root). */
|
|
166
270
|
const suppress = (event) => {
|
|
167
271
|
event.preventDefault();
|
|
@@ -188,7 +292,7 @@ export function mountFrameBridge(options) {
|
|
|
188
292
|
});
|
|
189
293
|
};
|
|
190
294
|
})();
|
|
191
|
-
|
|
295
|
+
function measure() {
|
|
192
296
|
// documentElement.scrollHeight never drops below the viewport, so a
|
|
193
297
|
// full-height host iframe would ratchet upward forever; the body's
|
|
194
298
|
// border box tracks actual content in both directions.
|
|
@@ -197,15 +301,19 @@ export function mountFrameBridge(options) {
|
|
|
197
301
|
if (height === lastHeight || height <= 0)
|
|
198
302
|
return;
|
|
199
303
|
lastHeight = height;
|
|
304
|
+
syncSelectionHighlight();
|
|
200
305
|
post({ type: 'height:changed', payload: { height } });
|
|
201
|
-
}
|
|
306
|
+
}
|
|
202
307
|
const resizeObserver = new win.ResizeObserver(measure);
|
|
203
308
|
resizeObserver.observe(doc.documentElement);
|
|
204
309
|
if (doc.body)
|
|
205
310
|
resizeObserver.observe(doc.body);
|
|
206
311
|
win.addEventListener('message', handleMessage);
|
|
312
|
+
win.addEventListener('resize', syncSelectionHighlight);
|
|
313
|
+
win.addEventListener('scroll', syncSelectionHighlight, true);
|
|
207
314
|
doc.addEventListener('click', handleClick, true);
|
|
208
315
|
doc.addEventListener('mousemove', handleMouseMove, true);
|
|
316
|
+
doc.addEventListener('wheel', handleWheel, { capture: true, passive: false });
|
|
209
317
|
for (const type of SUPPRESSED_EVENTS) {
|
|
210
318
|
doc.addEventListener(type, suppress, true);
|
|
211
319
|
}
|
|
@@ -215,20 +323,26 @@ export function mountFrameBridge(options) {
|
|
|
215
323
|
contractVersion: CONTRACT_VERSION,
|
|
216
324
|
templateName: options.templateName,
|
|
217
325
|
hash: options.hash,
|
|
326
|
+
capabilities: options.capabilities ?? [],
|
|
218
327
|
},
|
|
219
328
|
});
|
|
220
329
|
measure();
|
|
221
330
|
return {
|
|
222
331
|
unmount: () => {
|
|
223
332
|
win.removeEventListener('message', handleMessage);
|
|
333
|
+
win.removeEventListener('resize', syncSelectionHighlight);
|
|
334
|
+
win.removeEventListener('scroll', syncSelectionHighlight, true);
|
|
224
335
|
doc.removeEventListener('click', handleClick, true);
|
|
225
336
|
doc.removeEventListener('mousemove', handleMouseMove, true);
|
|
337
|
+
doc.removeEventListener('wheel', handleWheel, true);
|
|
226
338
|
for (const type of SUPPRESSED_EVENTS) {
|
|
227
339
|
doc.removeEventListener(type, suppress, true);
|
|
228
340
|
}
|
|
229
341
|
resizeObserver.disconnect();
|
|
230
|
-
|
|
342
|
+
selectionResizeObserver.disconnect();
|
|
343
|
+
setSelection(null);
|
|
231
344
|
setUnique(DATA_HOVERED_ATTR, null);
|
|
345
|
+
selectionHighlight.remove();
|
|
232
346
|
},
|
|
233
347
|
notifySynced: (hash) => {
|
|
234
348
|
post({ type: 'template:synced', payload: { hash } });
|
package/dist/editor/host.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* when the iframe reloads (sandbox HMR hard-refresh, crash recovery),
|
|
6
6
|
* and exposes typed send/subscribe surfaces.
|
|
7
7
|
*/
|
|
8
|
-
import { type Device, type FrameMessage, type HostMessage } from '../schemas/index.ts';
|
|
8
|
+
import { type Device, type EditorCapability, type FrameMessage, type HostMessage } from '../schemas/index.ts';
|
|
9
9
|
export interface HostBridgeOptions {
|
|
10
10
|
iframe: HTMLIFrameElement;
|
|
11
11
|
/** Origin the preview is served from (the sandbox preview URL). */
|
|
@@ -16,6 +16,10 @@ export interface HostBridgeOptions {
|
|
|
16
16
|
onMessage?: (message: FrameMessage) => void;
|
|
17
17
|
/** Called when the frame speaks an incompatible contract version. */
|
|
18
18
|
onIncompatible?: (frameVersion: string) => void;
|
|
19
|
+
/** Capabilities that must be present before the frame becomes writable. */
|
|
20
|
+
requiredCapabilities?: EditorCapability[];
|
|
21
|
+
/** Called with the required capabilities missing from bridge:ready. */
|
|
22
|
+
onMissingCapabilities?: (capabilities: EditorCapability[]) => void;
|
|
19
23
|
/** Overrides for tests / non-browser hosts. */
|
|
20
24
|
window?: Window;
|
|
21
25
|
}
|
package/dist/editor/host.js
CHANGED
|
@@ -38,6 +38,13 @@ export function createHostBridge(options) {
|
|
|
38
38
|
}
|
|
39
39
|
const message = data;
|
|
40
40
|
if (message.type === 'bridge:ready') {
|
|
41
|
+
const offered = new Set(message.payload.capabilities ?? []);
|
|
42
|
+
const missing = (options.requiredCapabilities ?? []).filter((capability) => !offered.has(capability));
|
|
43
|
+
if (missing.length) {
|
|
44
|
+
ready = false;
|
|
45
|
+
options.onMissingCapabilities?.(missing);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
41
48
|
// First load and every iframe reload land here: (re)init.
|
|
42
49
|
ready = true;
|
|
43
50
|
post({
|
|
@@ -10,6 +10,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
10
10
|
*/
|
|
11
11
|
import { useMemo } from 'react';
|
|
12
12
|
import { colorSchemes, themeSettings } from './settings';
|
|
13
|
+
import { getThemeStoreVersion } from './store';
|
|
13
14
|
import { googleFontsHrefsFromSettings, parseFontHandle, } from "../../commerce/google-fonts.js";
|
|
14
15
|
function buildCss() {
|
|
15
16
|
const s = themeSettings;
|
|
@@ -106,7 +107,8 @@ const FONT_CSS_LOADER = [
|
|
|
106
107
|
'})(l[i])}',
|
|
107
108
|
].join('');
|
|
108
109
|
export function CssVariables({ nonce, fonts, } = {}) {
|
|
109
|
-
const
|
|
110
|
+
const themeVersion = getThemeStoreVersion();
|
|
111
|
+
const { css, links } = useMemo(() => ({ css: buildCss(), links: googleFontsHrefs() }), [themeVersion]);
|
|
110
112
|
// Server-resolved fonts (the next/font pattern): @font-face rules are
|
|
111
113
|
// inlined and the latin woff2 files preloaded, so the font downloads
|
|
112
114
|
// with the HTML and usually beats first paint — no async stylesheet,
|
package/dist/react/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Server loaders: import {loadSectionData} from '@zalify/storefront-kit/react/server'.
|
|
14
14
|
*/
|
|
15
15
|
export * from './engine/types';
|
|
16
|
-
export { installTheme, getTemplate, getSectionGroup, parseThemeJson, } from './engine/store';
|
|
16
|
+
export { installTheme, getTemplate, getSectionGroup, getThemeStoreVersion, parseThemeJson, } from './engine/store';
|
|
17
17
|
export type { ThemeSchema, InstallThemeOptions } from './engine/store';
|
|
18
18
|
export { themeSettings, colorSchemes, getColorScheme } from './engine/settings';
|
|
19
19
|
export { t } from './engine/translate';
|
package/dist/react/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
// Engine
|
|
16
16
|
export * from './engine/types';
|
|
17
|
-
export { installTheme, getTemplate, getSectionGroup, parseThemeJson, } from './engine/store';
|
|
17
|
+
export { installTheme, getTemplate, getSectionGroup, getThemeStoreVersion, parseThemeJson, } from './engine/store';
|
|
18
18
|
export { themeSettings, colorSchemes, getColorScheme } from './engine/settings';
|
|
19
19
|
export { t } from './engine/translate';
|
|
20
20
|
export { ThemeTemplate, SectionGroup, RenderSection, RenderSections, } from './engine/render';
|
package/dist/schemas/bridge.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Both sides validate `event.origin` and the message envelope before
|
|
7
7
|
* acting; versions negotiate on CONTRACT_VERSION's major component.
|
|
8
8
|
*/
|
|
9
|
-
import type { SectionGroupData,
|
|
9
|
+
import type { SectionGroupData, SettingsData, TemplateData } from './data.ts';
|
|
10
10
|
import type { ThemeEditorManifest } from './manifest.ts';
|
|
11
11
|
export declare const BRIDGE_NAMESPACE = "zalify-editor-bridge";
|
|
12
12
|
/** Query param that switches a theme preview into editor mode. */
|
|
@@ -16,6 +16,35 @@ export declare const DATA_PATH_ATTR = "data-z-path";
|
|
|
16
16
|
/** DOM attribute the bridge sets on the selected node. */
|
|
17
17
|
export declare const DATA_SELECTED_ATTR = "data-z-selected";
|
|
18
18
|
export type Device = 'desktop' | 'mobile';
|
|
19
|
+
export type EditorCapability = 'editor-bootstrap-v1' | 'apply-template-v1' | 'apply-groups-v1' | 'apply-settings-v1' | 'preview-navigation-v1';
|
|
20
|
+
export type PreviewResourceType = 'index' | 'product' | 'collection' | 'page' | 'blog' | 'article' | 'cart' | 'search' | 'list-collections' | '404';
|
|
21
|
+
export interface PreviewContext {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
url: string;
|
|
25
|
+
resourceType: PreviewResourceType;
|
|
26
|
+
}
|
|
27
|
+
export interface EditorBootstrap {
|
|
28
|
+
revision: string;
|
|
29
|
+
manifest: ThemeEditorManifest;
|
|
30
|
+
templates: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
writePath: string;
|
|
33
|
+
data: TemplateData;
|
|
34
|
+
preview?: PreviewContext;
|
|
35
|
+
}>;
|
|
36
|
+
groups: Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
writePath: string;
|
|
39
|
+
data: SectionGroupData;
|
|
40
|
+
}>;
|
|
41
|
+
settings: {
|
|
42
|
+
writePath: string;
|
|
43
|
+
schema: ThemeEditorManifest['settingsSchema'];
|
|
44
|
+
resolvedData: SettingsData;
|
|
45
|
+
};
|
|
46
|
+
previewContexts: Partial<Record<'product' | 'collection' | 'page' | 'blog' | 'article', PreviewContext>>;
|
|
47
|
+
}
|
|
19
48
|
export interface DOMRectLike {
|
|
20
49
|
x: number;
|
|
21
50
|
y: number;
|
|
@@ -60,14 +89,15 @@ export type HostMessage = BridgeEnvelope<'bridge:init', {
|
|
|
60
89
|
templateName: string;
|
|
61
90
|
template: TemplateData;
|
|
62
91
|
groups?: Record<string, SectionGroupData>;
|
|
63
|
-
settingsData?:
|
|
92
|
+
settingsData?: SettingsData;
|
|
64
93
|
}> | BridgeEnvelope<'device:set', {
|
|
65
94
|
device: Device;
|
|
66
|
-
}> | BridgeEnvelope<'manifest:request', Record<string, never>>;
|
|
95
|
+
}> | BridgeEnvelope<'manifest:request', Record<string, never>> | BridgeEnvelope<'editor:bootstrap:request', Record<string, never>>;
|
|
67
96
|
export type FrameMessage = BridgeEnvelope<'bridge:ready', {
|
|
68
97
|
contractVersion: string;
|
|
69
98
|
templateName: string;
|
|
70
99
|
hash: string;
|
|
100
|
+
capabilities: EditorCapability[];
|
|
71
101
|
}> | BridgeEnvelope<'block:clicked', {
|
|
72
102
|
path: string;
|
|
73
103
|
rect: DOMRectLike;
|
|
@@ -81,10 +111,18 @@ export type FrameMessage = BridgeEnvelope<'bridge:ready', {
|
|
|
81
111
|
}>;
|
|
82
112
|
}> | BridgeEnvelope<'manifest:response', {
|
|
83
113
|
manifest: ThemeEditorManifest;
|
|
114
|
+
}> | BridgeEnvelope<'editor:bootstrap:response', {
|
|
115
|
+
bootstrap: EditorBootstrap;
|
|
84
116
|
}> | BridgeEnvelope<'template:synced', {
|
|
85
117
|
hash: string;
|
|
86
118
|
}> | BridgeEnvelope<'height:changed', {
|
|
87
119
|
height: number;
|
|
120
|
+
}> | BridgeEnvelope<'viewport:wheel', {
|
|
121
|
+
deltaX: number;
|
|
122
|
+
deltaY: number;
|
|
123
|
+
ctrlKey: boolean;
|
|
124
|
+
clientX: number;
|
|
125
|
+
clientY: number;
|
|
88
126
|
}> | BridgeEnvelope<'navigation', {
|
|
89
127
|
templateName: string;
|
|
90
128
|
url: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalify/storefront-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Zalify storefront SDK: framework-agnostic commerce logic (/commerce), the theme contract types and validators (/schemas), the canvas-editor bridge (/editor), and the React theme engine + shared components (/ui, /react/server). Consumed as TypeScript source inside the zalify-storefronts monorepo; published as compiled ESM + d.ts.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/editor/frame.ts
CHANGED
|
@@ -26,10 +26,12 @@ import {
|
|
|
26
26
|
isCompatibleVersion,
|
|
27
27
|
type Device,
|
|
28
28
|
type DOMRectLike,
|
|
29
|
+
type EditorBootstrap,
|
|
30
|
+
type EditorCapability,
|
|
29
31
|
type FrameMessage,
|
|
30
32
|
type HostMessage,
|
|
31
33
|
type SectionGroupData,
|
|
32
|
-
type
|
|
34
|
+
type SettingsData,
|
|
33
35
|
type TemplateData,
|
|
34
36
|
type ThemeEditorManifest,
|
|
35
37
|
} from '../schemas/index.ts';
|
|
@@ -45,6 +47,10 @@ export interface FrameBridgeOptions {
|
|
|
45
47
|
getManifest: () =>
|
|
46
48
|
| ThemeEditorManifest
|
|
47
49
|
| Promise<ThemeEditorManifest>;
|
|
50
|
+
/** Capabilities supported by this deployed storefront runtime. */
|
|
51
|
+
capabilities?: EditorCapability[];
|
|
52
|
+
/** Serve the complete Site Editor bootstrap payload. */
|
|
53
|
+
getBootstrap?: () => EditorBootstrap | Promise<EditorBootstrap>;
|
|
48
54
|
/**
|
|
49
55
|
* Hot-apply an updated template in memory (optimistic edit). Return
|
|
50
56
|
* false (or throw) if the app cannot apply without a reload.
|
|
@@ -53,7 +59,7 @@ export interface FrameBridgeOptions {
|
|
|
53
59
|
templateName: string;
|
|
54
60
|
template: TemplateData;
|
|
55
61
|
groups?: Record<string, SectionGroupData>;
|
|
56
|
-
settingsData?:
|
|
62
|
+
settingsData?: SettingsData;
|
|
57
63
|
}) => boolean | Promise<boolean>;
|
|
58
64
|
onDeviceChange?: (device: Device) => void;
|
|
59
65
|
/** Overrides for tests / non-browser hosts. */
|
|
@@ -77,6 +83,22 @@ function rectOf(element: Element): DOMRectLike {
|
|
|
77
83
|
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
function visibleRectOf(element: Element): DOMRectLike | null {
|
|
87
|
+
const ownRect = rectOf(element);
|
|
88
|
+
if (ownRect.width > 0 && ownRect.height > 0) return ownRect;
|
|
89
|
+
|
|
90
|
+
const childRects = [...element.children]
|
|
91
|
+
.map(rectOf)
|
|
92
|
+
.filter((rect) => rect.width > 0 && rect.height > 0);
|
|
93
|
+
if (childRects.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
const left = Math.min(...childRects.map((rect) => rect.x));
|
|
96
|
+
const top = Math.min(...childRects.map((rect) => rect.y));
|
|
97
|
+
const right = Math.max(...childRects.map((rect) => rect.x + rect.width));
|
|
98
|
+
const bottom = Math.max(...childRects.map((rect) => rect.y + rect.height));
|
|
99
|
+
return {x: left, y: top, width: right - left, height: bottom - top};
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function pathNodeOf(target: EventTarget | null): HTMLElement | null {
|
|
81
103
|
if (!(target instanceof Element)) return null;
|
|
82
104
|
return target.closest<HTMLElement>(`[${DATA_PATH_ATTR}]`);
|
|
@@ -103,6 +125,20 @@ export function mountFrameBridge(
|
|
|
103
125
|
const doc = win.document;
|
|
104
126
|
let editorOrigin: string | null = null;
|
|
105
127
|
let lastHeight = 0;
|
|
128
|
+
let selectedPath: string | null = null;
|
|
129
|
+
|
|
130
|
+
const selectionHighlight = doc.createElement('div');
|
|
131
|
+
selectionHighlight.id = 'zalify-editor-highlight';
|
|
132
|
+
selectionHighlight.setAttribute('aria-hidden', 'true');
|
|
133
|
+
Object.assign(selectionHighlight.style, {
|
|
134
|
+
position: 'fixed',
|
|
135
|
+
display: 'none',
|
|
136
|
+
pointerEvents: 'none',
|
|
137
|
+
boxSizing: 'border-box',
|
|
138
|
+
border: '2px solid #2563eb',
|
|
139
|
+
zIndex: '2147483647',
|
|
140
|
+
});
|
|
141
|
+
(doc.body ?? doc.documentElement).append(selectionHighlight);
|
|
106
142
|
|
|
107
143
|
const post = (message: Omit<FrameMessage, 'z' | 'v'>): void => {
|
|
108
144
|
// bridge:ready is the only pre-pin message; everything else waits.
|
|
@@ -123,6 +159,47 @@ export function mountFrameBridge(
|
|
|
123
159
|
doc.querySelector(selector)?.setAttribute(attr, '1');
|
|
124
160
|
};
|
|
125
161
|
|
|
162
|
+
const syncSelectionHighlight = (): void => {
|
|
163
|
+
if (selectedPath === null) {
|
|
164
|
+
selectionHighlight.style.display = 'none';
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const selector = `[${DATA_PATH_ATTR}="${CSS.escape(selectedPath)}"]`;
|
|
168
|
+
const node = doc.querySelector(selector);
|
|
169
|
+
const rect = node ? visibleRectOf(node) : null;
|
|
170
|
+
if (!rect) {
|
|
171
|
+
selectionHighlight.style.display = 'none';
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
Object.assign(selectionHighlight.style, {
|
|
175
|
+
display: 'block',
|
|
176
|
+
left: `${rect.x}px`,
|
|
177
|
+
top: `${rect.y}px`,
|
|
178
|
+
width: `${rect.width}px`,
|
|
179
|
+
height: `${rect.height}px`,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const selectionResizeObserver = new win.ResizeObserver(
|
|
184
|
+
syncSelectionHighlight,
|
|
185
|
+
);
|
|
186
|
+
const setSelection = (path: string | null): void => {
|
|
187
|
+
setUnique(DATA_SELECTED_ATTR, path);
|
|
188
|
+
selectedPath = path;
|
|
189
|
+
selectionResizeObserver.disconnect();
|
|
190
|
+
if (path !== null) {
|
|
191
|
+
const selector = `[${DATA_PATH_ATTR}="${CSS.escape(path)}"]`;
|
|
192
|
+
const node = doc.querySelector(selector);
|
|
193
|
+
if (node) {
|
|
194
|
+
selectionResizeObserver.observe(node);
|
|
195
|
+
for (const child of node.children) {
|
|
196
|
+
selectionResizeObserver.observe(child);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
syncSelectionHighlight();
|
|
201
|
+
};
|
|
202
|
+
|
|
126
203
|
const reportRects = (): void => {
|
|
127
204
|
const rects = [...doc.querySelectorAll(`[${DATA_PATH_ATTR}]`)].map(
|
|
128
205
|
(node) => ({
|
|
@@ -155,11 +232,18 @@ export function mountFrameBridge(
|
|
|
155
232
|
editorOrigin = event.origin;
|
|
156
233
|
}
|
|
157
234
|
options.onDeviceChange?.(message.payload.device);
|
|
158
|
-
|
|
235
|
+
setSelection(message.payload.selectedPath);
|
|
236
|
+
// The first measurement can run before bridge:init arrives. It is
|
|
237
|
+
// intentionally not posted until the editor origin is pinned, so
|
|
238
|
+
// invalidate the cached value and publish it now.
|
|
239
|
+
queueMicrotask(() => {
|
|
240
|
+
lastHeight = 0;
|
|
241
|
+
measure();
|
|
242
|
+
});
|
|
159
243
|
break;
|
|
160
244
|
}
|
|
161
245
|
case 'block:select':
|
|
162
|
-
|
|
246
|
+
setSelection(message.payload.path);
|
|
163
247
|
// Rects let the host scroll its full-height canvas to the
|
|
164
248
|
// selection (the frame itself has no scrollbar in that mode).
|
|
165
249
|
reportRects();
|
|
@@ -208,6 +292,21 @@ export function mountFrameBridge(
|
|
|
208
292
|
post({type: 'manifest:response', payload: {manifest}});
|
|
209
293
|
break;
|
|
210
294
|
}
|
|
295
|
+
case 'editor:bootstrap:request': {
|
|
296
|
+
if (!options.getBootstrap) {
|
|
297
|
+
post({
|
|
298
|
+
type: 'bridge:error',
|
|
299
|
+
payload: {
|
|
300
|
+
code: 'bootstrap-unsupported',
|
|
301
|
+
message: 'frame cannot provide Site Editor bootstrap data',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
const bootstrap = await options.getBootstrap();
|
|
307
|
+
post({type: 'editor:bootstrap:response', payload: {bootstrap}});
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
211
310
|
}
|
|
212
311
|
};
|
|
213
312
|
|
|
@@ -218,10 +317,27 @@ export function mountFrameBridge(
|
|
|
218
317
|
event.stopPropagation();
|
|
219
318
|
if (!node) return;
|
|
220
319
|
const path = node.getAttribute(DATA_PATH_ATTR)!;
|
|
221
|
-
|
|
320
|
+
setSelection(path);
|
|
222
321
|
post({type: 'block:clicked', payload: {path, rect: rectOf(node)}});
|
|
223
322
|
};
|
|
224
323
|
|
|
324
|
+
const handleWheel = (event: WheelEvent): void => {
|
|
325
|
+
// Both previews are fixed device viewports. Let ordinary wheel gestures
|
|
326
|
+
// scroll the storefront; pinch/Ctrl+wheel remains a canvas zoom gesture.
|
|
327
|
+
if (!event.ctrlKey) return;
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
post({
|
|
330
|
+
type: 'viewport:wheel',
|
|
331
|
+
payload: {
|
|
332
|
+
deltaX: event.deltaX,
|
|
333
|
+
deltaY: event.deltaY,
|
|
334
|
+
ctrlKey: event.ctrlKey,
|
|
335
|
+
clientX: event.clientX,
|
|
336
|
+
clientY: event.clientY,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
|
|
225
341
|
/** Suppress the page's own interactivity (capture phase, doc root). */
|
|
226
342
|
const suppress = (event: Event): void => {
|
|
227
343
|
event.preventDefault();
|
|
@@ -249,7 +365,7 @@ export function mountFrameBridge(
|
|
|
249
365
|
};
|
|
250
366
|
})();
|
|
251
367
|
|
|
252
|
-
|
|
368
|
+
function measure(): void {
|
|
253
369
|
// documentElement.scrollHeight never drops below the viewport, so a
|
|
254
370
|
// full-height host iframe would ratchet upward forever; the body's
|
|
255
371
|
// border box tracks actual content in both directions.
|
|
@@ -259,15 +375,19 @@ export function mountFrameBridge(
|
|
|
259
375
|
);
|
|
260
376
|
if (height === lastHeight || height <= 0) return;
|
|
261
377
|
lastHeight = height;
|
|
378
|
+
syncSelectionHighlight();
|
|
262
379
|
post({type: 'height:changed', payload: {height}});
|
|
263
|
-
}
|
|
380
|
+
}
|
|
264
381
|
const resizeObserver = new win.ResizeObserver(measure);
|
|
265
382
|
resizeObserver.observe(doc.documentElement);
|
|
266
383
|
if (doc.body) resizeObserver.observe(doc.body);
|
|
267
384
|
|
|
268
385
|
win.addEventListener('message', handleMessage);
|
|
386
|
+
win.addEventListener('resize', syncSelectionHighlight);
|
|
387
|
+
win.addEventListener('scroll', syncSelectionHighlight, true);
|
|
269
388
|
doc.addEventListener('click', handleClick, true);
|
|
270
389
|
doc.addEventListener('mousemove', handleMouseMove, true);
|
|
390
|
+
doc.addEventListener('wheel', handleWheel, {capture: true, passive: false});
|
|
271
391
|
for (const type of SUPPRESSED_EVENTS) {
|
|
272
392
|
doc.addEventListener(type, suppress, true);
|
|
273
393
|
}
|
|
@@ -278,6 +398,7 @@ export function mountFrameBridge(
|
|
|
278
398
|
contractVersion: CONTRACT_VERSION,
|
|
279
399
|
templateName: options.templateName,
|
|
280
400
|
hash: options.hash,
|
|
401
|
+
capabilities: options.capabilities ?? [],
|
|
281
402
|
},
|
|
282
403
|
});
|
|
283
404
|
measure();
|
|
@@ -285,14 +406,19 @@ export function mountFrameBridge(
|
|
|
285
406
|
return {
|
|
286
407
|
unmount: () => {
|
|
287
408
|
win.removeEventListener('message', handleMessage);
|
|
409
|
+
win.removeEventListener('resize', syncSelectionHighlight);
|
|
410
|
+
win.removeEventListener('scroll', syncSelectionHighlight, true);
|
|
288
411
|
doc.removeEventListener('click', handleClick, true);
|
|
289
412
|
doc.removeEventListener('mousemove', handleMouseMove, true);
|
|
413
|
+
doc.removeEventListener('wheel', handleWheel, true);
|
|
290
414
|
for (const type of SUPPRESSED_EVENTS) {
|
|
291
415
|
doc.removeEventListener(type, suppress, true);
|
|
292
416
|
}
|
|
293
417
|
resizeObserver.disconnect();
|
|
294
|
-
|
|
418
|
+
selectionResizeObserver.disconnect();
|
|
419
|
+
setSelection(null);
|
|
295
420
|
setUnique(DATA_HOVERED_ATTR, null);
|
|
421
|
+
selectionHighlight.remove();
|
|
296
422
|
},
|
|
297
423
|
notifySynced: (hash) => {
|
|
298
424
|
post({type: 'template:synced', payload: {hash}});
|
package/src/editor/host.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isBridgeMessage,
|
|
13
13
|
isCompatibleVersion,
|
|
14
14
|
type Device,
|
|
15
|
+
type EditorCapability,
|
|
15
16
|
type FrameMessage,
|
|
16
17
|
type HostMessage,
|
|
17
18
|
} from '../schemas/index.ts';
|
|
@@ -26,6 +27,10 @@ export interface HostBridgeOptions {
|
|
|
26
27
|
onMessage?: (message: FrameMessage) => void;
|
|
27
28
|
/** Called when the frame speaks an incompatible contract version. */
|
|
28
29
|
onIncompatible?: (frameVersion: string) => void;
|
|
30
|
+
/** Capabilities that must be present before the frame becomes writable. */
|
|
31
|
+
requiredCapabilities?: EditorCapability[];
|
|
32
|
+
/** Called with the required capabilities missing from bridge:ready. */
|
|
33
|
+
onMissingCapabilities?: (capabilities: EditorCapability[]) => void;
|
|
29
34
|
/** Overrides for tests / non-browser hosts. */
|
|
30
35
|
window?: Window;
|
|
31
36
|
}
|
|
@@ -71,6 +76,15 @@ export function createHostBridge(options: HostBridgeOptions): HostBridge {
|
|
|
71
76
|
}
|
|
72
77
|
const message = data as FrameMessage;
|
|
73
78
|
if (message.type === 'bridge:ready') {
|
|
79
|
+
const offered = new Set(message.payload.capabilities ?? []);
|
|
80
|
+
const missing = (options.requiredCapabilities ?? []).filter(
|
|
81
|
+
(capability) => !offered.has(capability),
|
|
82
|
+
);
|
|
83
|
+
if (missing.length) {
|
|
84
|
+
ready = false;
|
|
85
|
+
options.onMissingCapabilities?.(missing);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
74
88
|
// First load and every iframe reload land here: (re)init.
|
|
75
89
|
ready = true;
|
|
76
90
|
post({
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import {useMemo} from 'react';
|
|
11
11
|
import {colorSchemes, themeSettings} from './settings';
|
|
12
|
+
import {getThemeStoreVersion} from './store';
|
|
12
13
|
import {
|
|
13
14
|
googleFontsHrefsFromSettings,
|
|
14
15
|
parseFontHandle,
|
|
@@ -136,9 +137,10 @@ export function CssVariables({
|
|
|
136
137
|
nonce,
|
|
137
138
|
fonts,
|
|
138
139
|
}: {nonce?: string; fonts?: ResolvedGoogleFontCss | null} = {}) {
|
|
140
|
+
const themeVersion = getThemeStoreVersion();
|
|
139
141
|
const {css, links} = useMemo(
|
|
140
142
|
() => ({css: buildCss(), links: googleFontsHrefs()}),
|
|
141
|
-
[],
|
|
143
|
+
[themeVersion],
|
|
142
144
|
);
|
|
143
145
|
|
|
144
146
|
// Server-resolved fonts (the next/font pattern): @font-face rules are
|
package/src/react/index.ts
CHANGED
package/src/schemas/bridge.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* Both sides validate `event.origin` and the message envelope before
|
|
7
7
|
* acting; versions negotiate on CONTRACT_VERSION's major component.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
SectionGroupData,
|
|
11
|
+
SettingsData,
|
|
12
|
+
TemplateData,
|
|
13
|
+
} from './data.ts';
|
|
10
14
|
import type {ThemeEditorManifest} from './manifest.ts';
|
|
11
15
|
|
|
12
16
|
export const BRIDGE_NAMESPACE = 'zalify-editor-bridge';
|
|
@@ -21,6 +25,56 @@ export const DATA_SELECTED_ATTR = 'data-z-selected';
|
|
|
21
25
|
|
|
22
26
|
export type Device = 'desktop' | 'mobile';
|
|
23
27
|
|
|
28
|
+
export type EditorCapability =
|
|
29
|
+
| 'editor-bootstrap-v1'
|
|
30
|
+
| 'apply-template-v1'
|
|
31
|
+
| 'apply-groups-v1'
|
|
32
|
+
| 'apply-settings-v1'
|
|
33
|
+
| 'preview-navigation-v1';
|
|
34
|
+
|
|
35
|
+
export type PreviewResourceType =
|
|
36
|
+
| 'index'
|
|
37
|
+
| 'product'
|
|
38
|
+
| 'collection'
|
|
39
|
+
| 'page'
|
|
40
|
+
| 'blog'
|
|
41
|
+
| 'article'
|
|
42
|
+
| 'cart'
|
|
43
|
+
| 'search'
|
|
44
|
+
| 'list-collections'
|
|
45
|
+
| '404';
|
|
46
|
+
|
|
47
|
+
export interface PreviewContext {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
url: string;
|
|
51
|
+
resourceType: PreviewResourceType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EditorBootstrap {
|
|
55
|
+
revision: string;
|
|
56
|
+
manifest: ThemeEditorManifest;
|
|
57
|
+
templates: Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
writePath: string;
|
|
60
|
+
data: TemplateData;
|
|
61
|
+
preview?: PreviewContext;
|
|
62
|
+
}>;
|
|
63
|
+
groups: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
writePath: string;
|
|
66
|
+
data: SectionGroupData;
|
|
67
|
+
}>;
|
|
68
|
+
settings: {
|
|
69
|
+
writePath: string;
|
|
70
|
+
schema: ThemeEditorManifest['settingsSchema'];
|
|
71
|
+
resolvedData: SettingsData;
|
|
72
|
+
};
|
|
73
|
+
previewContexts: Partial<
|
|
74
|
+
Record<'product' | 'collection' | 'page' | 'blog' | 'article', PreviewContext>
|
|
75
|
+
>;
|
|
76
|
+
}
|
|
77
|
+
|
|
24
78
|
export interface DOMRectLike {
|
|
25
79
|
x: number;
|
|
26
80
|
y: number;
|
|
@@ -96,18 +150,24 @@ export type HostMessage =
|
|
|
96
150
|
templateName: string;
|
|
97
151
|
template: TemplateData;
|
|
98
152
|
groups?: Record<string, SectionGroupData>;
|
|
99
|
-
settingsData?:
|
|
153
|
+
settingsData?: SettingsData;
|
|
100
154
|
}
|
|
101
155
|
>
|
|
102
156
|
| BridgeEnvelope<'device:set', {device: Device}>
|
|
103
|
-
| BridgeEnvelope<'manifest:request', Record<string, never
|
|
157
|
+
| BridgeEnvelope<'manifest:request', Record<string, never>>
|
|
158
|
+
| BridgeEnvelope<'editor:bootstrap:request', Record<string, never>>;
|
|
104
159
|
|
|
105
160
|
/* ------------------------- frame -> host ---------------------------- */
|
|
106
161
|
|
|
107
162
|
export type FrameMessage =
|
|
108
163
|
| BridgeEnvelope<
|
|
109
164
|
'bridge:ready',
|
|
110
|
-
{
|
|
165
|
+
{
|
|
166
|
+
contractVersion: string;
|
|
167
|
+
templateName: string;
|
|
168
|
+
hash: string;
|
|
169
|
+
capabilities: EditorCapability[];
|
|
170
|
+
}
|
|
111
171
|
>
|
|
112
172
|
| BridgeEnvelope<'block:clicked', {path: string; rect: DOMRectLike}>
|
|
113
173
|
| BridgeEnvelope<'block:hovered', {path: string | null; rect?: DOMRectLike}>
|
|
@@ -116,8 +176,19 @@ export type FrameMessage =
|
|
|
116
176
|
{rects: Array<{path: string; rect: DOMRectLike}>}
|
|
117
177
|
>
|
|
118
178
|
| BridgeEnvelope<'manifest:response', {manifest: ThemeEditorManifest}>
|
|
179
|
+
| BridgeEnvelope<'editor:bootstrap:response', {bootstrap: EditorBootstrap}>
|
|
119
180
|
| BridgeEnvelope<'template:synced', {hash: string}>
|
|
120
181
|
| BridgeEnvelope<'height:changed', {height: number}>
|
|
182
|
+
| BridgeEnvelope<
|
|
183
|
+
'viewport:wheel',
|
|
184
|
+
{
|
|
185
|
+
deltaX: number;
|
|
186
|
+
deltaY: number;
|
|
187
|
+
ctrlKey: boolean;
|
|
188
|
+
clientX: number;
|
|
189
|
+
clientY: number;
|
|
190
|
+
}
|
|
191
|
+
>
|
|
121
192
|
| BridgeEnvelope<'navigation', {templateName: string; url: string}>
|
|
122
193
|
| BridgeEnvelope<'bridge:error', {code: string; message: string}>;
|
|
123
194
|
|