@zalify/storefront-kit 0.1.12 → 0.2.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 (65) hide show
  1. package/dist/commerce/disclosure-motion.d.ts +2 -0
  2. package/dist/commerce/disclosure-motion.js +69 -0
  3. package/dist/commerce/image-url.d.ts +2 -0
  4. package/dist/commerce/image-url.js +15 -0
  5. package/dist/commerce/index.d.ts +1 -0
  6. package/dist/commerce/index.js +1 -0
  7. package/dist/editor/bootstrap.d.ts +15 -0
  8. package/dist/editor/bootstrap.js +42 -0
  9. package/dist/editor/draft.d.ts +17 -0
  10. package/dist/editor/draft.js +32 -0
  11. package/dist/editor/frame.d.ts +2 -0
  12. package/dist/editor/frame.js +7 -4
  13. package/dist/react/adapter.d.ts +3 -0
  14. package/dist/react/adapter.js +1 -0
  15. package/dist/react/blocks/_slide.js +2 -1
  16. package/dist/react/blocks/_video-card.js +2 -1
  17. package/dist/react/blocks/image.js +2 -1
  18. package/dist/react/blocks/story.js +2 -1
  19. package/dist/react/blocks/video.js +2 -1
  20. package/dist/react/components/ProductCard.js +4 -3
  21. package/dist/react/components/StoreImage.d.ts +2 -0
  22. package/dist/react/components/StoreImage.js +6 -0
  23. package/dist/react/components/VideoModal.js +2 -1
  24. package/dist/react/engine/CssVariables.js +5 -4
  25. package/dist/react/engine/images.d.ts +1 -1
  26. package/dist/react/engine/images.js +2 -6
  27. package/dist/react/engine/render.js +3 -1
  28. package/dist/react/engine/store.d.ts +6 -0
  29. package/dist/react/engine/store.js +34 -3
  30. package/dist/react/index.d.ts +1 -0
  31. package/dist/react/index.js +1 -0
  32. package/dist/react/sections/article.js +2 -1
  33. package/dist/react/sections/custom-section.js +2 -1
  34. package/dist/react/sections/image-with-text.js +2 -1
  35. package/dist/react/sections/video-bubble.js +2 -1
  36. package/dist/react/useEditorTemplate.d.ts +15 -0
  37. package/dist/react/useEditorTemplate.js +114 -0
  38. package/dist/schemas/bridge.d.ts +1 -0
  39. package/package.json +1 -1
  40. package/src/commerce/disclosure-motion.ts +60 -0
  41. package/src/commerce/image-url.ts +11 -0
  42. package/src/commerce/index.ts +2 -0
  43. package/src/editor/bootstrap.ts +61 -0
  44. package/src/editor/draft.ts +54 -0
  45. package/src/editor/frame.ts +7 -3
  46. package/src/react/adapter.tsx +4 -0
  47. package/src/react/blocks/_slide.tsx +2 -1
  48. package/src/react/blocks/_video-card.tsx +3 -2
  49. package/src/react/blocks/image.tsx +2 -1
  50. package/src/react/blocks/story.tsx +2 -1
  51. package/src/react/blocks/video.tsx +2 -1
  52. package/src/react/components/ProductCard.tsx +6 -5
  53. package/src/react/components/StoreImage.tsx +6 -0
  54. package/src/react/components/VideoModal.tsx +3 -2
  55. package/src/react/engine/CssVariables.tsx +34 -10
  56. package/src/react/engine/images.tsx +2 -5
  57. package/src/react/engine/render.tsx +14 -1
  58. package/src/react/engine/store.ts +43 -4
  59. package/src/react/index.ts +2 -0
  60. package/src/react/sections/article.tsx +2 -1
  61. package/src/react/sections/custom-section.tsx +2 -1
  62. package/src/react/sections/image-with-text.tsx +2 -1
  63. package/src/react/sections/video-bubble.tsx +2 -1
  64. package/src/react/useEditorTemplate.ts +136 -0
  65. package/src/schemas/bridge.ts +1 -0
@@ -0,0 +1,2 @@
1
+ /** Progressive native disclosure motion. Returns listener/animation cleanup. */
2
+ export declare function initDisclosureMotion(): () => void;
@@ -0,0 +1,69 @@
1
+ /** Progressive native disclosure motion. Returns listener/animation cleanup. */
2
+ export function initDisclosureMotion() {
3
+ const pointerMode = () => { document.documentElement.dataset.inputMode = 'pointer'; };
4
+ const keyboardMode = (event) => { if (event.key === 'Tab' || event.key.startsWith('Arrow'))
5
+ document.documentElement.dataset.inputMode = 'keyboard'; };
6
+ document.addEventListener('pointerdown', pointerMode, true);
7
+ document.addEventListener('keydown', keyboardMode, true);
8
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)');
9
+ const running = new Map();
10
+ const toggle = (details, expanded) => {
11
+ const from = details.getBoundingClientRect().height;
12
+ running.get(details)?.cleanup();
13
+ const summary = details.querySelector('summary');
14
+ if (!summary)
15
+ return;
16
+ const originalHeight = details.style.height;
17
+ const originalOverflow = details.style.overflow;
18
+ const name = details.getAttribute('name');
19
+ // Hold named siblings open only long enough to finish their closing motion.
20
+ if (!expanded && name)
21
+ details.removeAttribute('name');
22
+ details.toggleAttribute('data-disclosure-closing', !expanded);
23
+ details.open = true;
24
+ const to = expanded ? details.getBoundingClientRect().height : summary.getBoundingClientRect().height +
25
+ parseFloat(getComputedStyle(details).paddingTop || '0') + parseFloat(getComputedStyle(details).paddingBottom || '0') +
26
+ parseFloat(getComputedStyle(details).borderTopWidth || '0') + parseFloat(getComputedStyle(details).borderBottomWidth || '0');
27
+ details.style.overflow = 'hidden';
28
+ const animation = details.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: 280, easing: 'cubic-bezier(.22, 1, .36, 1)' });
29
+ const cleanup = () => {
30
+ animation.onfinish = null;
31
+ animation.cancel();
32
+ details.removeAttribute('data-disclosure-closing');
33
+ details.style.height = originalHeight;
34
+ details.style.overflow = originalOverflow;
35
+ if (name)
36
+ details.setAttribute('name', name);
37
+ running.delete(details);
38
+ };
39
+ running.set(details, { animation, expanded, cleanup });
40
+ animation.onfinish = () => { details.open = expanded; cleanup(); };
41
+ };
42
+ const onClick = (event) => {
43
+ if (event.defaultPrevented || reduced.matches || event.button !== 0)
44
+ return;
45
+ const target = event.target instanceof Element ? event.target : null;
46
+ const summary = target?.closest('summary');
47
+ const details = summary?.parentElement;
48
+ if (!(details instanceof HTMLDetailsElement) || !summary || getComputedStyle(summary).display === 'none')
49
+ return;
50
+ if (target?.closest('a, button, input, select, textarea'))
51
+ return;
52
+ event.preventDefault();
53
+ const expanded = !(running.get(details)?.expanded ?? details.open);
54
+ if (expanded && details.name) {
55
+ document.querySelectorAll('details[open]').forEach(peer => {
56
+ if (peer !== details && peer.name === details.name)
57
+ toggle(peer, false);
58
+ });
59
+ }
60
+ toggle(details, expanded);
61
+ };
62
+ const stop = () => { for (const [details, state] of [...running]) {
63
+ details.open = state.expanded;
64
+ state.cleanup();
65
+ } };
66
+ document.addEventListener('click', onClick);
67
+ reduced.addEventListener('change', stop);
68
+ return () => { document.removeEventListener('pointerdown', pointerMode, true); document.removeEventListener('keydown', keyboardMode, true); delete document.documentElement.dataset.inputMode; document.removeEventListener('click', onClick); reduced.removeEventListener('change', stop); stop(); };
69
+ }
@@ -0,0 +1,2 @@
1
+ /** Replace Shopify width parameters while preserving version hashes and crops. */
2
+ export declare function imageUrl(url: string, width?: number): string;
@@ -0,0 +1,15 @@
1
+ /** Replace Shopify width parameters while preserving version hashes and crops. */
2
+ export function imageUrl(url, width) {
3
+ if (!width || !url.includes('cdn.shopify.com'))
4
+ return url;
5
+ try {
6
+ const parsed = new URL(url.startsWith('//') ? `https:${url}` : url);
7
+ if (parsed.hostname !== 'cdn.shopify.com')
8
+ return url;
9
+ parsed.searchParams.set('width', String(width));
10
+ return parsed.toString();
11
+ }
12
+ catch {
13
+ return url;
14
+ }
15
+ }
@@ -10,3 +10,4 @@ export * from './countdown';
10
10
  export * from './facets';
11
11
  export * from './product';
12
12
  export * from './google-fonts';
13
+ export { initDisclosureMotion } from './disclosure-motion';
@@ -10,3 +10,4 @@ export * from './countdown';
10
10
  export * from './facets';
11
11
  export * from './product';
12
12
  export * from './google-fonts';
13
+ export { initDisclosureMotion } from './disclosure-motion';
@@ -0,0 +1,15 @@
1
+ import type { EditorBootstrap, PreviewContext } from '../schemas/bridge.ts';
2
+ import type { ThemeEditorManifest } from '../schemas/manifest.ts';
3
+ import type { DraftSchema } from './draft.ts';
4
+ export interface EditorDocuments {
5
+ templates: string;
6
+ groups: string;
7
+ settings: string;
8
+ }
9
+ /** Only list routes the app can actually render; never fabricate handles. */
10
+ export declare function createEditorBootstrap(options: {
11
+ schema: DraftSchema;
12
+ manifest: ThemeEditorManifest;
13
+ paths: EditorDocuments;
14
+ previews: Record<string, PreviewContext>;
15
+ }): EditorBootstrap;
@@ -0,0 +1,42 @@
1
+ /** Only list routes the app can actually render; never fabricate handles. */
2
+ export function createEditorBootstrap(options) {
3
+ const { schema, manifest, paths, previews } = options;
4
+ for (const path of [paths.templates, paths.groups, paths.settings]) {
5
+ if (!path ||
6
+ path.startsWith('/') ||
7
+ path.includes('\\') ||
8
+ path.split('/').some((p) => p === '..' || p === '.')) {
9
+ throw new Error('Editor write paths must be repository-relative');
10
+ }
11
+ }
12
+ const templates = {
13
+ ...schema.templates,
14
+ ...Object.fromEntries(Object.entries(schema.customerTemplates ?? {}).map(([name, data]) => [
15
+ `customers/${name}`,
16
+ data,
17
+ ])),
18
+ };
19
+ return {
20
+ revision: manifest.hash,
21
+ manifest,
22
+ templates: Object.entries(templates)
23
+ .filter(([name]) => previews[name])
24
+ .map(([name, data]) => ({
25
+ name,
26
+ data: structuredClone(data),
27
+ writePath: `${paths.templates}/${name}.json`,
28
+ preview: previews[name],
29
+ })),
30
+ groups: Object.entries(schema.sectionGroups).map(([name, data]) => ({
31
+ name,
32
+ data: structuredClone(data),
33
+ writePath: `${paths.groups}/${name}.json`,
34
+ })),
35
+ settings: {
36
+ writePath: paths.settings,
37
+ schema: manifest.settingsSchema,
38
+ resolvedData: structuredClone(schema.settingsData),
39
+ },
40
+ previewContexts: {},
41
+ };
42
+ }
@@ -0,0 +1,17 @@
1
+ import type { SectionGroupData, SettingsData, TemplateData } from '../schemas/data.ts';
2
+ export interface DraftSchema {
3
+ templates: Record<string, TemplateData>;
4
+ customerTemplates?: Record<string, TemplateData>;
5
+ sectionGroups: Record<string, SectionGroupData>;
6
+ settingsData: SettingsData;
7
+ }
8
+ export interface PreviewApply {
9
+ templateName: string;
10
+ template: TemplateData;
11
+ groups?: Record<string, SectionGroupData>;
12
+ settingsData?: SettingsData;
13
+ }
14
+ /** Immutable, per-preview data. Never writes into an installed source schema. */
15
+ export declare function createEditorDraft(source: DraftSchema): {
16
+ apply(payload: PreviewApply): DraftSchema;
17
+ };
@@ -0,0 +1,32 @@
1
+ /** Immutable, per-preview data. Never writes into an installed source schema. */
2
+ export function createEditorDraft(source) {
3
+ let current = structuredClone(source);
4
+ return {
5
+ apply(payload) {
6
+ const template = structuredClone(payload.template);
7
+ const customer = payload.templateName.startsWith('customers/');
8
+ current = {
9
+ ...current,
10
+ ...(customer
11
+ ? {
12
+ customerTemplates: {
13
+ ...current.customerTemplates,
14
+ [payload.templateName.slice(10)]: template,
15
+ },
16
+ }
17
+ : {
18
+ templates: {
19
+ ...current.templates,
20
+ [payload.templateName]: template,
21
+ },
22
+ }),
23
+ sectionGroups: {
24
+ ...current.sectionGroups,
25
+ ...structuredClone(payload.groups ?? {}),
26
+ },
27
+ settingsData: structuredClone(payload.settingsData ?? current.settingsData),
28
+ };
29
+ return current;
30
+ },
31
+ };
32
+ }
@@ -39,6 +39,8 @@ export interface FrameBridgeOptions {
39
39
  groups?: Record<string, SectionGroupData>;
40
40
  settingsData?: SettingsData;
41
41
  }) => boolean | Promise<boolean>;
42
+ /** Optional trusted editor origin, checked before the initial handshake. */
43
+ editorOrigin?: string;
42
44
  onDeviceChange?: (device: Device) => void;
43
45
  /** Overrides for tests / non-browser hosts. */
44
46
  window?: Window & typeof globalThis;
@@ -136,6 +136,10 @@ export function mountFrameBridge(options) {
136
136
  post({ type: 'block:rects', payload: { rects } });
137
137
  };
138
138
  const handleMessage = async (event) => {
139
+ if (event.source !== win.parent)
140
+ return;
141
+ if (options.editorOrigin && event.origin !== options.editorOrigin)
142
+ return;
139
143
  const data = event.data;
140
144
  if (!isBridgeMessage(data))
141
145
  return;
@@ -250,10 +254,8 @@ export function mountFrameBridge(options) {
250
254
  post({ type: 'block:clicked', payload: { path, rect: rectOf(node) } });
251
255
  };
252
256
  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
+ // Cross-origin iframe wheel events never reach the host canvas. Forward
258
+ // wheel gestures so the editor can pan its canvas.
257
259
  event.preventDefault();
258
260
  post({
259
261
  type: 'viewport:wheel',
@@ -261,6 +263,7 @@ export function mountFrameBridge(options) {
261
263
  deltaX: event.deltaX,
262
264
  deltaY: event.deltaY,
263
265
  ctrlKey: event.ctrlKey,
266
+ shiftKey: event.shiftKey,
264
267
  clientX: event.clientX,
265
268
  clientY: event.clientY,
266
269
  },
@@ -23,6 +23,8 @@ export interface CartAddFormProps {
23
23
  }) => ReactNode;
24
24
  }
25
25
  export interface ThemeAdapter {
26
+ /** Optional responsive image renderer; hosts without one retain native images. */
27
+ Image?: ComponentType<React.ImgHTMLAttributes<HTMLImageElement>>;
26
28
  /** Client-side navigation link. */
27
29
  Link: ComponentType<AdapterLinkProps>;
28
30
  /** Hook returning an imperative navigate(url, opts) function. */
@@ -47,3 +49,4 @@ export declare function AdapterProvider({ adapter, children, }: {
47
49
  children: ReactNode;
48
50
  }): import("react").JSX.Element;
49
51
  export declare function useAdapter(): ThemeAdapter;
52
+ export declare function useAdapterImage(): ComponentType<import("react").ImgHTMLAttributes<HTMLImageElement>> | undefined;
@@ -21,3 +21,4 @@ export function useAdapter() {
21
21
  }
22
22
  return adapter;
23
23
  }
24
+ export function useAdapterImage() { return useContext(AdapterContext)?.Image; }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
4
  function bestMp4(video) {
4
5
  const mp4s = (video?.sources ?? []).filter((s) => s.format === 'mp4');
@@ -15,7 +16,7 @@ export default function SlideBlock({ settings, index, children, }) {
15
16
  const poster = image ?? mobileImage ?? settingImageUrl(settings.video?.preview_image?.src);
16
17
  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
18
  .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
+ .join(', '), sizes: "100vw" })), _jsx(StoreImage, { className: "hero__media", src: imageUrl(image, 3000), srcSet: [750, 1100, 1500, 2200, 3000]
19
20
  .map((w) => `${imageUrl(image, w)} ${w}w`)
20
21
  .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
22
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  /**
3
4
  * Port of blocks/_video-card.liquid — video carousel card: a muted
4
5
  * preview loop that expands into the shared video modal (main video
@@ -35,5 +36,5 @@ export default function VideoCardBlock({ settings, }) {
35
36
  const [open, setOpen] = useState(false);
36
37
  const triggerLabel = t('general.play_video') +
37
38
  (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
+ 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(StoreImage, { 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(StoreImage, { 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
40
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  import { imageSrcSet, imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
4
  export default function ImageBlock({ settings }) {
4
5
  const { aspect = 'auto' } = settings;
5
6
  const src = settingImageUrl(settings.image);
6
- return (_jsx("div", { className: `image-block image-block--${aspect}`, children: src ? (_jsx("img", { className: "image-block__image", src: imageUrl(src, 1500), srcSet: imageSrcSet(src), sizes: "(min-width: 48rem) 50vw, 100vw", loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "image-block__image image-block__placeholder" })) }));
7
+ return (_jsx("div", { className: `image-block image-block--${aspect}`, children: src ? (_jsx(StoreImage, { className: "image-block__image", src: imageUrl(src, 1500), srcSet: imageSrcSet(src), sizes: "(min-width: 48rem) 50vw, 100vw", loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "image-block__image image-block__placeholder" })) }));
7
8
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
4
  export default function StoryBlock({ settings }) {
4
5
  const image = settingImageUrl(settings.image);
5
6
  const label = settings.label ?? '';
6
- return (_jsxs("a", { className: "story", role: "listitem", href: settings.link || undefined, children: [_jsx("span", { className: "story__media", children: image ? (_jsx("img", { className: "story__image", src: imageUrl(image, 480), loading: "lazy", alt: label })) : (_jsx(PlaceholderSvg, { className: "story__image story__image--placeholder" })) }), label ? _jsx("span", { className: "story__label", children: label }) : null] }));
7
+ return (_jsxs("a", { className: "story", role: "listitem", href: settings.link || undefined, children: [_jsx("span", { className: "story__media", children: image ? (_jsx(StoreImage, { className: "story__image", src: imageUrl(image, 480), loading: "lazy", alt: label })) : (_jsx(PlaceholderSvg, { className: "story__image story__image--placeholder" })) }), label ? _jsx("span", { className: "story__label", children: label }) : null] }));
7
8
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  /**
3
4
  * Port of blocks/video.liquid — general video block: a Shopify-hosted
4
5
  * file (ambient loop or click-to-play with native controls) or a
@@ -50,7 +51,7 @@ export default function VideoBlock({ settings }) {
50
51
  : null;
51
52
  return (_jsx("div", { className: `video-block video-block--${aspect}${frameClass}`, children: hostedMp4 ? (_jsx("video", { ref: autoplay ? previewRef : undefined, className: "video-block__media", playsInline: true, preload: "metadata", ...(autoplay
52
53
  ? { autoPlay: true, loop: true, muted: true, 'data-video-preview': '' }
53
- : { controls: true }), poster: poster ? imageUrl(poster, 1500) : undefined, children: _jsx("source", { src: hostedMp4.url, type: hostedMp4.mime_type }) })) : embedUrl ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "video-block__facade", onClick: () => setPlaying(true), "aria-label": t('general.play_video'), hidden: playing, children: [poster ? (_jsx("img", { className: "video-block__media", src: imageUrl(poster, 1500), srcSet: [400, 700, 1000, 1500]
54
+ : { controls: true }), poster: poster ? imageUrl(poster, 1500) : undefined, children: _jsx("source", { src: hostedMp4.url, type: hostedMp4.mime_type }) })) : embedUrl ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "video-block__facade", onClick: () => setPlaying(true), "aria-label": t('general.play_video'), hidden: playing, children: [poster ? (_jsx(StoreImage, { className: "video-block__media", src: imageUrl(poster, 1500), srcSet: [400, 700, 1000, 1500]
54
55
  .map((w) => `${imageUrl(poster, w)} ${w}w`)
55
56
  .join(', '), loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "video-block__media video-block__placeholder" })), _jsx("span", { className: "video-block__play", children: _jsx(Icon, { name: "icon-play" }) })] }), _jsx("iframe", { className: "video-block__media", src: playing ? embedUrl : undefined, title: t('general.play_video'), allow: "autoplay; fullscreen; encrypted-media; picture-in-picture", allowFullScreen: true, hidden: !playing })] })) : (_jsxs(_Fragment, { children: [_jsx(PlaceholderSvg, { className: "video-block__media video-block__placeholder" }), _jsx("span", { className: "video-block__play", children: _jsx(Icon, { name: "icon-play" }) })] })) }));
56
57
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from './StoreImage';
2
3
  /**
3
4
  * Port of snippets/product-card.liquid — a product card for grids:
4
5
  * square media (second image on hover), title, price, and badges
@@ -46,7 +47,7 @@ export function ProductCard({ product, loading = 'lazy' }) {
46
47
  ? (product.images?.nodes?.[1] ?? null)
47
48
  : null;
48
49
  // Swatch-hover preview, crossfading over the base image. The Liquid
49
- // island creates the <img> on first hover; React renders it once a
50
+ // island creates the <StoreImage> on first hover; React renders it once a
50
51
  // swatch has actually been hovered.
51
52
  const [preview, setPreview] = useState(null);
52
53
  const onSwatchEnter = (mediaUrl) => () => {
@@ -95,7 +96,7 @@ export function ProductCard({ product, loading = 'lazy' }) {
95
96
  const extraCount = pickerOption
96
97
  ? pickerOption.optionValues.length - valueLimit
97
98
  : 0;
98
- return (_jsxs("div", { className: "product-card", children: [_jsxs("div", { className: "product-card__frame", children: [_jsxs(Link, { to: url, className: "product-card__media", "aria-label": product.title, children: [featuredImage ? (_jsxs(_Fragment, { children: [_jsx("img", { className: "product-card__image", src: imageUrl(featuredImage.url, 800), srcSet: cardSrcSet(featuredImage.url), sizes: CARD_SIZES, loading: loading, ...(loading === 'eager' ? { fetchpriority: 'high' } : null), alt: featuredImage.altText ?? '', width: featuredImage.width ?? undefined, height: featuredImage.height ?? undefined }), secondaryImage ? (_jsx("img", { className: "product-card__image product-card__image--hover", src: imageUrl(secondaryImage.url, 800), srcSet: cardSrcSet(secondaryImage.url), sizes: CARD_SIZES, loading: "lazy", alt: "", "aria-hidden": "true", width: secondaryImage.width ?? undefined, height: secondaryImage.height ?? undefined })) : null] })) : (_jsx(PlaceholderSvg, { className: "product-card__placeholder" })), preview ? (_jsx("img", { className: `product-card__image product-card__image--preview${preview.visible ? ' is-visible' : ''}`, src: preview.src, alt: "", "aria-hidden": "true", onLoad: () => setPreview((previous) => previous ? { ...previous, visible: true } : previous) })) : null, badgePosition === 'corner' && showBadges ? (_jsx("div", { className: "product-card__badges", children: _jsx(ProductBadges, { product: product }) })) : null] }), showQuickAdd ? (_jsx("div", { className: "product-card__quick product-card__quick--overlay", children: quickAdd })) : null] }), _jsxs("div", { className: "product-card__info stack", style: { '--stack-gap': 'var(--space-2xs)' }, children: [badgePosition === 'below' && showBadges ? (_jsx("div", { className: "product-card__badges", children: _jsx(ProductBadges, { product: product }) })) : null, _jsx("h3", { className: "product-card__title", children: _jsx(Link, { to: url, children: product.title }) }), _jsx(Price, { price: product.priceRange?.minVariantPrice, compareAt: product.compareAtPriceRange?.minVariantPrice, unitPrice: product.selectedOrFirstAvailableVariant?.unitPrice, unitPriceMeasurement: product.selectedOrFirstAvailableVariant?.unitPriceMeasurement }), themeSettings.card_show_rating ? _jsx(Rating, { product: product }) : null, pickerOption ? (_jsxs("div", { className: "product-card__swatches", onMouseOut: onSwatchLeave, children: [pickerOption.optionValues.slice(0, valueLimit).map((value) => {
99
+ return (_jsxs("div", { className: "product-card", children: [_jsxs("div", { className: "product-card__frame", children: [_jsxs(Link, { to: url, className: "product-card__media", "aria-label": product.title, children: [featuredImage ? (_jsxs(_Fragment, { children: [_jsx(StoreImage, { className: "product-card__image", src: imageUrl(featuredImage.url, 800), srcSet: cardSrcSet(featuredImage.url), sizes: CARD_SIZES, loading: loading, ...(loading === 'eager' ? { fetchpriority: 'high' } : null), alt: featuredImage.altText ?? '', width: featuredImage.width ?? undefined, height: featuredImage.height ?? undefined }), secondaryImage ? (_jsx(StoreImage, { className: "product-card__image product-card__image--hover", src: imageUrl(secondaryImage.url, 800), srcSet: cardSrcSet(secondaryImage.url), sizes: CARD_SIZES, loading: "lazy", alt: "", "aria-hidden": "true", width: secondaryImage.width ?? undefined, height: secondaryImage.height ?? undefined })) : null] })) : (_jsx(PlaceholderSvg, { className: "product-card__placeholder" })), preview ? (_jsx(StoreImage, { className: `product-card__image product-card__image--preview${preview.visible ? ' is-visible' : ''}`, src: preview.src, alt: "", "aria-hidden": "true", onLoad: () => setPreview((previous) => previous ? { ...previous, visible: true } : previous) })) : null, badgePosition === 'corner' && showBadges ? (_jsx("div", { className: "product-card__badges", children: _jsx(ProductBadges, { product: product }) })) : null] }), showQuickAdd ? (_jsx("div", { className: "product-card__quick product-card__quick--overlay", children: quickAdd })) : null] }), _jsxs("div", { className: "product-card__info stack", style: { '--stack-gap': 'var(--space-2xs)' }, children: [badgePosition === 'below' && showBadges ? (_jsx("div", { className: "product-card__badges", children: _jsx(ProductBadges, { product: product }) })) : null, _jsx("h3", { className: "product-card__title", children: _jsx(Link, { to: url, children: product.title }) }), _jsx(Price, { price: product.priceRange?.minVariantPrice, compareAt: product.compareAtPriceRange?.minVariantPrice, unitPrice: product.selectedOrFirstAvailableVariant?.unitPrice, unitPriceMeasurement: product.selectedOrFirstAvailableVariant?.unitPriceMeasurement }), themeSettings.card_show_rating ? _jsx(Rating, { product: product }) : null, pickerOption ? (_jsxs("div", { className: "product-card__swatches", onMouseOut: onSwatchLeave, children: [pickerOption.optionValues.slice(0, valueLimit).map((value) => {
99
100
  const targetVariant = targetVariantFor(product, pickerOption, value.name);
100
101
  const valueUrl = targetVariant
101
102
  ? `${url}?variant=${numericId(targetVariant.id)}`
@@ -106,7 +107,7 @@ export function ProductCard({ product, loading = 'lazy' }) {
106
107
  if (swatchStyleSetting === 'variant_image') {
107
108
  if (!targetVariant?.image)
108
109
  return null;
109
- return (_jsx(Link, { className: "product-card__variant-thumb", to: valueUrl, "data-card-media": cardMediaUrl, onMouseOver: onSwatchEnter(cardMediaUrl), "aria-label": `${product.title} – ${value.name}`, children: _jsx("img", { src: imageUrl(targetVariant.image.url, 80), loading: "lazy", alt: targetVariant.image.altText ?? '' }) }, value.name));
110
+ return (_jsx(Link, { className: "product-card__variant-thumb", to: valueUrl, "data-card-media": cardMediaUrl, onMouseOver: onSwatchEnter(cardMediaUrl), "aria-label": `${product.title} – ${value.name}`, children: _jsx(StoreImage, { src: imageUrl(targetVariant.image.url, 80), loading: "lazy", alt: targetVariant.image.altText ?? '' }) }, value.name));
110
111
  }
111
112
  return (_jsx(Link, { className: "swatch swatch--sm", to: valueUrl, "data-card-media": cardMediaUrl, onMouseOver: cardMediaUrl ? onSwatchEnter(cardMediaUrl) : undefined, style: swatchStyle(value.name, value.swatch), "aria-label": `${product.title} – ${value.name}` }, value.name));
112
113
  }), extraCount > 0 ? (swatchStyleSetting === 'variant_image' ? (_jsxs(Link, { className: "product-card__swatch-count product-card__swatch-count--tile", to: url, "aria-label": t('products.choose_options'), children: ["+", extraCount] })) : (_jsxs("span", { className: "product-card__swatch-count", "aria-hidden": "true", children: ["+", extraCount] }))) : null] })) : null, showQuickAdd ? (_jsx("div", { className: "product-card__quick product-card__quick--inline", children: quickAdd })) : null] })] }));
@@ -0,0 +1,2 @@
1
+ import type { ImgHTMLAttributes } from 'react';
2
+ export declare function StoreImage(props: ImgHTMLAttributes<HTMLImageElement>): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAdapterImage } from '../adapter';
3
+ export function StoreImage(props) {
4
+ const Image = useAdapterImage();
5
+ return Image ? _jsx(Image, { ...props }) : _jsx("img", { loading: "lazy", decoding: "async", ...props });
6
+ }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from './StoreImage';
2
3
  /**
3
4
  * Port of snippets/video-modal.liquid — expanded video player dialog
4
5
  * shared by the video carousel cards and the video bubble. Built on a
@@ -98,5 +99,5 @@ export function VideoModal({ id, open, onClose, video, poster, product, label, }
98
99
  onClick: (event) => {
99
100
  if (event.target === dialogRef.current)
100
101
  dialogRef.current?.close();
101
- }, children: [_jsx("button", { type: "button", className: "video-modal__close", onClick: () => dialogRef.current?.close(), "aria-label": t('general.close'), children: _jsx(Icon, { name: "icon-close" }) }), _jsx("div", { className: "video-modal__media", children: mp4 ? (_jsx("video", { ref: videoRef, className: "video-modal__video", controls: true, playsInline: true, preload: "metadata", poster: poster ? imageUrl(poster, 1100) : undefined, children: _jsx("source", { src: mp4.url, type: mp4.mime_type }) })) : poster ? (_jsx("img", { className: "video-modal__video", src: imageUrl(poster, 1100), loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "video-modal__video" })) }), product ? (_jsxs("a", { className: "video-modal__product", href: product.url, children: [productImage ? (_jsx("img", { className: "video-modal__product-image", src: imageUrl(productImage, 120), loading: "lazy", alt: "" })) : null, _jsxs("span", { className: "video-modal__product-info", children: [_jsx("span", { className: "video-modal__product-title", children: product.title }), _jsx("span", { className: "video-modal__product-price", children: _jsx(Price, { price: product.price, compareAt: product.compare_at_price }) })] }), _jsx("span", { className: "video-modal__product-cta", children: t('products.view_product') })] })) : null] }));
102
+ }, children: [_jsx("button", { type: "button", className: "video-modal__close", onClick: () => dialogRef.current?.close(), "aria-label": t('general.close'), children: _jsx(Icon, { name: "icon-close" }) }), _jsx("div", { className: "video-modal__media", children: mp4 ? (_jsx("video", { ref: videoRef, className: "video-modal__video", controls: true, playsInline: true, preload: "metadata", poster: poster ? imageUrl(poster, 1100) : undefined, children: _jsx("source", { src: mp4.url, type: mp4.mime_type }) })) : poster ? (_jsx(StoreImage, { className: "video-modal__video", src: imageUrl(poster, 1100), loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "video-modal__video" })) }), product ? (_jsxs("a", { className: "video-modal__product", href: product.url, children: [productImage ? (_jsx(StoreImage, { className: "video-modal__product-image", src: imageUrl(productImage, 120), loading: "lazy", alt: "" })) : null, _jsxs("span", { className: "video-modal__product-info", children: [_jsx("span", { className: "video-modal__product-title", children: product.title }), _jsx("span", { className: "video-modal__product-price", children: _jsx(Price, { price: product.price, compareAt: product.compare_at_price }) })] }), _jsx("span", { className: "video-modal__product-cta", children: t('products.view_product') })] })) : null] }));
102
103
  }
@@ -8,9 +8,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
8
8
  * Static token scales (spacing, type, radii) live in the app's
9
9
  * critical.css (synced from the theme).
10
10
  */
11
- import { useMemo } from 'react';
11
+ import { useMemo, useSyncExternalStore } from 'react';
12
12
  import { colorSchemes, themeSettings } from './settings';
13
- import { getThemeStoreVersion } from './store';
13
+ import { getThemeStoreVersion, subscribePreview, getPreviewVersion, getServerPreviewVersion, hasThemePreview, } from './store';
14
14
  import { googleFontsHrefsFromSettings, parseFontHandle, } from "../../commerce/google-fonts.js";
15
15
  function buildCss() {
16
16
  const s = themeSettings;
@@ -107,14 +107,15 @@ const FONT_CSS_LOADER = [
107
107
  '})(l[i])}',
108
108
  ].join('');
109
109
  export function CssVariables({ nonce, fonts, } = {}) {
110
+ useSyncExternalStore(subscribePreview, getPreviewVersion, getServerPreviewVersion);
110
111
  const themeVersion = getThemeStoreVersion();
111
112
  const { css, links } = useMemo(() => ({ css: buildCss(), links: googleFontsHrefs() }), [themeVersion]);
112
113
  // Server-resolved fonts (the next/font pattern): @font-face rules are
113
114
  // inlined and the latin woff2 files preloaded, so the font downloads
114
115
  // with the HTML and usually beats first paint — no async stylesheet,
115
116
  // no FOUT, no request to fonts.googleapis.com at all.
116
- if (fonts) {
117
+ if (fonts && !hasThemePreview()) {
117
118
  return (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), fonts.preloadUrls.map((href) => (_jsx("link", { rel: "preload", as: "font", type: "font/woff2", href: href, crossOrigin: "anonymous" }, href))), _jsx("style", { dangerouslySetInnerHTML: { __html: fonts.css } }), _jsx("style", { dangerouslySetInnerHTML: { __html: css } })] }));
118
119
  }
119
- return (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), links.map((href) => (_jsx("link", { rel: "preload", as: "style", href: href, "data-zfy-font-css": "" }, href))), _jsx("script", { nonce: nonce, dangerouslySetInnerHTML: { __html: FONT_CSS_LOADER } }), _jsx("noscript", { children: links.map((href) => (_jsx("link", { rel: "stylesheet", href: href }, href))) }), _jsx("style", { dangerouslySetInnerHTML: { __html: css } })] }));
120
+ return (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), links.map((href) => (_jsx("link", { rel: hasThemePreview() ? 'stylesheet' : 'preload', as: "style", href: href, "data-zfy-font-css": "" }, href))), _jsx("script", { nonce: nonce, dangerouslySetInnerHTML: { __html: FONT_CSS_LOADER } }), _jsx("noscript", { children: links.map((href) => (_jsx("link", { rel: "stylesheet", href: href }, href))) }), _jsx("style", { dangerouslySetInnerHTML: { __html: css } })] }));
120
121
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export declare function settingImageUrl(value: unknown): string | null;
14
14
  /** Append a width to a Shopify CDN URL (mirror of `| image_url: width`). */
15
- export declare function imageUrl(url: string, width?: number): string;
15
+ export { imageUrl } from '../../commerce/image-url';
16
16
  /** Srcset across the theme's standard width ladder. */
17
17
  export declare function imageSrcSet(url: string, widths?: number[]): string | undefined;
18
18
  /**
@@ -19,12 +19,8 @@ export function settingImageUrl(value) {
19
19
  return value;
20
20
  }
21
21
  /** Append a width to a Shopify CDN URL (mirror of `| image_url: width`). */
22
- export function imageUrl(url, width) {
23
- if (!width || !url.includes('cdn.shopify.com'))
24
- return url;
25
- const separator = url.includes('?') ? '&' : '?';
26
- return `${url}${separator}width=${width}`;
27
- }
22
+ export { imageUrl } from '../../commerce/image-url';
23
+ import { imageUrl } from '../../commerce/image-url';
28
24
  /** Srcset across the theme's standard width ladder. */
29
25
  export function imageSrcSet(url, widths = [400, 700, 1000, 1500]) {
30
26
  if (!url.includes('cdn.shopify.com'))
@@ -16,7 +16,8 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
16
16
  * emitted unconditionally so server and client markup stay identical.
17
17
  */
18
18
  import { formatThemePath, DATA_PATH_ATTR } from "../../schemas/index.js";
19
- import { getSectionGroup, getTemplate, getThemeStore } from './store';
19
+ import { getSectionGroup, getTemplate, getThemeStore, subscribePreview, getPreviewVersion, getServerPreviewVersion, } from './store';
20
+ import { useSyncExternalStore } from 'react';
20
21
  import { SectionProvider, TemplateProvider } from './context';
21
22
  const warned = new Set();
22
23
  function warnMissing(kind, type) {
@@ -82,6 +83,7 @@ export function ThemeTemplate({ name, resources = {}, sectionData = {}, }) {
82
83
  }
83
84
  /** Mirror of `{% sections 'header-group' %}` in layout/theme.liquid. */
84
85
  export function SectionGroup({ name, sectionData = {}, }) {
86
+ useSyncExternalStore(subscribePreview, getPreviewVersion, getServerPreviewVersion);
85
87
  const group = getSectionGroup(name);
86
88
  return (_jsx(TemplateProvider, { value: { name, kind: 'group', resources: {}, sectionData }, children: _jsx(RenderSections, { template: group, scope: { kind: 'group', name } }) }));
87
89
  }
@@ -49,6 +49,12 @@ export interface InstallThemeOptions {
49
49
  }
50
50
  interface ThemeStore extends InstallThemeOptions {
51
51
  }
52
+ export declare function subscribePreview(listener: () => void): () => void;
53
+ export declare function getPreviewVersion(): number;
54
+ export declare function getServerPreviewVersion(): number;
55
+ export declare function hasThemePreview(): boolean;
56
+ /** Browser-only overlay; the installed schema and SSR state remain untouched. */
57
+ export declare function setThemePreview(owner: ThemeStore, schema: ThemeSchema | null): void;
52
58
  export declare function installTheme(options: InstallThemeOptions): void;
53
59
  /** Monotonic install counter — lets derived caches detect re-installs. */
54
60
  export declare function getThemeStoreVersion(): number;
@@ -1,5 +1,34 @@
1
1
  let store = null;
2
2
  let version = 0;
3
+ let preview = null;
4
+ let previewVersion = 0;
5
+ const previewListeners = new Set();
6
+ export function subscribePreview(listener) {
7
+ previewListeners.add(listener);
8
+ return () => {
9
+ previewListeners.delete(listener);
10
+ };
11
+ }
12
+ export function getPreviewVersion() {
13
+ return previewVersion;
14
+ }
15
+ export function getServerPreviewVersion() {
16
+ return 0;
17
+ }
18
+ export function hasThemePreview() {
19
+ return (typeof window !== 'undefined' && preview !== null && preview.owner === store);
20
+ }
21
+ /** Browser-only overlay; the installed schema and SSR state remain untouched. */
22
+ export function setThemePreview(owner, schema) {
23
+ if (typeof window === 'undefined' || owner !== store)
24
+ return;
25
+ preview = schema
26
+ ? { owner, value: { ...owner, schema, settingsOverride: undefined } }
27
+ : null;
28
+ previewVersion++;
29
+ for (const listener of previewListeners)
30
+ listener();
31
+ }
3
32
  export function installTheme(options) {
4
33
  store = options;
5
34
  // Bump so derived caches (resolved settings) recompute — multi-tenant
@@ -8,14 +37,16 @@ export function installTheme(options) {
8
37
  }
9
38
  /** Monotonic install counter — lets derived caches detect re-installs. */
10
39
  export function getThemeStoreVersion() {
11
- return version;
40
+ return version + (typeof window === 'undefined' ? 0 : previewVersion);
12
41
  }
13
42
  export function getThemeStore() {
14
43
  if (!store) {
15
- throw new Error('[storefront-kit] installTheme() has not been called. Import your app\'s ' +
44
+ throw new Error("[storefront-kit] installTheme() has not been called. Import your app's " +
16
45
  'theme-setup module (which calls installTheme) before rendering.');
17
46
  }
18
- return store;
47
+ return typeof window !== 'undefined' && preview?.owner === store
48
+ ? preview.value
49
+ : store;
19
50
  }
20
51
  /** Look up a page template ("index", "product", "customers/login"…). */
21
52
  export function getTemplate(name) {
@@ -36,3 +36,4 @@ export { ProductCard, PRODUCT_CARD_FRAGMENT } from './components/ProductCard';
36
36
  export * from './components/VideoModal';
37
37
  export { Facets, COLLECTION_SORT_OPTIONS, SEARCH_SORT_OPTIONS, } from './components/Facets';
38
38
  export { builtinSections, builtinBlocks } from './registries';
39
+ export { useEditorTemplate } from './useEditorTemplate';
@@ -37,3 +37,4 @@ export { Facets, COLLECTION_SORT_OPTIONS, SEARCH_SORT_OPTIONS, } from './compone
37
37
  // Built-in registries (spread into installTheme with app extras).
38
38
  // Server-only loaders live in '@zalify/storefront-kit/react/server'.
39
39
  export { builtinSections, builtinBlocks } from './registries';
40
+ export { useEditorTemplate } from './useEditorTemplate';
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StoreImage } from '../components/StoreImage';
2
3
  import { useResource } from '../engine/context';
3
4
  import { t } from '../engine/translate';
4
5
  import { imageSrcSet, imageUrl } from '../engine/images';
@@ -27,5 +28,5 @@ export default function ArticleSection({ settings, }) {
27
28
  date: articleDateHtml(article.publishedAt),
28
29
  author: article.author?.name ?? '',
29
30
  }),
30
- } }), _jsx("h1", { className: "article__title", children: article.title })] }), article.image?.url && (_jsx("img", { className: "article__image", src: imageUrl(article.image.url, 1500), srcSet: imageSrcSet(article.image.url), sizes: "(min-width: 64rem) 60rem, 100vw", loading: "eager", width: article.image.width ?? undefined, height: article.image.height ?? undefined, alt: article.image.altText ?? article.title ?? '' })), _jsx("div", { className: "article__content prose", dangerouslySetInnerHTML: { __html: article.contentHtml ?? '' } })] }) }));
31
+ } }), _jsx("h1", { className: "article__title", children: article.title })] }), article.image?.url && (_jsx(StoreImage, { className: "article__image", src: imageUrl(article.image.url, 1500), srcSet: imageSrcSet(article.image.url), sizes: "(min-width: 64rem) 60rem, 100vw", loading: "eager", width: article.image.width ?? undefined, height: article.image.height ?? undefined, alt: article.image.altText ?? article.title ?? '' })), _jsx("div", { className: "article__content prose", dangerouslySetInnerHTML: { __html: article.contentHtml ?? '' } })] }) }));
31
32
  }