@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.
- package/dist/commerce/disclosure-motion.d.ts +2 -0
- package/dist/commerce/disclosure-motion.js +69 -0
- package/dist/commerce/image-url.d.ts +2 -0
- package/dist/commerce/image-url.js +15 -0
- package/dist/commerce/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -0
- package/dist/editor/bootstrap.d.ts +15 -0
- package/dist/editor/bootstrap.js +42 -0
- package/dist/editor/draft.d.ts +17 -0
- package/dist/editor/draft.js +32 -0
- package/dist/editor/frame.d.ts +2 -0
- package/dist/editor/frame.js +7 -4
- package/dist/react/adapter.d.ts +3 -0
- package/dist/react/adapter.js +1 -0
- package/dist/react/blocks/_slide.js +2 -1
- package/dist/react/blocks/_video-card.js +2 -1
- package/dist/react/blocks/image.js +2 -1
- package/dist/react/blocks/story.js +2 -1
- package/dist/react/blocks/video.js +2 -1
- package/dist/react/components/ProductCard.js +4 -3
- package/dist/react/components/StoreImage.d.ts +2 -0
- package/dist/react/components/StoreImage.js +6 -0
- package/dist/react/components/VideoModal.js +2 -1
- package/dist/react/engine/CssVariables.js +5 -4
- package/dist/react/engine/images.d.ts +1 -1
- package/dist/react/engine/images.js +2 -6
- package/dist/react/engine/render.js +3 -1
- package/dist/react/engine/store.d.ts +6 -0
- package/dist/react/engine/store.js +34 -3
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/sections/article.js +2 -1
- package/dist/react/sections/custom-section.js +2 -1
- package/dist/react/sections/image-with-text.js +2 -1
- package/dist/react/sections/video-bubble.js +2 -1
- package/dist/react/useEditorTemplate.d.ts +15 -0
- package/dist/react/useEditorTemplate.js +114 -0
- package/dist/schemas/bridge.d.ts +1 -0
- package/package.json +1 -1
- package/src/commerce/disclosure-motion.ts +60 -0
- package/src/commerce/image-url.ts +11 -0
- package/src/commerce/index.ts +2 -0
- package/src/editor/bootstrap.ts +61 -0
- package/src/editor/draft.ts +54 -0
- package/src/editor/frame.ts +7 -3
- package/src/react/adapter.tsx +4 -0
- package/src/react/blocks/_slide.tsx +2 -1
- package/src/react/blocks/_video-card.tsx +3 -2
- package/src/react/blocks/image.tsx +2 -1
- package/src/react/blocks/story.tsx +2 -1
- package/src/react/blocks/video.tsx +2 -1
- package/src/react/components/ProductCard.tsx +6 -5
- package/src/react/components/StoreImage.tsx +6 -0
- package/src/react/components/VideoModal.tsx +3 -2
- package/src/react/engine/CssVariables.tsx +34 -10
- package/src/react/engine/images.tsx +2 -5
- package/src/react/engine/render.tsx +14 -1
- package/src/react/engine/store.ts +43 -4
- package/src/react/index.ts +2 -0
- package/src/react/sections/article.tsx +2 -1
- package/src/react/sections/custom-section.tsx +2 -1
- package/src/react/sections/image-with-text.tsx +2 -1
- package/src/react/sections/video-bubble.tsx +2 -1
- package/src/react/useEditorTemplate.ts +136 -0
- package/src/schemas/bridge.ts +1 -0
|
@@ -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,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
|
+
}
|
package/dist/commerce/index.d.ts
CHANGED
package/dist/commerce/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/editor/frame.d.ts
CHANGED
|
@@ -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;
|
package/dist/editor/frame.js
CHANGED
|
@@ -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
|
-
//
|
|
254
|
-
//
|
|
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
|
},
|
package/dist/react/adapter.d.ts
CHANGED
|
@@ -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;
|
package/dist/react/adapter.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 <
|
|
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(
|
|
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(
|
|
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,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(
|
|
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:
|
|
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
|
|
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
|
|
23
|
-
|
|
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(
|
|
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) {
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/react/index.js
CHANGED
|
@@ -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(
|
|
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
|
}
|