@zalify/storefront-kit 0.1.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.
Files changed (213) hide show
  1. package/LICENSE.md +49 -0
  2. package/README.md +63 -0
  3. package/dist/commerce/countdown.d.ts +18 -0
  4. package/dist/commerce/countdown.js +28 -0
  5. package/dist/commerce/events.d.ts +29 -0
  6. package/dist/commerce/events.js +14 -0
  7. package/dist/commerce/facets.d.ts +27 -0
  8. package/dist/commerce/facets.js +71 -0
  9. package/dist/commerce/index.d.ts +11 -0
  10. package/dist/commerce/index.js +11 -0
  11. package/dist/commerce/money.d.ts +51 -0
  12. package/dist/commerce/money.js +83 -0
  13. package/dist/commerce/product.d.ts +102 -0
  14. package/dist/commerce/product.js +93 -0
  15. package/dist/editor/frame.d.ts +57 -0
  16. package/dist/editor/frame.js +241 -0
  17. package/dist/editor/host.d.ts +31 -0
  18. package/dist/editor/host.js +67 -0
  19. package/dist/editor/index.d.ts +9 -0
  20. package/dist/editor/index.js +9 -0
  21. package/dist/react/adapter.d.ts +49 -0
  22. package/dist/react/adapter.js +23 -0
  23. package/dist/react/blocks/_box.d.ts +10 -0
  24. package/dist/react/blocks/_box.js +17 -0
  25. package/dist/react/blocks/_slide.d.ts +24 -0
  26. package/dist/react/blocks/_slide.js +21 -0
  27. package/dist/react/blocks/_video-card.d.ts +11 -0
  28. package/dist/react/blocks/_video-card.js +39 -0
  29. package/dist/react/blocks/accordion.d.ts +12 -0
  30. package/dist/react/blocks/accordion.js +5 -0
  31. package/dist/react/blocks/button.d.ts +10 -0
  32. package/dist/react/blocks/button.js +15 -0
  33. package/dist/react/blocks/countdown.d.ts +9 -0
  34. package/dist/react/blocks/countdown.js +64 -0
  35. package/dist/react/blocks/custom-liquid.d.ts +11 -0
  36. package/dist/react/blocks/custom-liquid.js +4 -0
  37. package/dist/react/blocks/group.d.ts +9 -0
  38. package/dist/react/blocks/group.js +8 -0
  39. package/dist/react/blocks/image.d.ts +8 -0
  40. package/dist/react/blocks/image.js +7 -0
  41. package/dist/react/blocks/message.d.ts +14 -0
  42. package/dist/react/blocks/message.js +5 -0
  43. package/dist/react/blocks/quote.d.ts +9 -0
  44. package/dist/react/blocks/quote.js +5 -0
  45. package/dist/react/blocks/spacer.d.ts +8 -0
  46. package/dist/react/blocks/spacer.js +5 -0
  47. package/dist/react/blocks/story.d.ts +17 -0
  48. package/dist/react/blocks/story.js +7 -0
  49. package/dist/react/blocks/text.d.ts +9 -0
  50. package/dist/react/blocks/text.js +5 -0
  51. package/dist/react/blocks/video.d.ts +17 -0
  52. package/dist/react/blocks/video.js +56 -0
  53. package/dist/react/components/Facets.d.ts +21 -0
  54. package/dist/react/components/Facets.js +220 -0
  55. package/dist/react/components/Icon.d.ts +12 -0
  56. package/dist/react/components/Icon.js +66 -0
  57. package/dist/react/components/PaymentIcons.d.ts +5 -0
  58. package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
  59. package/dist/react/components/PaymentIcons.generated.js +21 -0
  60. package/dist/react/components/PaymentIcons.js +18 -0
  61. package/dist/react/components/Price.d.ts +24 -0
  62. package/dist/react/components/Price.js +9 -0
  63. package/dist/react/components/ProductBadges.d.ts +24 -0
  64. package/dist/react/components/ProductBadges.js +57 -0
  65. package/dist/react/components/ProductCard.d.ts +17 -0
  66. package/dist/react/components/ProductCard.js +202 -0
  67. package/dist/react/components/Rating.d.ts +11 -0
  68. package/dist/react/components/Rating.js +35 -0
  69. package/dist/react/components/SwatchStyle.d.ts +23 -0
  70. package/dist/react/components/SwatchStyle.js +22 -0
  71. package/dist/react/components/VideoModal.d.ts +55 -0
  72. package/dist/react/components/VideoModal.js +102 -0
  73. package/dist/react/engine/CssVariables.d.ts +1 -0
  74. package/dist/react/engine/CssVariables.js +105 -0
  75. package/dist/react/engine/context.d.ts +43 -0
  76. package/dist/react/engine/context.js +45 -0
  77. package/dist/react/engine/fonts.d.ts +15 -0
  78. package/dist/react/engine/fonts.js +33 -0
  79. package/dist/react/engine/images.d.ts +24 -0
  80. package/dist/react/engine/images.js +40 -0
  81. package/dist/react/engine/render.d.ts +29 -0
  82. package/dist/react/engine/render.js +87 -0
  83. package/dist/react/engine/settings.d.ts +8 -0
  84. package/dist/react/engine/settings.js +68 -0
  85. package/dist/react/engine/store.d.ts +64 -0
  86. package/dist/react/engine/store.js +42 -0
  87. package/dist/react/engine/translate.d.ts +7 -0
  88. package/dist/react/engine/translate.js +33 -0
  89. package/dist/react/engine/types.d.ts +128 -0
  90. package/dist/react/engine/types.js +1 -0
  91. package/dist/react/index.d.ts +38 -0
  92. package/dist/react/index.js +39 -0
  93. package/dist/react/registries.d.ts +10 -0
  94. package/dist/react/registries.js +68 -0
  95. package/dist/react/registries.server.d.ts +2 -0
  96. package/dist/react/registries.server.js +98 -0
  97. package/dist/react/sections/404.d.ts +10 -0
  98. package/dist/react/sections/404.js +10 -0
  99. package/dist/react/sections/announcement-bar.d.ts +14 -0
  100. package/dist/react/sections/announcement-bar.js +84 -0
  101. package/dist/react/sections/article.d.ts +17 -0
  102. package/dist/react/sections/article.js +31 -0
  103. package/dist/react/sections/contact-form.d.ts +16 -0
  104. package/dist/react/sections/contact-form.js +8 -0
  105. package/dist/react/sections/custom-liquid.d.ts +14 -0
  106. package/dist/react/sections/custom-liquid.js +7 -0
  107. package/dist/react/sections/custom-section.d.ts +14 -0
  108. package/dist/react/sections/custom-section.js +9 -0
  109. package/dist/react/sections/featured-collection.d.ts +10 -0
  110. package/dist/react/sections/featured-collection.js +23 -0
  111. package/dist/react/sections/hero.d.ts +6 -0
  112. package/dist/react/sections/hero.js +35 -0
  113. package/dist/react/sections/image-with-text.d.ts +13 -0
  114. package/dist/react/sections/image-with-text.js +9 -0
  115. package/dist/react/sections/multicolumn.d.ts +15 -0
  116. package/dist/react/sections/multicolumn.js +8 -0
  117. package/dist/react/sections/page.d.ts +13 -0
  118. package/dist/react/sections/page.js +11 -0
  119. package/dist/react/sections/password.d.ts +15 -0
  120. package/dist/react/sections/password.js +9 -0
  121. package/dist/react/sections/rich-text.d.ts +12 -0
  122. package/dist/react/sections/rich-text.js +7 -0
  123. package/dist/react/sections/stories.d.ts +18 -0
  124. package/dist/react/sections/stories.js +10 -0
  125. package/dist/react/sections/video-bubble.d.ts +13 -0
  126. package/dist/react/sections/video-bubble.js +54 -0
  127. package/dist/react/sections/video-carousel.d.ts +19 -0
  128. package/dist/react/sections/video-carousel.js +9 -0
  129. package/dist/react/server.d.ts +7 -0
  130. package/dist/react/server.js +42 -0
  131. package/dist/schemas/bridge.d.ts +98 -0
  132. package/dist/schemas/bridge.js +50 -0
  133. package/dist/schemas/data.d.ts +54 -0
  134. package/dist/schemas/data.js +22 -0
  135. package/dist/schemas/index.d.ts +14 -0
  136. package/dist/schemas/index.js +14 -0
  137. package/dist/schemas/manifest.d.ts +63 -0
  138. package/dist/schemas/manifest.js +32 -0
  139. package/dist/schemas/schema.d.ts +159 -0
  140. package/dist/schemas/schema.js +129 -0
  141. package/dist/schemas/validate.d.ts +22 -0
  142. package/dist/schemas/validate.js +199 -0
  143. package/package.json +84 -0
  144. package/src/commerce/countdown.ts +38 -0
  145. package/src/commerce/events.ts +43 -0
  146. package/src/commerce/facets.ts +88 -0
  147. package/src/commerce/index.ts +11 -0
  148. package/src/commerce/money.ts +120 -0
  149. package/src/commerce/product.ts +177 -0
  150. package/src/editor/frame.ts +305 -0
  151. package/src/editor/host.ts +101 -0
  152. package/src/editor/index.ts +9 -0
  153. package/src/react/adapter.tsx +83 -0
  154. package/src/react/blocks/_box.tsx +37 -0
  155. package/src/react/blocks/_slide.tsx +96 -0
  156. package/src/react/blocks/_video-card.tsx +131 -0
  157. package/src/react/blocks/accordion.tsx +33 -0
  158. package/src/react/blocks/button.tsx +36 -0
  159. package/src/react/blocks/countdown.tsx +105 -0
  160. package/src/react/blocks/custom-liquid.tsx +21 -0
  161. package/src/react/blocks/group.tsx +32 -0
  162. package/src/react/blocks/image.tsx +29 -0
  163. package/src/react/blocks/message.tsx +28 -0
  164. package/src/react/blocks/quote.tsx +24 -0
  165. package/src/react/blocks/spacer.tsx +17 -0
  166. package/src/react/blocks/story.tsx +40 -0
  167. package/src/react/blocks/text.tsx +24 -0
  168. package/src/react/blocks/video.tsx +132 -0
  169. package/src/react/components/Facets.tsx +424 -0
  170. package/src/react/components/Icon.tsx +96 -0
  171. package/src/react/components/PaymentIcons.generated.ts +24 -0
  172. package/src/react/components/PaymentIcons.tsx +27 -0
  173. package/src/react/components/Price.tsx +72 -0
  174. package/src/react/components/ProductBadges.tsx +95 -0
  175. package/src/react/components/ProductCard.tsx +417 -0
  176. package/src/react/components/Rating.tsx +59 -0
  177. package/src/react/components/SwatchStyle.tsx +50 -0
  178. package/src/react/components/VideoModal.tsx +236 -0
  179. package/src/react/engine/CssVariables.tsx +142 -0
  180. package/src/react/engine/context.tsx +108 -0
  181. package/src/react/engine/fonts.ts +42 -0
  182. package/src/react/engine/images.tsx +58 -0
  183. package/src/react/engine/render.tsx +190 -0
  184. package/src/react/engine/settings.ts +89 -0
  185. package/src/react/engine/store.ts +103 -0
  186. package/src/react/engine/translate.ts +40 -0
  187. package/src/react/engine/types.ts +142 -0
  188. package/src/react/index.ts +78 -0
  189. package/src/react/registries.server.ts +110 -0
  190. package/src/react/registries.ts +80 -0
  191. package/src/react/sections/404.tsx +37 -0
  192. package/src/react/sections/announcement-bar.tsx +170 -0
  193. package/src/react/sections/article.tsx +97 -0
  194. package/src/react/sections/contact-form.tsx +104 -0
  195. package/src/react/sections/custom-liquid.tsx +39 -0
  196. package/src/react/sections/custom-section.tsx +44 -0
  197. package/src/react/sections/featured-collection.tsx +89 -0
  198. package/src/react/sections/hero.tsx +84 -0
  199. package/src/react/sections/image-with-text.tsx +61 -0
  200. package/src/react/sections/multicolumn.tsx +46 -0
  201. package/src/react/sections/page.tsx +43 -0
  202. package/src/react/sections/password.tsx +60 -0
  203. package/src/react/sections/rich-text.tsx +37 -0
  204. package/src/react/sections/stories.tsx +51 -0
  205. package/src/react/sections/video-bubble.tsx +145 -0
  206. package/src/react/sections/video-carousel.tsx +57 -0
  207. package/src/react/server.ts +60 -0
  208. package/src/schemas/bridge.ts +143 -0
  209. package/src/schemas/data.ts +72 -0
  210. package/src/schemas/index.ts +14 -0
  211. package/src/schemas/manifest.ts +85 -0
  212. package/src/schemas/schema.ts +287 -0
  213. package/src/schemas/validate.ts +302 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Frame side of the canvas-editor bridge: runs inside the theme
3
+ * preview when it is iframed by the Zalify editor. Owns everything
4
+ * that needs same-document DOM access — hit-testing clicks/hovers to
5
+ * `data-z-path` nodes, selection highlight, rect reporting, height
6
+ * sync — and hands app-specific concerns (template hot-apply, device
7
+ * emulation) to callbacks.
8
+ *
9
+ * Editor mode neutralizes the page's own interactivity: clicks,
10
+ * double/middle clicks, form submits, and context menus are captured
11
+ * at the document root and suppressed, so selecting a node never
12
+ * navigates a link, opens a drawer, or submits a form — a click is
13
+ * only ever a selection. (Hover stays live for highlight tracking.)
14
+ *
15
+ * Security: the first `bridge:init` pins the editor origin; every
16
+ * later message must match it, and nothing but `bridge:ready` is ever
17
+ * posted before the pin.
18
+ */
19
+ import { type Device, type SectionGroupData, type SettingsBag, type TemplateData, type ThemeEditorManifest } from '../schemas/index.ts';
20
+ export declare const DATA_HOVERED_ATTR = "data-z-hovered";
21
+ export interface FrameBridgeOptions {
22
+ /** Template currently rendered ("index", "product", …). */
23
+ templateName: string;
24
+ /** Content hash of the theme data the frame booted with. */
25
+ hash: string;
26
+ /** Serve the editor manifest (static import or dynamic build). */
27
+ getManifest: () => ThemeEditorManifest | Promise<ThemeEditorManifest>;
28
+ /**
29
+ * Hot-apply an updated template in memory (optimistic edit). Return
30
+ * false (or throw) if the app cannot apply without a reload.
31
+ */
32
+ applyTemplate?: (payload: {
33
+ templateName: string;
34
+ template: TemplateData;
35
+ groups?: Record<string, SectionGroupData>;
36
+ settingsData?: SettingsBag;
37
+ }) => boolean | Promise<boolean>;
38
+ onDeviceChange?: (device: Device) => void;
39
+ /** Overrides for tests / non-browser hosts. */
40
+ window?: Window & typeof globalThis;
41
+ }
42
+ /** True when this document should mount the editor bridge. */
43
+ export declare function isEditorMode(win?: Window): boolean;
44
+ export interface FrameBridgeController {
45
+ unmount: () => void;
46
+ /** Report that a persisted write round-tripped (HMR applied `hash`). */
47
+ notifySynced: (hash: string) => void;
48
+ /** Report an in-preview navigation to another template. */
49
+ notifyNavigation: (templateName: string, url: string) => void;
50
+ /** Re-send every block's rect (call after app-driven re-renders). */
51
+ reportRects: () => void;
52
+ }
53
+ /**
54
+ * Mount the frame bridge. Returns a controller with an `unmount`
55
+ * function. Call only when `isEditorMode()` is true.
56
+ */
57
+ export declare function mountFrameBridge(options: FrameBridgeOptions): FrameBridgeController;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Frame side of the canvas-editor bridge: runs inside the theme
3
+ * preview when it is iframed by the Zalify editor. Owns everything
4
+ * that needs same-document DOM access — hit-testing clicks/hovers to
5
+ * `data-z-path` nodes, selection highlight, rect reporting, height
6
+ * sync — and hands app-specific concerns (template hot-apply, device
7
+ * emulation) to callbacks.
8
+ *
9
+ * Editor mode neutralizes the page's own interactivity: clicks,
10
+ * double/middle clicks, form submits, and context menus are captured
11
+ * at the document root and suppressed, so selecting a node never
12
+ * navigates a link, opens a drawer, or submits a form — a click is
13
+ * only ever a selection. (Hover stays live for highlight tracking.)
14
+ *
15
+ * Security: the first `bridge:init` pins the editor origin; every
16
+ * later message must match it, and nothing but `bridge:ready` is ever
17
+ * posted before the pin.
18
+ */
19
+ import { BRIDGE_NAMESPACE, CONTRACT_VERSION, DATA_PATH_ATTR, DATA_SELECTED_ATTR, EDITOR_MODE_PARAM, isBridgeMessage, isCompatibleVersion, } from "../schemas/index.js";
20
+ export const DATA_HOVERED_ATTR = 'data-z-hovered';
21
+ /** True when this document should mount the editor bridge. */
22
+ export function isEditorMode(win = window) {
23
+ if (win.self === win.top)
24
+ return false;
25
+ try {
26
+ return (new URL(win.location.href).searchParams.get(EDITOR_MODE_PARAM) === '1');
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function rectOf(element) {
33
+ const rect = element.getBoundingClientRect();
34
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
35
+ }
36
+ function pathNodeOf(target) {
37
+ if (!(target instanceof Element))
38
+ return null;
39
+ return target.closest(`[${DATA_PATH_ATTR}]`);
40
+ }
41
+ /**
42
+ * Mount the frame bridge. Returns a controller with an `unmount`
43
+ * function. Call only when `isEditorMode()` is true.
44
+ */
45
+ export function mountFrameBridge(options) {
46
+ const win = options.window ?? window;
47
+ const doc = win.document;
48
+ let editorOrigin = null;
49
+ let lastHeight = 0;
50
+ const post = (message) => {
51
+ // bridge:ready is the only pre-pin message; everything else waits.
52
+ const target = editorOrigin ?? '*';
53
+ if (!editorOrigin && message.type !== 'bridge:ready')
54
+ return;
55
+ win.parent.postMessage({ z: BRIDGE_NAMESPACE, v: CONTRACT_VERSION, ...message }, target);
56
+ };
57
+ const setUnique = (attr, path) => {
58
+ for (const node of doc.querySelectorAll(`[${attr}]`)) {
59
+ node.removeAttribute(attr);
60
+ }
61
+ if (path === null)
62
+ return;
63
+ const selector = `[${DATA_PATH_ATTR}="${CSS.escape(path)}"]`;
64
+ doc.querySelector(selector)?.setAttribute(attr, '1');
65
+ };
66
+ const reportRects = () => {
67
+ const rects = [...doc.querySelectorAll(`[${DATA_PATH_ATTR}]`)].map((node) => ({
68
+ path: node.getAttribute(DATA_PATH_ATTR),
69
+ rect: rectOf(node),
70
+ }));
71
+ post({ type: 'block:rects', payload: { rects } });
72
+ };
73
+ const handleMessage = async (event) => {
74
+ const data = event.data;
75
+ if (!isBridgeMessage(data))
76
+ return;
77
+ if (editorOrigin && event.origin !== editorOrigin)
78
+ return;
79
+ if (!isCompatibleVersion(CONTRACT_VERSION, data.v)) {
80
+ post({
81
+ type: 'bridge:error',
82
+ payload: {
83
+ code: 'version-mismatch',
84
+ message: `frame speaks ${CONTRACT_VERSION}, host sent ${data.v}`,
85
+ },
86
+ });
87
+ return;
88
+ }
89
+ const message = data;
90
+ switch (message.type) {
91
+ case 'bridge:init': {
92
+ if (!editorOrigin) {
93
+ if (event.origin !== message.payload.editorOrigin)
94
+ return;
95
+ editorOrigin = event.origin;
96
+ }
97
+ options.onDeviceChange?.(message.payload.device);
98
+ setUnique(DATA_SELECTED_ATTR, message.payload.selectedPath);
99
+ break;
100
+ }
101
+ case 'block:select':
102
+ setUnique(DATA_SELECTED_ATTR, message.payload.path);
103
+ // Rects let the host scroll its full-height canvas to the
104
+ // selection (the frame itself has no scrollbar in that mode).
105
+ reportRects();
106
+ break;
107
+ case 'block:hover':
108
+ setUnique(DATA_HOVERED_ATTR, message.payload.path);
109
+ break;
110
+ case 'block:scroll-to': {
111
+ const selector = `[${DATA_PATH_ATTR}="${CSS.escape(message.payload.path)}"]`;
112
+ doc
113
+ .querySelector(selector)
114
+ ?.scrollIntoView({ behavior: 'smooth', block: 'center' });
115
+ break;
116
+ }
117
+ case 'template:apply': {
118
+ try {
119
+ const applied = (await options.applyTemplate?.(message.payload)) ?? false;
120
+ if (!applied) {
121
+ post({
122
+ type: 'bridge:error',
123
+ payload: {
124
+ code: 'apply-unsupported',
125
+ message: 'frame cannot hot-apply templates; wait for the file write',
126
+ },
127
+ });
128
+ }
129
+ else {
130
+ reportRects();
131
+ }
132
+ }
133
+ catch (error) {
134
+ post({
135
+ type: 'bridge:error',
136
+ payload: {
137
+ code: 'apply-failed',
138
+ message: error instanceof Error ? error.message : String(error),
139
+ },
140
+ });
141
+ }
142
+ break;
143
+ }
144
+ case 'device:set':
145
+ options.onDeviceChange?.(message.payload.device);
146
+ break;
147
+ case 'manifest:request': {
148
+ const manifest = await options.getManifest();
149
+ post({ type: 'manifest:response', payload: { manifest } });
150
+ break;
151
+ }
152
+ }
153
+ };
154
+ const handleClick = (event) => {
155
+ const node = pathNodeOf(event.target);
156
+ // Editor mode owns clicks: no navigation, no dialogs.
157
+ event.preventDefault();
158
+ event.stopPropagation();
159
+ if (!node)
160
+ return;
161
+ const path = node.getAttribute(DATA_PATH_ATTR);
162
+ setUnique(DATA_SELECTED_ATTR, path);
163
+ post({ type: 'block:clicked', payload: { path, rect: rectOf(node) } });
164
+ };
165
+ /** Suppress the page's own interactivity (capture phase, doc root). */
166
+ const suppress = (event) => {
167
+ event.preventDefault();
168
+ event.stopPropagation();
169
+ };
170
+ const SUPPRESSED_EVENTS = [
171
+ 'dblclick',
172
+ 'auxclick',
173
+ 'submit',
174
+ 'contextmenu',
175
+ ];
176
+ const handleMouseMove = (() => {
177
+ let lastPath = null;
178
+ return (event) => {
179
+ const node = pathNodeOf(event.target);
180
+ const path = node?.getAttribute(DATA_PATH_ATTR) ?? null;
181
+ if (path === lastPath)
182
+ return;
183
+ lastPath = path;
184
+ setUnique(DATA_HOVERED_ATTR, path);
185
+ post({
186
+ type: 'block:hovered',
187
+ payload: { path, rect: node ? rectOf(node) : undefined },
188
+ });
189
+ };
190
+ })();
191
+ const measure = () => {
192
+ // documentElement.scrollHeight never drops below the viewport, so a
193
+ // full-height host iframe would ratchet upward forever; the body's
194
+ // border box tracks actual content in both directions.
195
+ const height = Math.ceil(doc.body?.getBoundingClientRect().height ??
196
+ doc.documentElement.scrollHeight);
197
+ if (height === lastHeight || height <= 0)
198
+ return;
199
+ lastHeight = height;
200
+ post({ type: 'height:changed', payload: { height } });
201
+ };
202
+ const resizeObserver = new win.ResizeObserver(measure);
203
+ resizeObserver.observe(doc.documentElement);
204
+ if (doc.body)
205
+ resizeObserver.observe(doc.body);
206
+ win.addEventListener('message', handleMessage);
207
+ doc.addEventListener('click', handleClick, true);
208
+ doc.addEventListener('mousemove', handleMouseMove, true);
209
+ for (const type of SUPPRESSED_EVENTS) {
210
+ doc.addEventListener(type, suppress, true);
211
+ }
212
+ post({
213
+ type: 'bridge:ready',
214
+ payload: {
215
+ contractVersion: CONTRACT_VERSION,
216
+ templateName: options.templateName,
217
+ hash: options.hash,
218
+ },
219
+ });
220
+ measure();
221
+ return {
222
+ unmount: () => {
223
+ win.removeEventListener('message', handleMessage);
224
+ doc.removeEventListener('click', handleClick, true);
225
+ doc.removeEventListener('mousemove', handleMouseMove, true);
226
+ for (const type of SUPPRESSED_EVENTS) {
227
+ doc.removeEventListener(type, suppress, true);
228
+ }
229
+ resizeObserver.disconnect();
230
+ setUnique(DATA_SELECTED_ATTR, null);
231
+ setUnique(DATA_HOVERED_ATTR, null);
232
+ },
233
+ notifySynced: (hash) => {
234
+ post({ type: 'template:synced', payload: { hash } });
235
+ },
236
+ notifyNavigation: (templateName, url) => {
237
+ post({ type: 'navigation', payload: { templateName, url } });
238
+ },
239
+ reportRects,
240
+ };
241
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Host side of the canvas-editor bridge: drives a theme preview iframe
3
+ * from the Zalify editor. Owns the handshake (pin the frame origin,
4
+ * answer `bridge:ready` with `bridge:init`), re-handshakes transparently
5
+ * when the iframe reloads (sandbox HMR hard-refresh, crash recovery),
6
+ * and exposes typed send/subscribe surfaces.
7
+ */
8
+ import { type Device, type FrameMessage, type HostMessage } from '../schemas/index.ts';
9
+ export interface HostBridgeOptions {
10
+ iframe: HTMLIFrameElement;
11
+ /** Origin the preview is served from (the sandbox preview URL). */
12
+ frameOrigin: string;
13
+ device?: Device;
14
+ /** Currently selected path, replayed on every (re)handshake. */
15
+ getSelectedPath?: () => string | null;
16
+ onMessage?: (message: FrameMessage) => void;
17
+ /** Called when the frame speaks an incompatible contract version. */
18
+ onIncompatible?: (frameVersion: string) => void;
19
+ /** Overrides for tests / non-browser hosts. */
20
+ window?: Window;
21
+ }
22
+ export interface HostBridge {
23
+ /** Send a message; silently dropped until the frame is ready. */
24
+ send: (message: Omit<HostMessage, 'z' | 'v'>) => void;
25
+ /** True after the current frame document completed the handshake. */
26
+ isReady: () => boolean;
27
+ destroy: () => void;
28
+ }
29
+ /** Append the editor-mode marker to a preview URL. */
30
+ export declare function editorPreviewUrl(previewUrl: string): string;
31
+ export declare function createHostBridge(options: HostBridgeOptions): HostBridge;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Host side of the canvas-editor bridge: drives a theme preview iframe
3
+ * from the Zalify editor. Owns the handshake (pin the frame origin,
4
+ * answer `bridge:ready` with `bridge:init`), re-handshakes transparently
5
+ * when the iframe reloads (sandbox HMR hard-refresh, crash recovery),
6
+ * and exposes typed send/subscribe surfaces.
7
+ */
8
+ import { BRIDGE_NAMESPACE, CONTRACT_VERSION, EDITOR_MODE_PARAM, isBridgeMessage, isCompatibleVersion, } from "../schemas/index.js";
9
+ /** Append the editor-mode marker to a preview URL. */
10
+ export function editorPreviewUrl(previewUrl) {
11
+ const url = new URL(previewUrl);
12
+ url.searchParams.set(EDITOR_MODE_PARAM, '1');
13
+ return url.toString();
14
+ }
15
+ export function createHostBridge(options) {
16
+ const win = options.window ?? window;
17
+ let ready = false;
18
+ let destroyed = false;
19
+ const post = (message) => {
20
+ if (destroyed)
21
+ return;
22
+ options.iframe.contentWindow?.postMessage({ z: BRIDGE_NAMESPACE, v: CONTRACT_VERSION, ...message }, options.frameOrigin);
23
+ };
24
+ const handleMessage = (event) => {
25
+ if (destroyed)
26
+ return;
27
+ if (event.origin !== options.frameOrigin)
28
+ return;
29
+ if (event.source !== options.iframe.contentWindow)
30
+ return;
31
+ const data = event.data;
32
+ if (!isBridgeMessage(data))
33
+ return;
34
+ if (!isCompatibleVersion(CONTRACT_VERSION, data.v)) {
35
+ ready = false;
36
+ options.onIncompatible?.(data.v);
37
+ return;
38
+ }
39
+ const message = data;
40
+ if (message.type === 'bridge:ready') {
41
+ // First load and every iframe reload land here: (re)init.
42
+ ready = true;
43
+ post({
44
+ type: 'bridge:init',
45
+ payload: {
46
+ editorOrigin: win.location.origin,
47
+ device: options.device ?? 'desktop',
48
+ selectedPath: options.getSelectedPath?.() ?? null,
49
+ },
50
+ });
51
+ }
52
+ options.onMessage?.(message);
53
+ };
54
+ win.addEventListener('message', handleMessage);
55
+ return {
56
+ send: (message) => {
57
+ if (ready)
58
+ post(message);
59
+ },
60
+ isReady: () => ready,
61
+ destroy: () => {
62
+ destroyed = true;
63
+ ready = false;
64
+ win.removeEventListener('message', handleMessage);
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zalify/storefront-kit/editor — both halves of the canvas-editor
3
+ * bridge. Protocol types come from @zalify/storefront-kit/schemas.
4
+ *
5
+ * frame.ts — mounts inside the theme preview (sandbox side)
6
+ * host.ts — drives the preview iframe (editor side)
7
+ */
8
+ export * from './frame.ts';
9
+ export * from './host.ts';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zalify/storefront-kit/editor — both halves of the canvas-editor
3
+ * bridge. Protocol types come from @zalify/storefront-kit/schemas.
4
+ *
5
+ * frame.ts — mounts inside the theme preview (sandbox side)
6
+ * host.ts — drives the preview iframe (editor side)
7
+ */
8
+ export * from "./frame.js";
9
+ export * from "./host.js";
@@ -0,0 +1,49 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ /** Loose Link props — hosts map them onto their router's Link. */
3
+ export interface AdapterLinkProps {
4
+ to: string;
5
+ className?: string;
6
+ children?: ReactNode;
7
+ /** Hint to prefetch on intent where the router supports it. */
8
+ prefetch?: boolean;
9
+ replace?: boolean;
10
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
11
+ 'aria-label'?: string;
12
+ [key: string]: unknown;
13
+ }
14
+ export interface CartLineInput {
15
+ merchandiseId: string;
16
+ quantity: number;
17
+ }
18
+ export interface CartAddFormProps {
19
+ lines: CartLineInput[];
20
+ /** Render prop receives the submission state for button disabling. */
21
+ children: (state: {
22
+ submitting: boolean;
23
+ }) => ReactNode;
24
+ }
25
+ export interface ThemeAdapter {
26
+ /** Client-side navigation link. */
27
+ Link: ComponentType<AdapterLinkProps>;
28
+ /** Hook returning an imperative navigate(url, opts) function. */
29
+ useNavigate: () => (url: string, opts?: {
30
+ replace?: boolean;
31
+ preventScrollReset?: boolean;
32
+ }) => void;
33
+ /** Hook returning the current URL's search params (read-only). */
34
+ useSearchParams: () => URLSearchParams;
35
+ /** Hook returning the current pathname. */
36
+ usePathname: () => string;
37
+ /**
38
+ * A form that adds lines to the cart on submit. Hosts wire it to
39
+ * their cart mutation (Hydrogen CartForm, Next server action…) and
40
+ * are responsible for emitting commerce-core's 'cart:updated' /
41
+ * 'cart:error' events when the mutation settles.
42
+ */
43
+ CartAddForm: ComponentType<CartAddFormProps>;
44
+ }
45
+ export declare function AdapterProvider({ adapter, children, }: {
46
+ adapter: ThemeAdapter;
47
+ children: ReactNode;
48
+ }): import("react").JSX.Element;
49
+ export declare function useAdapter(): ThemeAdapter;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The framework seam. ui-react components never import a router or
4
+ * commerce SDK — the host app injects these primitives once:
5
+ *
6
+ * <AdapterProvider adapter={hydrogenAdapter}>…</AdapterProvider>
7
+ *
8
+ * Keep this surface deliberately small: only what shared components
9
+ * actually need. Deeply framework-coupled features (cart page, variant
10
+ * system, predictive search) live in each app, not behind this seam.
11
+ */
12
+ import { createContext, useContext } from 'react';
13
+ const AdapterContext = createContext(null);
14
+ export function AdapterProvider({ adapter, children, }) {
15
+ return (_jsx(AdapterContext.Provider, { value: adapter, children: children }));
16
+ }
17
+ export function useAdapter() {
18
+ const adapter = useContext(AdapterContext);
19
+ if (!adapter) {
20
+ throw new Error('[storefront-kit] No ThemeAdapter provided. Wrap the app in <AdapterProvider adapter={…}>.');
21
+ }
22
+ return adapter;
23
+ }
@@ -0,0 +1,10 @@
1
+ /** Port of blocks/_box.liquid — hero content box. CSS: blocks-_box.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface BoxSettings {
4
+ position_x?: 'start' | 'center' | 'end';
5
+ position_y?: 'start' | 'center' | 'end';
6
+ color_scheme?: string;
7
+ backdrop?: boolean;
8
+ }
9
+ export default function BoxBlock({ settings, children }: BlockProps<BoxSettings>): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function BoxBlock({ settings, children }) {
3
+ const { position_x: positionX = 'start', position_y: positionY = 'center', color_scheme: colorScheme = 'scheme-1', backdrop = false, } = settings;
4
+ const className = [
5
+ 'hero__box',
6
+ `hero__box--x-${positionX}`,
7
+ `hero__box--y-${positionY}`,
8
+ `color-${colorScheme}`,
9
+ backdrop ? 'hero__box--backdrop' : '',
10
+ ]
11
+ .filter(Boolean)
12
+ .join(' ');
13
+ return (_jsx("div", { className: className,
14
+ // Inline transparent: the scheme class paints via an unlayered rule
15
+ // that outranks any @layer override (mirror of _box.liquid).
16
+ style: backdrop ? undefined : { backgroundColor: 'transparent' }, children: children }));
17
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Port of blocks/_slide.liquid — hero slide: full-bleed media canvas.
3
+ * Content lives in nested _box blocks. CSS: blocks-_slide.css
4
+ */
5
+ import type { BlockProps } from '../engine/types';
6
+ interface SlideSettings {
7
+ image?: string;
8
+ mobile_image?: string;
9
+ video?: {
10
+ sources?: Array<{
11
+ url: string;
12
+ format: string;
13
+ height: number;
14
+ mime_type: string;
15
+ }>;
16
+ preview_image?: {
17
+ src?: string;
18
+ };
19
+ };
20
+ mobile_video?: SlideSettings['video'];
21
+ link?: string;
22
+ }
23
+ export default function SlideBlock({ settings, index, children, }: BlockProps<SlideSettings>): import("react").JSX.Element;
24
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
+ function bestMp4(video) {
4
+ const mp4s = (video?.sources ?? []).filter((s) => s.format === 'mp4');
5
+ return mp4s.sort((a, b) => a.height - b.height).at(-1);
6
+ }
7
+ export default function SlideBlock({ settings, index, children, }) {
8
+ // First slide is the LCP candidate: eager + high priority.
9
+ const isFirst = index === 0;
10
+ const image = settingImageUrl(settings.image);
11
+ const mobileImage = settingImageUrl(settings.mobile_image);
12
+ const desktopMp4 = bestMp4(settings.video);
13
+ const mobileMp4 = bestMp4(settings.mobile_video);
14
+ // Poster paints video slides before any video bytes arrive
15
+ const poster = image ?? mobileImage ?? settingImageUrl(settings.video?.preview_image?.src);
16
+ return (_jsxs("div", { className: "hero__slide", children: [desktopMp4 || mobileMp4 ? (_jsxs("video", { className: "hero__media", autoPlay: true, loop: true, muted: true, playsInline: true, preload: "metadata", poster: poster ? imageUrl(poster, 1800) : undefined, children: [mobileMp4 && (_jsx("source", { media: "(max-width: 47.9375rem)", src: mobileMp4.url, type: mobileMp4.mime_type })), desktopMp4 && (_jsx("source", { src: desktopMp4.url, type: desktopMp4.mime_type }))] })) : image ? (_jsxs("picture", { children: [mobileImage && (_jsx("source", { media: "(max-width: 47.9375rem)", srcSet: [400, 750, 1100, 1500]
17
+ .map((w) => `${imageUrl(mobileImage, w)} ${w}w`)
18
+ .join(', '), sizes: "100vw" })), _jsx("img", { className: "hero__media", src: imageUrl(image, 3000), srcSet: [750, 1100, 1500, 2200, 3000]
19
+ .map((w) => `${imageUrl(image, w)} ${w}w`)
20
+ .join(', '), sizes: "100vw", loading: isFirst ? 'eager' : 'lazy', fetchPriority: isFirst ? 'high' : 'auto', alt: "" })] })) : (_jsx(PlaceholderSvg, { className: "hero__media hero__media--placeholder" })), settings.link ? (_jsx("a", { className: "hero__media-link", href: settings.link, "aria-label": "View" })) : null, _jsx("div", { className: "hero__content", children: children })] }));
21
+ }
@@ -0,0 +1,11 @@
1
+ import type { BlockProps } from '../engine/types';
2
+ import { type ThemeVideo } from '../components/VideoModal';
3
+ interface VideoCardSettings {
4
+ preview_video?: ThemeVideo;
5
+ video?: ThemeVideo;
6
+ image?: string;
7
+ product?: unknown;
8
+ label?: string;
9
+ }
10
+ export default function VideoCardBlock({ settings, }: BlockProps<VideoCardSettings>): import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Port of blocks/_video-card.liquid — video carousel card: a muted
4
+ * preview loop that expands into the shared video modal (main video
5
+ * with sound + optional product). Private block of
6
+ * sections/video-carousel.liquid. Behavior from the <video-carousel>
7
+ * island in src/entries/video.ts.
8
+ * CSS: app/styles/components/blocks-_video-card.css
9
+ */
10
+ import { useState } from 'react';
11
+ import { t } from '../engine/translate';
12
+ import { Icon } from '../components/Icon';
13
+ import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
14
+ import { formatMoney } from "../../commerce/index.js";
15
+ import { mp4Sources, productImageUrl, resolveVideoProduct, useViewportPlayback, VideoModal, videoPreviewImage, } from '../components/VideoModal';
16
+ /**
17
+ * Cards render ~300px wide: the smallest rendition from 480p up is
18
+ * plenty and keeps a many-card strip light (mirror of the Liquid loop).
19
+ */
20
+ function previewMp4For(video) {
21
+ const sources = mp4Sources(video);
22
+ return sources.find((s) => (s.height ?? 0) >= 480) ?? sources.at(-1);
23
+ }
24
+ export default function VideoCardBlock({ settings, }) {
25
+ const preview = settings.preview_video;
26
+ const mainVideo = settings.video ?? preview;
27
+ const poster = settingImageUrl(settings.image) ??
28
+ videoPreviewImage(preview) ??
29
+ videoPreviewImage(mainVideo);
30
+ const previewMp4 = previewMp4For(preview);
31
+ const product = resolveVideoProduct(settings.product);
32
+ const productImage = productImageUrl(product?.featured_image);
33
+ const label = settings.label;
34
+ const previewRef = useViewportPlayback();
35
+ const [open, setOpen] = useState(false);
36
+ const triggerLabel = t('general.play_video') +
37
+ (product?.title ? `: ${product.title}` : label ? `: ${label}` : '');
38
+ return (_jsxs("div", { className: "video-card", children: [_jsxs("button", { type: "button", className: "video-card__trigger", onClick: () => setOpen(true), "aria-label": triggerLabel, children: [previewMp4 ? (_jsx("video", { ref: previewRef, className: "video-card__media", autoPlay: true, loop: true, muted: true, playsInline: true, preload: "metadata", "data-video-preview": "", poster: poster ? imageUrl(poster, 800) : undefined, children: _jsx("source", { src: previewMp4.url, type: previewMp4.mime_type }) })) : poster ? (_jsx("img", { className: "video-card__media", src: imageUrl(poster, 800), loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "video-card__media video-card__media--placeholder" })), _jsx("span", { className: "video-card__play", children: _jsx(Icon, { name: "icon-play" }) })] }), product ? (_jsxs("a", { className: "video-card__product", href: product.url, children: [productImage ? (_jsx("img", { className: "video-card__product-image", src: imageUrl(productImage, 96), loading: "lazy", alt: "" })) : null, _jsxs("span", { className: "video-card__product-info", children: [_jsx("span", { className: "video-card__product-title", children: product.title }), _jsx("span", { className: "video-card__product-price", children: formatMoney(product.price) })] })] })) : label ? (_jsx("span", { className: "video-card__label", children: label })) : null, _jsx(VideoModal, { open: open, onClose: () => setOpen(false), video: mainVideo, poster: poster, product: product, label: label })] }));
39
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Port of blocks/accordion.liquid — a <details>-based accordion with a
3
+ * heading and rich-text content.
4
+ * CSS: app/styles/components/blocks-accordion.css
5
+ */
6
+ import type { BlockProps } from '../engine/types';
7
+ interface AccordionSettings {
8
+ heading?: string;
9
+ content?: string;
10
+ }
11
+ export default function AccordionBlock({ settings, }: BlockProps<AccordionSettings>): import("react").JSX.Element;
12
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export default function AccordionBlock({ settings, }) {
3
+ const { heading = 'Details', content = '<p>Share details about ingredients, sizing, shipping, or care.</p>', } = settings;
4
+ return (_jsxs("details", { className: "accordion", children: [_jsxs("summary", { className: "accordion__summary", children: [_jsx("span", { children: heading }), _jsx("span", { className: "accordion__icon", "aria-hidden": "true" })] }), _jsx("div", { className: "accordion__content", dangerouslySetInnerHTML: { __html: content } })] }));
5
+ }
@@ -0,0 +1,10 @@
1
+ /** Port of blocks/button.liquid (button styles live in critical.css). */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface ButtonSettings {
4
+ label?: string;
5
+ link?: string;
6
+ style?: '' | 'button--secondary';
7
+ size?: '' | 'button--sm' | 'button--lg';
8
+ }
9
+ export default function ButtonBlock({ settings }: BlockProps<ButtonSettings>): import("react").JSX.Element;
10
+ export {};