@zalify/storefront-kit 0.3.2 → 0.5.0
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 +14 -0
- package/bin/storefront-kit.mjs +66 -0
- package/dist/editor/bootstrap.d.ts +8 -0
- package/dist/editor/bootstrap.js +40 -0
- package/dist/editor/selection.d.ts +3 -0
- package/dist/editor/selection.js +42 -0
- package/dist/editor/visible-paths.d.ts +6 -0
- package/dist/editor/visible-paths.js +88 -0
- package/dist/react/design-mode.d.ts +7 -0
- package/dist/react/design-mode.js +17 -0
- package/dist/react/editor-origin.d.ts +2 -0
- package/dist/react/editor-origin.js +5 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/server-preview.d.ts +55 -0
- package/dist/react/server-preview.js +216 -0
- package/dist/react/useEditorTemplate.d.ts +4 -2
- package/dist/react/useEditorTemplate.js +6 -2
- package/package.json +25 -3
- package/skills/zalify-storefront-editor/SKILL.md +137 -0
- package/src/editor/bootstrap.ts +42 -0
- package/src/editor/selection.ts +52 -0
- package/src/editor/visible-paths.ts +99 -0
- package/src/react/design-mode.ts +21 -0
- package/src/react/editor-origin.ts +7 -0
- package/src/react/index.ts +4 -1
- package/src/react/server-preview.tsx +333 -0
- package/src/react/useEditorTemplate.ts +11 -3
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { getThemeStore, setThemePreview } from "./engine/store";
|
|
4
|
-
import { resolveEditorOrigin } from "./editor-origin";
|
|
4
|
+
import { resolveEditorOrigin, ZALIFY_EDITOR_ORIGINS } from "./editor-origin";
|
|
5
|
+
export { ZALIFY_EDITOR_ORIGINS };
|
|
6
|
+
import { enterDesignMode } from "./design-mode";
|
|
5
7
|
/** No editor code or schema is fetched outside an explicitly allowed preview. */
|
|
6
8
|
export function useEditorTemplate(name, options) {
|
|
7
9
|
const [draft, setDraft] = useState(null);
|
|
8
10
|
const controller = useRef(null);
|
|
9
11
|
useEffect(() => {
|
|
10
12
|
setDraft(null);
|
|
11
|
-
const parentOrigin = resolveEditorOrigin(options.origins);
|
|
13
|
+
const parentOrigin = resolveEditorOrigin(options.origins ?? ZALIFY_EDITOR_ORIGINS);
|
|
12
14
|
if (!parentOrigin)
|
|
13
15
|
return;
|
|
16
|
+
const leaveDesignMode = enterDesignMode();
|
|
14
17
|
let disposed = false;
|
|
15
18
|
const owner = getThemeStore();
|
|
16
19
|
void Promise.all([
|
|
@@ -93,6 +96,7 @@ export function useEditorTemplate(name, options) {
|
|
|
93
96
|
});
|
|
94
97
|
return () => {
|
|
95
98
|
disposed = true;
|
|
99
|
+
leaveDesignMode();
|
|
96
100
|
controller.current?.unmount();
|
|
97
101
|
controller.current = null;
|
|
98
102
|
setThemePreview(owner, null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalify/storefront-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"types": "./dist/editor/index.d.ts",
|
|
19
19
|
"default": "./dist/editor/index.js"
|
|
20
20
|
},
|
|
21
|
+
"./editor/bootstrap": {
|
|
22
|
+
"types": "./dist/editor/bootstrap.d.ts",
|
|
23
|
+
"default": "./dist/editor/bootstrap.js"
|
|
24
|
+
},
|
|
21
25
|
"./editor/frame": {
|
|
22
26
|
"types": "./dist/editor/frame.d.ts",
|
|
23
27
|
"default": "./dist/editor/frame.js"
|
|
@@ -26,16 +30,29 @@
|
|
|
26
30
|
"types": "./dist/editor/host.d.ts",
|
|
27
31
|
"default": "./dist/editor/host.js"
|
|
28
32
|
},
|
|
33
|
+
"./editor/selection": {
|
|
34
|
+
"types": "./dist/editor/selection.d.ts",
|
|
35
|
+
"default": "./dist/editor/selection.js"
|
|
36
|
+
},
|
|
29
37
|
"./react": {
|
|
30
38
|
"types": "./dist/react/index.d.ts",
|
|
31
39
|
"default": "./dist/react/index.js"
|
|
32
40
|
},
|
|
41
|
+
"./react/preview": {
|
|
42
|
+
"types": "./dist/react/server-preview.d.ts",
|
|
43
|
+
"default": "./dist/react/server-preview.js"
|
|
44
|
+
},
|
|
33
45
|
"./react/server": {
|
|
34
46
|
"types": "./dist/react/server.d.ts",
|
|
35
47
|
"default": "./dist/react/server.js"
|
|
36
48
|
}
|
|
37
49
|
},
|
|
50
|
+
"bin": {
|
|
51
|
+
"storefront-kit": "./bin/storefront-kit.mjs"
|
|
52
|
+
},
|
|
38
53
|
"files": [
|
|
54
|
+
"bin",
|
|
55
|
+
"skills",
|
|
39
56
|
"dist",
|
|
40
57
|
"src",
|
|
41
58
|
"!src/**/*.test.ts"
|
|
@@ -44,17 +61,22 @@
|
|
|
44
61
|
"access": "public"
|
|
45
62
|
},
|
|
46
63
|
"peerDependencies": {
|
|
47
|
-
"react": ">=18"
|
|
64
|
+
"react": ">=18",
|
|
65
|
+
"react-dom": ">=18"
|
|
48
66
|
},
|
|
49
67
|
"peerDependenciesMeta": {
|
|
50
68
|
"react": {
|
|
51
69
|
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react-dom": {
|
|
72
|
+
"optional": true
|
|
52
73
|
}
|
|
53
74
|
},
|
|
54
75
|
"devDependencies": {
|
|
55
76
|
"@types/node": "^24.0.0",
|
|
56
77
|
"@types/react": "^18.3.28",
|
|
57
|
-
"typescript": "^5.9.2"
|
|
78
|
+
"typescript": "^5.9.2",
|
|
79
|
+
"@types/react-dom": "^18.3.7"
|
|
58
80
|
},
|
|
59
81
|
"repository": {
|
|
60
82
|
"type": "git",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zalify-storefront-editor
|
|
3
|
+
description: Contract between a Zalify headless storefront and the Zalify Site Editor. Use before touching templates, section/block schemas, template routing, editor/preview integration, or anything under theme/ — and before writing any "editor glue" in a storefront repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Zalify storefront ↔ editor contract
|
|
7
|
+
|
|
8
|
+
This file ships inside `@zalify/storefront-kit` and is copied into the repo by
|
|
9
|
+
`pnpm exec storefront-kit skills sync`. Do not edit the copy: change it in the
|
|
10
|
+
kit and re-sync. Its version is the kit version in `package.json`.
|
|
11
|
+
|
|
12
|
+
## 0. Infrastructure lives in the kit, not in this repo
|
|
13
|
+
|
|
14
|
+
A storefront repo holds **content and brand**: templates, section/block
|
|
15
|
+
components, schemas, styles, data loaders. Everything that talks to the editor
|
|
16
|
+
is SDK code and must be imported, never re-implemented:
|
|
17
|
+
|
|
18
|
+
| Need | Use | Never |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| Editor bootstrap | `createEditorBootstrap` (via `useEditorTemplate`) | a hand-built `EditorBootstrap` object or template catalog |
|
|
21
|
+
| Frame bridge, selection, rects, navigation, interaction modes | `useEditorTemplate` / `mountFrameBridge` | custom `postMessage` handlers |
|
|
22
|
+
| Is this document inside the editor? | `resolveEditorOrigin` | reading `document.referrer` or the query param yourself |
|
|
23
|
+
| Allowed editor origins | `ZALIFY_EDITOR_ORIGINS` (default) | a copied origin list |
|
|
24
|
+
| Design-mode flag + `--editor-viewport-height` | set by `useEditorTemplate` | a per-repo effect that sets them |
|
|
25
|
+
| Draft application | `createEditorDraft` | mutating template JSON in components |
|
|
26
|
+
| Preview for server-rendered (RSC) storefronts | `EditorPreviewProvider`, `EditorTemplateRegion`, `EditorGroupRegion`, `useEditorPreview`, `useEditorSelection` from `@zalify/storefront-kit/react/preview` | a local provider, selection listener or visible-path observer |
|
|
27
|
+
|
|
28
|
+
Two integration shapes exist, pick by how the storefront renders:
|
|
29
|
+
|
|
30
|
+
- **Client theme engine** (`installTheme` + `ThemeTemplate`): call
|
|
31
|
+
`useEditorTemplate(name, {paths, previews, loadManifest})`.
|
|
32
|
+
- **Server-rendered templates** (RSC): wrap the layout in
|
|
33
|
+
`<EditorPreviewProvider getBootstrap={…} renderTemplate={…}>`, where both
|
|
34
|
+
props are server functions of the app — `getBootstrap` returns
|
|
35
|
+
`createEditorBootstrap(...)`, `renderTemplate` renders a draft read-only —
|
|
36
|
+
and wrap each page in `<EditorTemplateRegion name="…">`, each header/footer
|
|
37
|
+
in `<EditorGroupRegion name="…">`.
|
|
38
|
+
|
|
39
|
+
If the kit cannot express what the storefront needs, the fix is a kit change
|
|
40
|
+
(open a PR on `zalify/zalify-theme-2026`, bump the version), not a local
|
|
41
|
+
workaround. A local workaround silently diverges from every other storefront
|
|
42
|
+
and from the editor.
|
|
43
|
+
|
|
44
|
+
## 1. Templates
|
|
45
|
+
|
|
46
|
+
- Names follow Shopify: `<type>` or `<type>.<suffix>` — `product`,
|
|
47
|
+
`product.drawing-projector`, `page.about`. The type is the part before the
|
|
48
|
+
first dot and must be one of `index, product, collection, list-collections,
|
|
49
|
+
page, blog, article, cart, search, 404, password, gift_card`. A name like
|
|
50
|
+
`product-drawing-projector` or `luka-features` is an unknown type: the editor
|
|
51
|
+
cannot group it, icon it, or offer an entity picker for it. Landing pages are
|
|
52
|
+
`page.<suffix>`.
|
|
53
|
+
- Assert `templateNameProblems(names)` (from
|
|
54
|
+
`@zalify/storefront-kit/editor/bootstrap`) is empty in the repo's tests.
|
|
55
|
+
- A type that has alternates also registers its default (`page` next to
|
|
56
|
+
`page.about`). The editor nests alternates under the default.
|
|
57
|
+
- `writePath` is `<templates dir>/<name>.json`; the file on disk has the same
|
|
58
|
+
name. Renaming a template renames the file and every route that loads it.
|
|
59
|
+
- Only advertise a preview route the app can really render. Resource routes
|
|
60
|
+
(`product`, `collection`, `page`, `blog`, `article`) are added by the hook
|
|
61
|
+
when visited; never fabricate handles.
|
|
62
|
+
- `disabled: true` on a section or block means "kept in the template, not
|
|
63
|
+
rendered". The editor shows it as hidden and can toggle it. Do not use it as
|
|
64
|
+
a feature flag the merchant is not supposed to see.
|
|
65
|
+
|
|
66
|
+
## 2. Schema, template and rendering agree
|
|
67
|
+
|
|
68
|
+
- Components read section/block settings; a schema whose fields the component
|
|
69
|
+
ignores is a bug. Cover text, images, alt, button label/link, repeated items.
|
|
70
|
+
- Clearing a value stays cleared: no silent fallback to a default, never an
|
|
71
|
+
empty `src`.
|
|
72
|
+
- Repeated items have stable ids and `block_order`. After insert / delete /
|
|
73
|
+
duplicate / reorder, the serialized data and the DOM paths still match.
|
|
74
|
+
- `data-z-path` matches the template structure for every block, including ones
|
|
75
|
+
added later — not just the default blocks.
|
|
76
|
+
- Different structures get different block types (`feature-card`,
|
|
77
|
+
`timeline-entry`, `faq-item`), never a shared `item`.
|
|
78
|
+
- Labels name the purpose ("Primary button link"), not the default value.
|
|
79
|
+
- The first name-like setting (`heading`, `title`, `name`, `label`) is what the
|
|
80
|
+
editor shows in Layers. Give sections one.
|
|
81
|
+
|
|
82
|
+
## 3. Links and images
|
|
83
|
+
|
|
84
|
+
| Value | Meaning |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `#contact` | in-page href, not an element id field |
|
|
87
|
+
| `/products/example` | store-relative link |
|
|
88
|
+
| full HTTPS URL | by the field's purpose |
|
|
89
|
+
| `/images/example.jpg` | asset in `public/`; keep it root-relative |
|
|
90
|
+
| empty image | render no image node |
|
|
91
|
+
|
|
92
|
+
No preview or local origins in template defaults.
|
|
93
|
+
|
|
94
|
+
## 4. Design mode (inside the editor)
|
|
95
|
+
|
|
96
|
+
The editor sizes the iframe to the page's content. Therefore:
|
|
97
|
+
|
|
98
|
+
- Any height derived from `100vh` / `100dvh` / `min-height: 100vh` grows with
|
|
99
|
+
every height report. Under `html[data-z-design-mode='1']`, base those heights
|
|
100
|
+
on `var(--editor-viewport-height)` instead. This applies to heroes, product
|
|
101
|
+
galleries, sticky columns, full-screen drawers — check every `vh` in the CSS.
|
|
102
|
+
- Never clip the page (`max-height`, `overflow: hidden` on the root) to hide a
|
|
103
|
+
problem: later sections become unreachable.
|
|
104
|
+
- Content revealed only by scroll (IntersectionObserver, scroll animations,
|
|
105
|
+
lazy sections) needs a visible, editable presentation in design mode.
|
|
106
|
+
- Do not change the normal site's scrolling or animations to fix the preview.
|
|
107
|
+
|
|
108
|
+
## 5. Editor mode must not leak into the business
|
|
109
|
+
|
|
110
|
+
- Analytics, pixels and ad tags are skipped in editor mode (use
|
|
111
|
+
`resolveEditorOrigin` on the client, the `x-zalify-editor` header on the
|
|
112
|
+
server). An editing session is not a visitor.
|
|
113
|
+
- Forms keep their real submit logic; styling changes never alter attribution,
|
|
114
|
+
subscription, validation or payment requests.
|
|
115
|
+
- Drafts never reach public caches or production data. No unauthenticated
|
|
116
|
+
write endpoints.
|
|
117
|
+
- The storefront works with the editor unreachable; editor code is lazy-loaded
|
|
118
|
+
only after `resolveEditorOrigin` returns an origin.
|
|
119
|
+
|
|
120
|
+
## 6. Verify before shipping
|
|
121
|
+
|
|
122
|
+
- Per route (home, product, collection, content, legal, landing pages): right
|
|
123
|
+
page, images loaded, real page height and section boundaries — not a
|
|
124
|
+
screenshot squeezed to match.
|
|
125
|
+
- Per block type: edit, clear, insert, duplicate, delete, reorder; check the
|
|
126
|
+
DOM and the saved JSON.
|
|
127
|
+
- Desktop and mobile. Normal visit and inside the editor.
|
|
128
|
+
- Report what was run, what was skipped, and what is still a risk. Unverified
|
|
129
|
+
is not passed.
|
|
130
|
+
|
|
131
|
+
## 7. Upgrading the kit
|
|
132
|
+
|
|
133
|
+
1. Bump `@zalify/storefront-kit`, install, confirm the resolved version in the
|
|
134
|
+
lockfile.
|
|
135
|
+
2. `pnpm exec storefront-kit skills sync` and commit the result.
|
|
136
|
+
3. Delete any local code the new version makes redundant (see section 0).
|
|
137
|
+
4. Typecheck, test, build; open the site inside the editor once.
|
package/src/editor/bootstrap.ts
CHANGED
|
@@ -8,6 +8,48 @@ export interface EditorDocuments {
|
|
|
8
8
|
settings: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Template types the editor can group, icon and offer an entity picker for. */
|
|
12
|
+
export const TEMPLATE_TYPES = [
|
|
13
|
+
'index',
|
|
14
|
+
'product',
|
|
15
|
+
'collection',
|
|
16
|
+
'list-collections',
|
|
17
|
+
'page',
|
|
18
|
+
'blog',
|
|
19
|
+
'article',
|
|
20
|
+
'cart',
|
|
21
|
+
'search',
|
|
22
|
+
'404',
|
|
23
|
+
'password',
|
|
24
|
+
'gift_card',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What is wrong with a set of template names, for a storefront's own tests:
|
|
29
|
+
* names must be `<type>` or `<type>.<suffix>`, and a type that has alternates
|
|
30
|
+
* must also have its default. Empty means the editor can lay them out.
|
|
31
|
+
*/
|
|
32
|
+
export function templateNameProblems(names: readonly string[]): string[] {
|
|
33
|
+
const problems: string[] = [];
|
|
34
|
+
const known = new Set(names);
|
|
35
|
+
const needsDefault = new Set<string>();
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
if (name.startsWith('customers/')) continue;
|
|
38
|
+
const type = name.split('.')[0];
|
|
39
|
+
if (!(TEMPLATE_TYPES as readonly string[]).includes(type)) {
|
|
40
|
+
problems.push(
|
|
41
|
+
`"${name}": unknown template type "${type}" — use <type>.<suffix>, e.g. "page.${name.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}"`,
|
|
42
|
+
);
|
|
43
|
+
} else if (name !== type && !known.has(type)) {
|
|
44
|
+
needsDefault.add(type);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const type of needsDefault) {
|
|
48
|
+
problems.push(`"${type}.*" alternates exist but the default "${type}" template is not registered`);
|
|
49
|
+
}
|
|
50
|
+
return problems;
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
/** Only list routes the app can actually render; never fabricate handles. */
|
|
12
54
|
export function createEditorBootstrap(options: {
|
|
13
55
|
schema: DraftSchema;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_NAMESPACE,
|
|
3
|
+
CONTRACT_VERSION,
|
|
4
|
+
isCompatibleVersion,
|
|
5
|
+
} from "../schemas/index.ts";
|
|
6
|
+
|
|
7
|
+
/** Observe the same authenticated parent selection as the SDK. Components can
|
|
8
|
+
* reveal the selected tab/menu before the SDK measures its DOM node. */
|
|
9
|
+
export function mountEditorSelection(
|
|
10
|
+
win: Window,
|
|
11
|
+
onSelect: (path: string | null) => void,
|
|
12
|
+
) {
|
|
13
|
+
let origin: string | undefined;
|
|
14
|
+
const listener = (event: MessageEvent) => {
|
|
15
|
+
if (event.source !== win.parent) return;
|
|
16
|
+
const data = event.data;
|
|
17
|
+
if (
|
|
18
|
+
!data ||
|
|
19
|
+
data.z !== BRIDGE_NAMESPACE ||
|
|
20
|
+
typeof data.v !== "string" ||
|
|
21
|
+
!isCompatibleVersion(CONTRACT_VERSION, data.v) ||
|
|
22
|
+
!data.payload
|
|
23
|
+
)
|
|
24
|
+
return;
|
|
25
|
+
if (
|
|
26
|
+
!origin &&
|
|
27
|
+
data.type === "bridge:init" &&
|
|
28
|
+
event.origin === data.payload.editorOrigin
|
|
29
|
+
)
|
|
30
|
+
origin = event.origin;
|
|
31
|
+
if (!origin || event.origin !== origin) return;
|
|
32
|
+
const path =
|
|
33
|
+
data.type === "bridge:init"
|
|
34
|
+
? data.payload.selectedPath
|
|
35
|
+
: data.type === "block:select"
|
|
36
|
+
? data.payload.path
|
|
37
|
+
: undefined;
|
|
38
|
+
if (path === null || typeof path === "string") onSelect(path);
|
|
39
|
+
};
|
|
40
|
+
const click = (event: Event) => {
|
|
41
|
+
const target = event.target as Element | null;
|
|
42
|
+
const path =
|
|
43
|
+
target?.closest?.("[data-z-path]")?.getAttribute("data-z-path") ?? null;
|
|
44
|
+
if (path) onSelect(path);
|
|
45
|
+
};
|
|
46
|
+
win.addEventListener("message", listener);
|
|
47
|
+
win.document.addEventListener("click", click, true);
|
|
48
|
+
return () => {
|
|
49
|
+
win.removeEventListener("message", listener);
|
|
50
|
+
win.document.removeEventListener("click", click, true);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const PATH = "data-z-path";
|
|
2
|
+
const HIDDEN_PATH = "data-z-editor-hidden-path";
|
|
3
|
+
|
|
4
|
+
/** The SDK selects the first matching path. Responsive copies must not let a
|
|
5
|
+
* hidden desktop node shadow its visible mobile counterpart (or vice versa). */
|
|
6
|
+
export function syncVisiblePaths(doc: Document) {
|
|
7
|
+
for (const node of doc.querySelectorAll(`[${PATH}], [${HIDDEN_PATH}]`)) {
|
|
8
|
+
const path = node.getAttribute(PATH) ?? node.getAttribute(HIDDEN_PATH);
|
|
9
|
+
if (!path) continue;
|
|
10
|
+
// display:contents has no own box; a visible child still makes it selectable.
|
|
11
|
+
const visible =
|
|
12
|
+
node.getClientRects().length > 0 ||
|
|
13
|
+
Array.from(node.children).some(
|
|
14
|
+
(child) => child.getClientRects().length > 0,
|
|
15
|
+
);
|
|
16
|
+
if (visible) {
|
|
17
|
+
if (node.getAttribute(PATH) !== path) node.setAttribute(PATH, path);
|
|
18
|
+
if (node.getAttribute(HIDDEN_PATH) !== null)
|
|
19
|
+
node.removeAttribute(HIDDEN_PATH);
|
|
20
|
+
} else {
|
|
21
|
+
if (node.getAttribute(HIDDEN_PATH) !== path)
|
|
22
|
+
node.setAttribute(HIDDEN_PATH, path);
|
|
23
|
+
if (node.getAttribute(PATH) !== null) node.removeAttribute(PATH);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function restoreHiddenPaths(doc: Document) {
|
|
29
|
+
for (const node of doc.querySelectorAll(`[${HIDDEN_PATH}]`)) {
|
|
30
|
+
const path = node.getAttribute(HIDDEN_PATH);
|
|
31
|
+
if (path && !node.hasAttribute(PATH)) node.setAttribute(PATH, path);
|
|
32
|
+
node.removeAttribute(HIDDEN_PATH);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Mounted only in the editor, before the SDK's message/resize listeners. */
|
|
37
|
+
export function mountVisiblePaths(win: Window, onChange: () => void) {
|
|
38
|
+
let frame = 0;
|
|
39
|
+
const sync = () => syncVisiblePaths(win.document);
|
|
40
|
+
const schedule = () => {
|
|
41
|
+
if (frame) return;
|
|
42
|
+
frame = win.requestAnimationFrame(() => {
|
|
43
|
+
frame = 0;
|
|
44
|
+
sync();
|
|
45
|
+
onChange();
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
// Transform/opacity animations do not change which responsive copy has a box.
|
|
49
|
+
// Scanning every path for each animation frame makes long previews expensive.
|
|
50
|
+
const boxStyle = (style: string | null) =>
|
|
51
|
+
["display", "content-visibility"]
|
|
52
|
+
.map(
|
|
53
|
+
(property) =>
|
|
54
|
+
new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([^;]+)`, "i")
|
|
55
|
+
.exec(style ?? "")?.[1]
|
|
56
|
+
.trim() ?? "",
|
|
57
|
+
)
|
|
58
|
+
.join("|");
|
|
59
|
+
const observer = new MutationObserver((records) => {
|
|
60
|
+
if (
|
|
61
|
+
records.some((record) => {
|
|
62
|
+
const target = record.target as Element;
|
|
63
|
+
if (target.id === "zalify-editor-highlight") return false;
|
|
64
|
+
if (record.type === "attributes" && record.attributeName === "style")
|
|
65
|
+
return (
|
|
66
|
+
boxStyle(record.oldValue) !== boxStyle(target.getAttribute("style"))
|
|
67
|
+
);
|
|
68
|
+
return true;
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
schedule();
|
|
72
|
+
});
|
|
73
|
+
observer.observe(win.document.documentElement, {
|
|
74
|
+
subtree: true,
|
|
75
|
+
childList: true,
|
|
76
|
+
attributes: true,
|
|
77
|
+
attributeOldValue: true,
|
|
78
|
+
attributeFilter: [PATH, "class", "style", "hidden"],
|
|
79
|
+
});
|
|
80
|
+
const onMessage = (event: MessageEvent) => {
|
|
81
|
+
if (
|
|
82
|
+
event.data?.z === "zalify-editor-bridge" &&
|
|
83
|
+
["bridge:init", "block:select", "device:set", "template:apply"].includes(
|
|
84
|
+
event.data.type,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
sync();
|
|
88
|
+
};
|
|
89
|
+
win.addEventListener("resize", sync);
|
|
90
|
+
win.addEventListener("message", onMessage);
|
|
91
|
+
sync();
|
|
92
|
+
return () => {
|
|
93
|
+
observer.disconnect();
|
|
94
|
+
win.cancelAnimationFrame(frame);
|
|
95
|
+
win.removeEventListener("resize", sync);
|
|
96
|
+
win.removeEventListener("message", onMessage);
|
|
97
|
+
restoreHiddenPaths(win.document);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Cap for `--editor-viewport-height`: a tall editor pane is not a tall phone. */
|
|
2
|
+
const MAX_EDITOR_VIEWPORT_HEIGHT = 1000;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Design mode is the SDK's to declare, not each storefront's: the editor
|
|
6
|
+
* sizes the iframe to its content, so `100vh` inside it grows with every
|
|
7
|
+
* height report. Themes read `--editor-viewport-height` under
|
|
8
|
+
* `html[data-z-design-mode='1']` instead. Fixed per mount on purpose.
|
|
9
|
+
*/
|
|
10
|
+
export function enterDesignMode(): () => void {
|
|
11
|
+
const root = document.documentElement;
|
|
12
|
+
root.style.setProperty(
|
|
13
|
+
"--editor-viewport-height",
|
|
14
|
+
`${Math.min(window.innerHeight, MAX_EDITOR_VIEWPORT_HEIGHT)}px`,
|
|
15
|
+
);
|
|
16
|
+
root.setAttribute("data-z-design-mode", "1");
|
|
17
|
+
return () => {
|
|
18
|
+
root.removeAttribute("data-z-design-mode");
|
|
19
|
+
root.style.removeProperty("--editor-viewport-height");
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const STORAGE_KEY = "zalify-editor-origin";
|
|
12
12
|
|
|
13
|
+
/** Editors allowed to frame a storefront unless the app narrows the list. */
|
|
14
|
+
export const ZALIFY_EDITOR_ORIGINS: readonly string[] = [
|
|
15
|
+
"https://app.zalify.com",
|
|
16
|
+
"http://localhost:3000",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
export function resolveEditorOrigin(
|
|
14
21
|
origins: readonly string[],
|
|
15
22
|
win: Window = window,
|
package/src/react/index.ts
CHANGED
|
@@ -78,5 +78,8 @@ export {
|
|
|
78
78
|
// Server-only loaders live in '@zalify/storefront-kit/react/server'.
|
|
79
79
|
export {builtinSections, builtinBlocks} from './registries';
|
|
80
80
|
|
|
81
|
-
export {
|
|
81
|
+
export {
|
|
82
|
+
useEditorTemplate,
|
|
83
|
+
ZALIFY_EDITOR_ORIGINS,
|
|
84
|
+
} from './useEditorTemplate';
|
|
82
85
|
export {resolveEditorOrigin} from './editor-origin';
|