@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.
- package/LICENSE.md +49 -0
- package/README.md +63 -0
- package/dist/commerce/countdown.d.ts +18 -0
- package/dist/commerce/countdown.js +28 -0
- package/dist/commerce/events.d.ts +29 -0
- package/dist/commerce/events.js +14 -0
- package/dist/commerce/facets.d.ts +27 -0
- package/dist/commerce/facets.js +71 -0
- package/dist/commerce/index.d.ts +11 -0
- package/dist/commerce/index.js +11 -0
- package/dist/commerce/money.d.ts +51 -0
- package/dist/commerce/money.js +83 -0
- package/dist/commerce/product.d.ts +102 -0
- package/dist/commerce/product.js +93 -0
- package/dist/editor/frame.d.ts +57 -0
- package/dist/editor/frame.js +241 -0
- package/dist/editor/host.d.ts +31 -0
- package/dist/editor/host.js +67 -0
- package/dist/editor/index.d.ts +9 -0
- package/dist/editor/index.js +9 -0
- package/dist/react/adapter.d.ts +49 -0
- package/dist/react/adapter.js +23 -0
- package/dist/react/blocks/_box.d.ts +10 -0
- package/dist/react/blocks/_box.js +17 -0
- package/dist/react/blocks/_slide.d.ts +24 -0
- package/dist/react/blocks/_slide.js +21 -0
- package/dist/react/blocks/_video-card.d.ts +11 -0
- package/dist/react/blocks/_video-card.js +39 -0
- package/dist/react/blocks/accordion.d.ts +12 -0
- package/dist/react/blocks/accordion.js +5 -0
- package/dist/react/blocks/button.d.ts +10 -0
- package/dist/react/blocks/button.js +15 -0
- package/dist/react/blocks/countdown.d.ts +9 -0
- package/dist/react/blocks/countdown.js +64 -0
- package/dist/react/blocks/custom-liquid.d.ts +11 -0
- package/dist/react/blocks/custom-liquid.js +4 -0
- package/dist/react/blocks/group.d.ts +9 -0
- package/dist/react/blocks/group.js +8 -0
- package/dist/react/blocks/image.d.ts +8 -0
- package/dist/react/blocks/image.js +7 -0
- package/dist/react/blocks/message.d.ts +14 -0
- package/dist/react/blocks/message.js +5 -0
- package/dist/react/blocks/quote.d.ts +9 -0
- package/dist/react/blocks/quote.js +5 -0
- package/dist/react/blocks/spacer.d.ts +8 -0
- package/dist/react/blocks/spacer.js +5 -0
- package/dist/react/blocks/story.d.ts +17 -0
- package/dist/react/blocks/story.js +7 -0
- package/dist/react/blocks/text.d.ts +9 -0
- package/dist/react/blocks/text.js +5 -0
- package/dist/react/blocks/video.d.ts +17 -0
- package/dist/react/blocks/video.js +56 -0
- package/dist/react/components/Facets.d.ts +21 -0
- package/dist/react/components/Facets.js +220 -0
- package/dist/react/components/Icon.d.ts +12 -0
- package/dist/react/components/Icon.js +66 -0
- package/dist/react/components/PaymentIcons.d.ts +5 -0
- package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
- package/dist/react/components/PaymentIcons.generated.js +21 -0
- package/dist/react/components/PaymentIcons.js +18 -0
- package/dist/react/components/Price.d.ts +24 -0
- package/dist/react/components/Price.js +9 -0
- package/dist/react/components/ProductBadges.d.ts +24 -0
- package/dist/react/components/ProductBadges.js +57 -0
- package/dist/react/components/ProductCard.d.ts +17 -0
- package/dist/react/components/ProductCard.js +202 -0
- package/dist/react/components/Rating.d.ts +11 -0
- package/dist/react/components/Rating.js +35 -0
- package/dist/react/components/SwatchStyle.d.ts +23 -0
- package/dist/react/components/SwatchStyle.js +22 -0
- package/dist/react/components/VideoModal.d.ts +55 -0
- package/dist/react/components/VideoModal.js +102 -0
- package/dist/react/engine/CssVariables.d.ts +1 -0
- package/dist/react/engine/CssVariables.js +105 -0
- package/dist/react/engine/context.d.ts +43 -0
- package/dist/react/engine/context.js +45 -0
- package/dist/react/engine/fonts.d.ts +15 -0
- package/dist/react/engine/fonts.js +33 -0
- package/dist/react/engine/images.d.ts +24 -0
- package/dist/react/engine/images.js +40 -0
- package/dist/react/engine/render.d.ts +29 -0
- package/dist/react/engine/render.js +87 -0
- package/dist/react/engine/settings.d.ts +8 -0
- package/dist/react/engine/settings.js +68 -0
- package/dist/react/engine/store.d.ts +64 -0
- package/dist/react/engine/store.js +42 -0
- package/dist/react/engine/translate.d.ts +7 -0
- package/dist/react/engine/translate.js +33 -0
- package/dist/react/engine/types.d.ts +128 -0
- package/dist/react/engine/types.js +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +39 -0
- package/dist/react/registries.d.ts +10 -0
- package/dist/react/registries.js +68 -0
- package/dist/react/registries.server.d.ts +2 -0
- package/dist/react/registries.server.js +98 -0
- package/dist/react/sections/404.d.ts +10 -0
- package/dist/react/sections/404.js +10 -0
- package/dist/react/sections/announcement-bar.d.ts +14 -0
- package/dist/react/sections/announcement-bar.js +84 -0
- package/dist/react/sections/article.d.ts +17 -0
- package/dist/react/sections/article.js +31 -0
- package/dist/react/sections/contact-form.d.ts +16 -0
- package/dist/react/sections/contact-form.js +8 -0
- package/dist/react/sections/custom-liquid.d.ts +14 -0
- package/dist/react/sections/custom-liquid.js +7 -0
- package/dist/react/sections/custom-section.d.ts +14 -0
- package/dist/react/sections/custom-section.js +9 -0
- package/dist/react/sections/featured-collection.d.ts +10 -0
- package/dist/react/sections/featured-collection.js +23 -0
- package/dist/react/sections/hero.d.ts +6 -0
- package/dist/react/sections/hero.js +35 -0
- package/dist/react/sections/image-with-text.d.ts +13 -0
- package/dist/react/sections/image-with-text.js +9 -0
- package/dist/react/sections/multicolumn.d.ts +15 -0
- package/dist/react/sections/multicolumn.js +8 -0
- package/dist/react/sections/page.d.ts +13 -0
- package/dist/react/sections/page.js +11 -0
- package/dist/react/sections/password.d.ts +15 -0
- package/dist/react/sections/password.js +9 -0
- package/dist/react/sections/rich-text.d.ts +12 -0
- package/dist/react/sections/rich-text.js +7 -0
- package/dist/react/sections/stories.d.ts +18 -0
- package/dist/react/sections/stories.js +10 -0
- package/dist/react/sections/video-bubble.d.ts +13 -0
- package/dist/react/sections/video-bubble.js +54 -0
- package/dist/react/sections/video-carousel.d.ts +19 -0
- package/dist/react/sections/video-carousel.js +9 -0
- package/dist/react/server.d.ts +7 -0
- package/dist/react/server.js +42 -0
- package/dist/schemas/bridge.d.ts +98 -0
- package/dist/schemas/bridge.js +50 -0
- package/dist/schemas/data.d.ts +54 -0
- package/dist/schemas/data.js +22 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.js +14 -0
- package/dist/schemas/manifest.d.ts +63 -0
- package/dist/schemas/manifest.js +32 -0
- package/dist/schemas/schema.d.ts +159 -0
- package/dist/schemas/schema.js +129 -0
- package/dist/schemas/validate.d.ts +22 -0
- package/dist/schemas/validate.js +199 -0
- package/package.json +84 -0
- package/src/commerce/countdown.ts +38 -0
- package/src/commerce/events.ts +43 -0
- package/src/commerce/facets.ts +88 -0
- package/src/commerce/index.ts +11 -0
- package/src/commerce/money.ts +120 -0
- package/src/commerce/product.ts +177 -0
- package/src/editor/frame.ts +305 -0
- package/src/editor/host.ts +101 -0
- package/src/editor/index.ts +9 -0
- package/src/react/adapter.tsx +83 -0
- package/src/react/blocks/_box.tsx +37 -0
- package/src/react/blocks/_slide.tsx +96 -0
- package/src/react/blocks/_video-card.tsx +131 -0
- package/src/react/blocks/accordion.tsx +33 -0
- package/src/react/blocks/button.tsx +36 -0
- package/src/react/blocks/countdown.tsx +105 -0
- package/src/react/blocks/custom-liquid.tsx +21 -0
- package/src/react/blocks/group.tsx +32 -0
- package/src/react/blocks/image.tsx +29 -0
- package/src/react/blocks/message.tsx +28 -0
- package/src/react/blocks/quote.tsx +24 -0
- package/src/react/blocks/spacer.tsx +17 -0
- package/src/react/blocks/story.tsx +40 -0
- package/src/react/blocks/text.tsx +24 -0
- package/src/react/blocks/video.tsx +132 -0
- package/src/react/components/Facets.tsx +424 -0
- package/src/react/components/Icon.tsx +96 -0
- package/src/react/components/PaymentIcons.generated.ts +24 -0
- package/src/react/components/PaymentIcons.tsx +27 -0
- package/src/react/components/Price.tsx +72 -0
- package/src/react/components/ProductBadges.tsx +95 -0
- package/src/react/components/ProductCard.tsx +417 -0
- package/src/react/components/Rating.tsx +59 -0
- package/src/react/components/SwatchStyle.tsx +50 -0
- package/src/react/components/VideoModal.tsx +236 -0
- package/src/react/engine/CssVariables.tsx +142 -0
- package/src/react/engine/context.tsx +108 -0
- package/src/react/engine/fonts.ts +42 -0
- package/src/react/engine/images.tsx +58 -0
- package/src/react/engine/render.tsx +190 -0
- package/src/react/engine/settings.ts +89 -0
- package/src/react/engine/store.ts +103 -0
- package/src/react/engine/translate.ts +40 -0
- package/src/react/engine/types.ts +142 -0
- package/src/react/index.ts +78 -0
- package/src/react/registries.server.ts +110 -0
- package/src/react/registries.ts +80 -0
- package/src/react/sections/404.tsx +37 -0
- package/src/react/sections/announcement-bar.tsx +170 -0
- package/src/react/sections/article.tsx +97 -0
- package/src/react/sections/contact-form.tsx +104 -0
- package/src/react/sections/custom-liquid.tsx +39 -0
- package/src/react/sections/custom-section.tsx +44 -0
- package/src/react/sections/featured-collection.tsx +89 -0
- package/src/react/sections/hero.tsx +84 -0
- package/src/react/sections/image-with-text.tsx +61 -0
- package/src/react/sections/multicolumn.tsx +46 -0
- package/src/react/sections/page.tsx +43 -0
- package/src/react/sections/password.tsx +60 -0
- package/src/react/sections/rich-text.tsx +37 -0
- package/src/react/sections/stories.tsx +51 -0
- package/src/react/sections/video-bubble.tsx +145 -0
- package/src/react/sections/video-carousel.tsx +57 -0
- package/src/react/server.ts +60 -0
- package/src/schemas/bridge.ts +143 -0
- package/src/schemas/data.ts +72 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/manifest.ts +85 -0
- package/src/schemas/schema.ts +287 -0
- package/src/schemas/validate.ts +302 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { imageUrl } from '../engine/images';
|
|
2
|
+
// Only vetted CSS keywords may pass through — an arbitrary name would
|
|
3
|
+
// invalidate the whole background declaration
|
|
4
|
+
const CSS_COLORS = new Set('aliceblue,antiquewhite,aqua,aquamarine,azure,beige,bisque,black,blanchedalmond,blue,blueviolet,brown,burlywood,cadetblue,chartreuse,chocolate,coral,cornflowerblue,cornsilk,crimson,cyan,darkblue,darkcyan,darkgoldenrod,darkgray,darkgreen,darkgrey,darkkhaki,darkmagenta,darkolivegreen,darkorange,darkorchid,darkred,darksalmon,darkseagreen,darkslateblue,darkslategray,darkslategrey,darkturquoise,darkviolet,deeppink,deepskyblue,dimgray,dimgrey,dodgerblue,firebrick,floralwhite,forestgreen,fuchsia,gainsboro,ghostwhite,gold,goldenrod,gray,green,greenyellow,grey,honeydew,hotpink,indianred,indigo,ivory,khaki,lavender,lavenderblush,lawngreen,lemonchiffon,lightblue,lightcoral,lightcyan,lightgoldenrodyellow,lightgray,lightgreen,lightgrey,lightpink,lightsalmon,lightseagreen,lightskyblue,lightslategray,lightslategrey,lightsteelblue,lightyellow,lime,limegreen,linen,magenta,maroon,mediumaquamarine,mediumblue,mediumorchid,mediumpurple,mediumseagreen,mediumslateblue,mediumspringgreen,mediumturquoise,mediumvioletred,midnightblue,mintcream,mistyrose,moccasin,navajowhite,navy,oldlace,olive,olivedrab,orange,orangered,orchid,palegoldenrod,palegreen,paleturquoise,palevioletred,papayawhip,peachpuff,peru,pink,plum,powderblue,purple,rebeccapurple,red,rosybrown,royalblue,saddlebrown,salmon,sandybrown,seagreen,seashell,sienna,silver,skyblue,slateblue,slategray,slategrey,snow,springgreen,steelblue,tan,teal,thistle,tomato,turquoise,violet,wheat,white,whitesmoke,yellow,yellowgreen'.split(','));
|
|
5
|
+
/**
|
|
6
|
+
* `style={swatchStyle(value.name, value.swatch)}` ≙
|
|
7
|
+
* `style='{% render 'swatch-style', swatch: value.swatch, name: value.name %}'`
|
|
8
|
+
*/
|
|
9
|
+
export function swatchStyle(name, swatch) {
|
|
10
|
+
const imageSrc = swatch?.image?.previewImage?.url;
|
|
11
|
+
if (imageSrc) {
|
|
12
|
+
return { '--swatch': `url(${imageUrl(imageSrc, 100)})` };
|
|
13
|
+
}
|
|
14
|
+
if (swatch?.color) {
|
|
15
|
+
return { '--swatch': swatch.color };
|
|
16
|
+
}
|
|
17
|
+
const probe = name.toLowerCase().trim().replace(/ /g, '').replace(/-/g, '');
|
|
18
|
+
if (CSS_COLORS.has(probe)) {
|
|
19
|
+
return { '--swatch': probe };
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Money } from '../../commerce/index.ts';
|
|
2
|
+
export interface VideoSource {
|
|
3
|
+
url: string;
|
|
4
|
+
format?: string;
|
|
5
|
+
height?: number;
|
|
6
|
+
mime_type?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Theme-editor `video` setting object (usually absent in the mirror). */
|
|
9
|
+
export interface ThemeVideo {
|
|
10
|
+
sources?: VideoSource[];
|
|
11
|
+
preview_image?: {
|
|
12
|
+
src?: string;
|
|
13
|
+
} | string;
|
|
14
|
+
alt?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Mirror of `video.sources | where: 'format', 'mp4' | sort: 'height'`. */
|
|
17
|
+
export declare function mp4Sources(video: ThemeVideo | undefined | null): VideoSource[];
|
|
18
|
+
/** Resolve a video's preview_image to a usable URL (or null). */
|
|
19
|
+
export declare function videoPreviewImage(video: ThemeVideo | undefined | null): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Loose `product` setting shape. Template JSON stores product settings
|
|
22
|
+
* as a handle string, which only a section loader could resolve — those
|
|
23
|
+
* return null so callers skip the product row gracefully.
|
|
24
|
+
*/
|
|
25
|
+
export interface VideoProductSetting {
|
|
26
|
+
url?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
featured_image?: unknown;
|
|
29
|
+
price?: Money;
|
|
30
|
+
compare_at_price?: Money | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveVideoProduct(value: unknown): VideoProductSetting | null;
|
|
33
|
+
/** Resolve a loose product image value (string or {src|url}) to a URL. */
|
|
34
|
+
export declare function productImageUrl(value: unknown): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Mirror of watchPreviews() in src/entries/video.ts: a muted
|
|
37
|
+
* `video[data-video-preview]` loop plays only while near the viewport.
|
|
38
|
+
* Attach the returned ref to the preview <video>.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useViewportPlayback(): import("react").RefObject<HTMLVideoElement>;
|
|
41
|
+
export interface VideoModalProps {
|
|
42
|
+
/** Mirror of the Liquid snippet's `id` param (data-dialog-open target). */
|
|
43
|
+
id?: string;
|
|
44
|
+
open: boolean;
|
|
45
|
+
onClose: () => void;
|
|
46
|
+
/** Shopify-hosted video to play (largest mp4 rendition is used). */
|
|
47
|
+
video?: ThemeVideo | null;
|
|
48
|
+
/** Cover image URL painted before playback. */
|
|
49
|
+
poster?: string | null;
|
|
50
|
+
/** Product featured in the video. */
|
|
51
|
+
product?: VideoProductSetting | null;
|
|
52
|
+
/** Accessible name for the dialog. */
|
|
53
|
+
label?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function VideoModal({ id, open, onClose, video, poster, product, label, }: VideoModalProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Port of snippets/video-modal.liquid — expanded video player dialog
|
|
4
|
+
* shared by the video carousel cards and the video bubble. Built on a
|
|
5
|
+
* native <dialog>, mirroring the <theme-dialog> semantics from
|
|
6
|
+
* src/lib/dialog.ts (showModal/close, light dismiss on backdrop click,
|
|
7
|
+
* data-dialog-close ≙ the close button) and the play-on-open /
|
|
8
|
+
* pause-on-close wiring from src/entries/video.ts.
|
|
9
|
+
* CSS: app/styles/components/snippets-video-modal.css
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useRef } from 'react';
|
|
12
|
+
import { t } from '../engine/translate';
|
|
13
|
+
import { Icon } from './Icon';
|
|
14
|
+
import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
|
|
15
|
+
import { Price } from './Price';
|
|
16
|
+
/** Mirror of `video.sources | where: 'format', 'mp4' | sort: 'height'`. */
|
|
17
|
+
export function mp4Sources(video) {
|
|
18
|
+
return (video?.sources ?? [])
|
|
19
|
+
.filter((s) => s.format === 'mp4')
|
|
20
|
+
.sort((a, b) => (a.height ?? 0) - (b.height ?? 0));
|
|
21
|
+
}
|
|
22
|
+
/** Resolve a video's preview_image to a usable URL (or null). */
|
|
23
|
+
export function videoPreviewImage(video) {
|
|
24
|
+
const pi = video?.preview_image;
|
|
25
|
+
return settingImageUrl(typeof pi === 'string' ? pi : pi?.src);
|
|
26
|
+
}
|
|
27
|
+
export function resolveVideoProduct(value) {
|
|
28
|
+
if (!value || typeof value !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** Resolve a loose product image value (string or {src|url}) to a URL. */
|
|
33
|
+
export function productImageUrl(value) {
|
|
34
|
+
if (typeof value === 'string')
|
|
35
|
+
return settingImageUrl(value);
|
|
36
|
+
if (value && typeof value === 'object') {
|
|
37
|
+
const obj = value;
|
|
38
|
+
return settingImageUrl(obj.src ?? obj.url);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Mirror of watchPreviews() in src/entries/video.ts: a muted
|
|
44
|
+
* `video[data-video-preview]` loop plays only while near the viewport.
|
|
45
|
+
* Attach the returned ref to the preview <video>.
|
|
46
|
+
*/
|
|
47
|
+
export function useViewportPlayback() {
|
|
48
|
+
const ref = useRef(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const video = ref.current;
|
|
51
|
+
if (!video)
|
|
52
|
+
return;
|
|
53
|
+
const io = new IntersectionObserver((entries) => {
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.isIntersecting)
|
|
56
|
+
video.play().catch(() => { });
|
|
57
|
+
else
|
|
58
|
+
video.pause();
|
|
59
|
+
}
|
|
60
|
+
}, { rootMargin: '80px' });
|
|
61
|
+
io.observe(video);
|
|
62
|
+
return () => io.disconnect();
|
|
63
|
+
}, []);
|
|
64
|
+
return ref;
|
|
65
|
+
}
|
|
66
|
+
export function VideoModal({ id, open, onClose, video, poster, product, label, }) {
|
|
67
|
+
const dialogRef = useRef(null);
|
|
68
|
+
const videoRef = useRef(null);
|
|
69
|
+
// showModal()/close() driven by the open prop — the React mirror of
|
|
70
|
+
// ThemeDialog.show()/hide(). Playback starts a frame later so the
|
|
71
|
+
// video starts inside the open dialog (≙ playDialogVideo in video.ts).
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const dialog = dialogRef.current;
|
|
74
|
+
if (!dialog)
|
|
75
|
+
return;
|
|
76
|
+
if (open && !dialog.open) {
|
|
77
|
+
dialog.showModal();
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
videoRef.current?.play().catch(() => { });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (!open && dialog.open) {
|
|
83
|
+
dialog.close();
|
|
84
|
+
}
|
|
85
|
+
}, [open]);
|
|
86
|
+
const mp4 = mp4Sources(video).at(-1);
|
|
87
|
+
const modalLabel = label || t('general.play_video');
|
|
88
|
+
const productImage = productImageUrl(product?.featured_image);
|
|
89
|
+
return (_jsxs("dialog", { ref: dialogRef, id: id, "scroll-lock": "", className: "video-modal", "aria-label": modalLabel,
|
|
90
|
+
// Expanded players stop when their dialog closes (Esc included) —
|
|
91
|
+
// mirror of pauseOnDialogClose in video.ts.
|
|
92
|
+
onClose: () => {
|
|
93
|
+
videoRef.current?.pause();
|
|
94
|
+
onClose();
|
|
95
|
+
},
|
|
96
|
+
// Light dismiss: clicks land on the <dialog> itself only when
|
|
97
|
+
// they hit the backdrop (mirror of ThemeDialog's click handler).
|
|
98
|
+
onClick: (event) => {
|
|
99
|
+
if (event.target === dialogRef.current)
|
|
100
|
+
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function CssVariables(): import("react").JSX.Element;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* React mirror of snippets/css-variables.liquid: emits merchant-driven
|
|
4
|
+
* design tokens as CSS custom properties — font slots, page layout,
|
|
5
|
+
* corner radii, badge colors, grid density, and one class per color
|
|
6
|
+
* scheme (the first scheme also seeds :root).
|
|
7
|
+
*
|
|
8
|
+
* Static token scales (spacing, type, radii) live in the app's
|
|
9
|
+
* critical.css (synced from the theme).
|
|
10
|
+
*/
|
|
11
|
+
import { useMemo } from 'react';
|
|
12
|
+
import { colorSchemes, themeSettings } from './settings';
|
|
13
|
+
import { googleFontsHref, parseFontHandle } from './fonts';
|
|
14
|
+
function buildCss() {
|
|
15
|
+
const s = themeSettings;
|
|
16
|
+
const bodyGoogle = (s.type_body_google ?? '').trim();
|
|
17
|
+
const headingGoogle = (s.type_heading_google ?? '').trim();
|
|
18
|
+
const monoGoogle = (s.type_mono_google ?? '').trim();
|
|
19
|
+
const bodyFont = parseFontHandle(s.type_body_font);
|
|
20
|
+
const headingFont = parseFontHandle(s.type_heading_font);
|
|
21
|
+
const bodyFamily = bodyGoogle
|
|
22
|
+
? `'${bodyGoogle}', sans-serif`
|
|
23
|
+
: `'${bodyFont.family}', sans-serif`;
|
|
24
|
+
const headingFamily = headingGoogle
|
|
25
|
+
? `'${headingGoogle}', var(--font-body--family)`
|
|
26
|
+
: `'${headingFont.family}', sans-serif`;
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push(':root {');
|
|
29
|
+
lines.push(` --font-body--family: ${bodyFamily};`);
|
|
30
|
+
lines.push(` --font-body--style: ${bodyGoogle ? 'normal' : bodyFont.style};`);
|
|
31
|
+
lines.push(` --font-body--weight: ${bodyGoogle ? 400 : bodyFont.weight};`);
|
|
32
|
+
lines.push(` --font-heading--family: ${headingFamily};`);
|
|
33
|
+
if (monoGoogle) {
|
|
34
|
+
lines.push(` --font-mono--family: '${monoGoogle}', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;`);
|
|
35
|
+
// Accent elements (eyebrows, badges, footer headings) use mono only
|
|
36
|
+
// when chosen; otherwise they inherit via the var() fallback.
|
|
37
|
+
lines.push(` --font-accent--family: var(--font-mono--family);`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push(` --font-mono--family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;`);
|
|
41
|
+
}
|
|
42
|
+
lines.push(` --page-width: ${s.max_page_width ?? '90rem'};`);
|
|
43
|
+
lines.push(` --page-margin: ${s.min_page_margin ?? 20}px;`);
|
|
44
|
+
lines.push(` --style-border-radius-inputs: ${s.input_corner_radius ?? 0}px;`);
|
|
45
|
+
lines.push(` --style-border-radius-buttons: ${s.button_corner_radius ?? 0}px;`);
|
|
46
|
+
lines.push(` --style-border-radius-cards: ${s.card_corner_radius ?? 0}px;`);
|
|
47
|
+
lines.push(` --style-badge-background: ${s.card_badge_background ?? '#F9EDEF'};`);
|
|
48
|
+
lines.push(` --style-badge-text: ${s.card_badge_text ?? '#1A1A1A'};`);
|
|
49
|
+
lines.push(` --style-badge-sale-background: ${s.card_badge_sale_background ?? '#F26B8A'};`);
|
|
50
|
+
lines.push(` --style-badge-sale-text: ${s.card_badge_sale_text ?? '#FFFFFF'};`);
|
|
51
|
+
lines.push(` --style-badge-1-background: ${s.badge_style_1_background ?? '#1A1A1A'};`);
|
|
52
|
+
lines.push(` --style-badge-1-text: ${s.badge_style_1_text ?? '#FFFFFF'};`);
|
|
53
|
+
lines.push(` --style-badge-2-background: ${s.badge_style_2_background ?? '#F9EDEF'};`);
|
|
54
|
+
lines.push(` --style-badge-2-text: ${s.badge_style_2_text ?? '#1A1A1A'};`);
|
|
55
|
+
// Product grid density: comfortable (fluid) or compact (tight, edge-bleed on mobile)
|
|
56
|
+
if (s.card_density === 'compact') {
|
|
57
|
+
lines.push(` --product-grid-gap: 0.75rem 0.5rem;`);
|
|
58
|
+
lines.push(` --product-grid-margin: -0.75rem;`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push(` --product-grid-gap: clamp(0.5rem, 2.1333vw, 1rem);`);
|
|
62
|
+
lines.push(` --product-grid-margin: 0;`);
|
|
63
|
+
}
|
|
64
|
+
// Variant image thumbs: fully round by default; follow the card
|
|
65
|
+
// radius once the merchant sets one.
|
|
66
|
+
const cardRadius = s.card_corner_radius ?? 0;
|
|
67
|
+
lines.push(` --style-border-radius-swatch: ${cardRadius === 0 ? '999px' : `${cardRadius}px`};`);
|
|
68
|
+
lines.push('}');
|
|
69
|
+
for (const [index, scheme] of colorSchemes().entries()) {
|
|
70
|
+
const prefix = index === 0 ? ':root,\n' : '';
|
|
71
|
+
lines.push(`${prefix}.color-${scheme.id} {`);
|
|
72
|
+
lines.push(` --color-background: ${scheme.settings.background};`);
|
|
73
|
+
lines.push(` --color-foreground: ${scheme.settings.text};`);
|
|
74
|
+
lines.push(` --color-accent: ${scheme.settings.accent};`);
|
|
75
|
+
lines.push(` --color-button: ${scheme.settings.button};`);
|
|
76
|
+
lines.push(` --color-button-label: ${scheme.settings.button_label};`);
|
|
77
|
+
lines.push(` --color-border: color-mix(in oklch, ${scheme.settings.text} 15%, ${scheme.settings.background});`);
|
|
78
|
+
lines.push('}');
|
|
79
|
+
}
|
|
80
|
+
// Applying a scheme class paints the element itself
|
|
81
|
+
lines.push(`[class*='color-scheme-'] {`);
|
|
82
|
+
lines.push(` background-color: var(--color-background);`);
|
|
83
|
+
lines.push(` color: var(--color-foreground);`);
|
|
84
|
+
lines.push('}');
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
function fontLinks() {
|
|
88
|
+
const s = themeSettings;
|
|
89
|
+
const hrefs = [];
|
|
90
|
+
const bodyGoogle = (s.type_body_google ?? '').trim();
|
|
91
|
+
const headingGoogle = (s.type_heading_google ?? '').trim();
|
|
92
|
+
const monoGoogle = (s.type_mono_google ?? '').trim();
|
|
93
|
+
hrefs.push(googleFontsHref(bodyGoogle || parseFontHandle(s.type_body_font).family));
|
|
94
|
+
const heading = headingGoogle || parseFontHandle(s.type_heading_font).family;
|
|
95
|
+
if (!hrefs[0].includes(heading.replace(/ /g, '+'))) {
|
|
96
|
+
hrefs.push(googleFontsHref(heading, 'wght@400;500;600;700'));
|
|
97
|
+
}
|
|
98
|
+
if (monoGoogle)
|
|
99
|
+
hrefs.push(googleFontsHref(monoGoogle, 'wght@400;500'));
|
|
100
|
+
return hrefs;
|
|
101
|
+
}
|
|
102
|
+
export function CssVariables() {
|
|
103
|
+
const { css, links } = useMemo(() => ({ css: buildCss(), links: fontLinks() }), []);
|
|
104
|
+
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: "stylesheet", href: href }, href))), _jsx("style", { dangerouslySetInnerHTML: { __html: css } })] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface ThemeGlobals {
|
|
3
|
+
/** Root loader data from the Hydrogen scaffold (header/footer queries, cart…). */
|
|
4
|
+
header?: unknown;
|
|
5
|
+
footer?: unknown;
|
|
6
|
+
cart?: unknown;
|
|
7
|
+
isLoggedIn?: unknown;
|
|
8
|
+
publicStoreDomain?: string;
|
|
9
|
+
shop?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface TemplateScope {
|
|
12
|
+
/** Template name, e.g. "index", "product". */
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether `name` is a page template or a section group — the scope
|
|
16
|
+
* half of a @zalify/storefront-kit/schemas ThemeJsonPath. Defaults to
|
|
17
|
+
* "template".
|
|
18
|
+
*/
|
|
19
|
+
kind?: 'template' | 'group';
|
|
20
|
+
/** Route resources: product, collection, blog, article, page, searchResult… */
|
|
21
|
+
resources: Record<string, unknown>;
|
|
22
|
+
/** Results of app/sections/<type>.server.ts loaders, keyed by section key. */
|
|
23
|
+
sectionData: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export declare function ThemeDataProvider({ value, children, }: {
|
|
26
|
+
value: ThemeGlobals;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
export declare function TemplateProvider({ value, children, }: {
|
|
30
|
+
value: TemplateScope;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
export declare function SectionProvider({ id, children, }: {
|
|
34
|
+
id: string;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
37
|
+
/** Liquid globals mirror (`cart`, `shop`, menus…). */
|
|
38
|
+
export declare function useThemeGlobals(): ThemeGlobals;
|
|
39
|
+
/** Route resource mirror of Liquid template objects, e.g. useResource('product'). */
|
|
40
|
+
export declare function useResource<T = unknown>(name: string): T | undefined;
|
|
41
|
+
export declare function useTemplateScope(): TemplateScope;
|
|
42
|
+
/** Result of this section's optional .server.ts loader. */
|
|
43
|
+
export declare function useSectionData<T = unknown>(): T | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Contexts giving ported sections access to Shopify data.
|
|
4
|
+
*
|
|
5
|
+
* - ThemeDataContext (set once in root/PageLayout): header menu, footer
|
|
6
|
+
* menu, cart promise, shop info — the mirror of Liquid globals like
|
|
7
|
+
* `linklists`, `cart`, `shop`.
|
|
8
|
+
* - TemplateContext (set per route): the route's resources (`product`,
|
|
9
|
+
* `collection`, …) — the mirror of Liquid template objects — plus
|
|
10
|
+
* per-section loader results.
|
|
11
|
+
* - SectionContext / current section id, used by useSectionData().
|
|
12
|
+
*/
|
|
13
|
+
import { createContext, useContext } from 'react';
|
|
14
|
+
const ThemeDataContext = createContext({});
|
|
15
|
+
const TemplateContext = createContext({
|
|
16
|
+
name: '',
|
|
17
|
+
resources: {},
|
|
18
|
+
sectionData: {},
|
|
19
|
+
});
|
|
20
|
+
const SectionContext = createContext({ id: '' });
|
|
21
|
+
export function ThemeDataProvider({ value, children, }) {
|
|
22
|
+
return (_jsx(ThemeDataContext.Provider, { value: value, children: children }));
|
|
23
|
+
}
|
|
24
|
+
export function TemplateProvider({ value, children, }) {
|
|
25
|
+
return (_jsx(TemplateContext.Provider, { value: value, children: children }));
|
|
26
|
+
}
|
|
27
|
+
export function SectionProvider({ id, children, }) {
|
|
28
|
+
return (_jsx(SectionContext.Provider, { value: { id }, children: children }));
|
|
29
|
+
}
|
|
30
|
+
/** Liquid globals mirror (`cart`, `shop`, menus…). */
|
|
31
|
+
export function useThemeGlobals() {
|
|
32
|
+
return useContext(ThemeDataContext);
|
|
33
|
+
}
|
|
34
|
+
/** Route resource mirror of Liquid template objects, e.g. useResource('product'). */
|
|
35
|
+
export function useResource(name) {
|
|
36
|
+
return useContext(TemplateContext).resources[name];
|
|
37
|
+
}
|
|
38
|
+
export function useTemplateScope() {
|
|
39
|
+
return useContext(TemplateContext);
|
|
40
|
+
}
|
|
41
|
+
/** Result of this section's optional .server.ts loader. */
|
|
42
|
+
export function useSectionData() {
|
|
43
|
+
const { id } = useContext(SectionContext);
|
|
44
|
+
return useContext(TemplateContext).sectionData[id];
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font resolution for the mirror. The Liquid theme uses Shopify's
|
|
3
|
+
* font_picker (handles like "work_sans_n4") plus optional Google-font
|
|
4
|
+
* override text settings. Outside Shopify's font CDN we resolve picker
|
|
5
|
+
* handles to the same family served from Google Fonts, keeping the
|
|
6
|
+
* css-variables contract (--font-body--family etc.) identical.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResolvedFont {
|
|
9
|
+
family: string;
|
|
10
|
+
weight: number;
|
|
11
|
+
style: 'normal' | 'italic';
|
|
12
|
+
}
|
|
13
|
+
/** Parse a Shopify font handle: "work_sans_n4" → Work Sans / 400 / normal. */
|
|
14
|
+
export declare function parseFontHandle(handle: string | undefined): ResolvedFont;
|
|
15
|
+
export declare function googleFontsHref(family: string, axes?: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font resolution for the mirror. The Liquid theme uses Shopify's
|
|
3
|
+
* font_picker (handles like "work_sans_n4") plus optional Google-font
|
|
4
|
+
* override text settings. Outside Shopify's font CDN we resolve picker
|
|
5
|
+
* handles to the same family served from Google Fonts, keeping the
|
|
6
|
+
* css-variables contract (--font-body--family etc.) identical.
|
|
7
|
+
*/
|
|
8
|
+
/** Parse a Shopify font handle: "work_sans_n4" → Work Sans / 400 / normal. */
|
|
9
|
+
export function parseFontHandle(handle) {
|
|
10
|
+
const fallback = {
|
|
11
|
+
family: 'Work Sans',
|
|
12
|
+
weight: 400,
|
|
13
|
+
style: 'normal',
|
|
14
|
+
};
|
|
15
|
+
if (!handle)
|
|
16
|
+
return fallback;
|
|
17
|
+
const match = /^(.*)_([nib])(\d)$/.exec(handle);
|
|
18
|
+
if (!match)
|
|
19
|
+
return fallback;
|
|
20
|
+
const family = match[1]
|
|
21
|
+
.split('_')
|
|
22
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
23
|
+
.join(' ');
|
|
24
|
+
return {
|
|
25
|
+
family,
|
|
26
|
+
weight: Number(match[3]) * 100,
|
|
27
|
+
style: match[2] === 'i' ? 'italic' : 'normal',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function googleFontsHref(family, axes = 'ital,wght@0,400;0,500;0,700;1,400') {
|
|
31
|
+
const param = family.trim().replace(/ /g, '+');
|
|
32
|
+
return `https://fonts.googleapis.com/css2?family=${param}:${axes}&display=swap`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers mirroring the theme's image snippet / `image_url` usage for
|
|
3
|
+
* images that arrive from JSON settings (not the Storefront API — for
|
|
4
|
+
* those use @shopify/hydrogen's <Image>).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an image_picker setting value to a usable URL. Values saved
|
|
8
|
+
* by the theme editor look like "shopify://shop_images/foo.png", which
|
|
9
|
+
* only the Liquid runtime can resolve — those return null here so the
|
|
10
|
+
* component falls back to its placeholder (matching placeholder_svg_tag
|
|
11
|
+
* behavior on a fresh install).
|
|
12
|
+
*/
|
|
13
|
+
export declare function settingImageUrl(value: unknown): string | null;
|
|
14
|
+
/** Append a width to a Shopify CDN URL (mirror of `| image_url: width`). */
|
|
15
|
+
export declare function imageUrl(url: string, width?: number): string;
|
|
16
|
+
/** Srcset across the theme's standard width ladder. */
|
|
17
|
+
export declare function imageSrcSet(url: string, widths?: number[]): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Neutral placeholder mirroring Liquid's placeholder_svg_tag: a muted
|
|
20
|
+
* box that inherits the component's sizing classes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function PlaceholderSvg({ className }: {
|
|
23
|
+
className?: string;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers mirroring the theme's image snippet / `image_url` usage for
|
|
4
|
+
* images that arrive from JSON settings (not the Storefront API — for
|
|
5
|
+
* those use @shopify/hydrogen's <Image>).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Resolve an image_picker setting value to a usable URL. Values saved
|
|
9
|
+
* by the theme editor look like "shopify://shop_images/foo.png", which
|
|
10
|
+
* only the Liquid runtime can resolve — those return null here so the
|
|
11
|
+
* component falls back to its placeholder (matching placeholder_svg_tag
|
|
12
|
+
* behavior on a fresh install).
|
|
13
|
+
*/
|
|
14
|
+
export function settingImageUrl(value) {
|
|
15
|
+
if (typeof value !== 'string' || !value)
|
|
16
|
+
return null;
|
|
17
|
+
if (value.startsWith('shopify://'))
|
|
18
|
+
return null;
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
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
|
+
}
|
|
28
|
+
/** Srcset across the theme's standard width ladder. */
|
|
29
|
+
export function imageSrcSet(url, widths = [400, 700, 1000, 1500]) {
|
|
30
|
+
if (!url.includes('cdn.shopify.com'))
|
|
31
|
+
return undefined;
|
|
32
|
+
return widths.map((w) => `${imageUrl(url, w)} ${w}w`).join(', ');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Neutral placeholder mirroring Liquid's placeholder_svg_tag: a muted
|
|
36
|
+
* box that inherits the component's sizing classes.
|
|
37
|
+
*/
|
|
38
|
+
export function PlaceholderSvg({ className }) {
|
|
39
|
+
return (_jsxs("svg", { className: className, viewBox: "0 0 525 525", xmlns: "http://www.w3.org/2000/svg", role: "img", "aria-hidden": "true", preserveAspectRatio: "xMidYMid slice", children: [_jsx("rect", { width: "525", height: "525", fill: "currentColor", opacity: "0.06" }), _jsx("path", { d: "M262.5 138a52 52 0 1 1 0 104 52 52 0 0 1 0-104Zm-131 249c0-58 59-90 131-90s131 32 131 90v0H131.5v0Z", fill: "currentColor", opacity: "0.12" })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SectionData, TemplateData } from './types';
|
|
2
|
+
/** The scope half of a ThemeJsonPath, threaded through rendering. */
|
|
3
|
+
export interface RenderScope {
|
|
4
|
+
kind: 'template' | 'group';
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function RenderSection({ id, section, scope, }: {
|
|
8
|
+
id: string;
|
|
9
|
+
section: SectionData;
|
|
10
|
+
scope?: RenderScope;
|
|
11
|
+
}): import("react").JSX.Element | null;
|
|
12
|
+
export declare function RenderSections({ template, scope, }: {
|
|
13
|
+
template: TemplateData;
|
|
14
|
+
scope?: RenderScope;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
/**
|
|
17
|
+
* Render a page template by name with its route resources and
|
|
18
|
+
* per-section loader data (from loadSectionData in the server module).
|
|
19
|
+
*/
|
|
20
|
+
export declare function ThemeTemplate({ name, resources, sectionData, }: {
|
|
21
|
+
name: string;
|
|
22
|
+
resources?: Record<string, unknown>;
|
|
23
|
+
sectionData?: Record<string, unknown>;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
/** Mirror of `{% sections 'header-group' %}` in layout/theme.liquid. */
|
|
26
|
+
export declare function SectionGroup({ name, sectionData, }: {
|
|
27
|
+
name: string;
|
|
28
|
+
sectionData?: Record<string, unknown>;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The rendering mirror of Shopify's section/blocks runtime:
|
|
4
|
+
*
|
|
5
|
+
* - <ThemeTemplate name="index" …/> ≙ {{ content_for_layout }} for a JSON template
|
|
6
|
+
* - <SectionGroup name="header-group"/> ≙ {% sections 'header-group' %}
|
|
7
|
+
* - Child blocks arrive as `children` ≙ {% content_for 'blocks' %}
|
|
8
|
+
*
|
|
9
|
+
* Sections are wrapped in `.shopify-section` divs and blocks in
|
|
10
|
+
* `.shopify-block` divs, matching the wrappers Shopify emits — the
|
|
11
|
+
* extracted component CSS relies on them.
|
|
12
|
+
*
|
|
13
|
+
* Every wrapper also carries a `data-z-path` attribute (the wire form
|
|
14
|
+
* of a @zalify/storefront-kit/schemas ThemeJsonPath) identifying the node in
|
|
15
|
+
* the theme JSON. The canvas-editor bridge hit-tests against it; it is
|
|
16
|
+
* emitted unconditionally so server and client markup stay identical.
|
|
17
|
+
*/
|
|
18
|
+
import { formatThemePath, DATA_PATH_ATTR } from "../../schemas/index.js";
|
|
19
|
+
import { getSectionGroup, getTemplate, getThemeStore } from './store';
|
|
20
|
+
import { SectionProvider, TemplateProvider } from './context';
|
|
21
|
+
const warned = new Set();
|
|
22
|
+
function warnMissing(kind, type) {
|
|
23
|
+
const key = `${kind}:${type}`;
|
|
24
|
+
if (warned.has(key))
|
|
25
|
+
return;
|
|
26
|
+
warned.add(key);
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.warn(`[theme] No ${kind} component registered for type "${type}"`);
|
|
29
|
+
}
|
|
30
|
+
function pathAttr(scope, sectionKey, blockKeys) {
|
|
31
|
+
if (!scope)
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
[DATA_PATH_ATTR]: formatThemePath({ scope, sectionKey, blockKeys }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function RenderBlocks({ container, sectionId, scope, parentBlockKeys = [], }) {
|
|
38
|
+
const { blocks: blockRegistry } = getThemeStore();
|
|
39
|
+
const order = container.block_order ?? [];
|
|
40
|
+
if (!order.length || !container.blocks)
|
|
41
|
+
return null;
|
|
42
|
+
const visible = order.filter((key) => {
|
|
43
|
+
const block = container.blocks[key];
|
|
44
|
+
return block && !block.disabled;
|
|
45
|
+
});
|
|
46
|
+
return (_jsx(_Fragment, { children: visible.map((key, index) => {
|
|
47
|
+
const block = container.blocks[key];
|
|
48
|
+
const Component = blockRegistry[block.type];
|
|
49
|
+
if (!Component) {
|
|
50
|
+
warnMissing('block', block.type);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const blockKeys = [...parentBlockKeys, key];
|
|
54
|
+
return (_jsx("div", { className: "shopify-block", "data-block-type": block.type, ...pathAttr(scope, sectionId, blockKeys), children: _jsx(Component, { id: key, block: block, settings: block.settings ?? {}, index: index, total: visible.length, sectionId: sectionId, children: _jsx(RenderBlocks, { container: block, sectionId: sectionId, scope: scope, parentBlockKeys: blockKeys }) }) }, key));
|
|
55
|
+
}) }));
|
|
56
|
+
}
|
|
57
|
+
export function RenderSection({ id, section, scope, }) {
|
|
58
|
+
if (section.disabled)
|
|
59
|
+
return null;
|
|
60
|
+
const Component = getThemeStore().sections[section.type];
|
|
61
|
+
if (!Component) {
|
|
62
|
+
warnMissing('section', section.type);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return (_jsx("div", { id: `shopify-section-${id}`, className: `shopify-section shopify-section--${section.type}`, ...pathAttr(scope, id, []), children: _jsx(SectionProvider, { id: id, children: _jsx(Component, { id: id, section: section, settings: section.settings ?? {}, children: _jsx(RenderBlocks, { container: section, sectionId: id, scope: scope }) }) }) }));
|
|
66
|
+
}
|
|
67
|
+
export function RenderSections({ template, scope, }) {
|
|
68
|
+
return (_jsx(_Fragment, { children: template.order.map((key) => {
|
|
69
|
+
const section = template.sections[key];
|
|
70
|
+
if (!section)
|
|
71
|
+
return null;
|
|
72
|
+
return (_jsx(RenderSection, { id: key, section: section, scope: scope }, key));
|
|
73
|
+
}) }));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Render a page template by name with its route resources and
|
|
77
|
+
* per-section loader data (from loadSectionData in the server module).
|
|
78
|
+
*/
|
|
79
|
+
export function ThemeTemplate({ name, resources = {}, sectionData = {}, }) {
|
|
80
|
+
const template = getTemplate(name);
|
|
81
|
+
return (_jsx(TemplateProvider, { value: { name, kind: 'template', resources, sectionData }, children: _jsx(RenderSections, { template: template, scope: { kind: 'template', name } }) }));
|
|
82
|
+
}
|
|
83
|
+
/** Mirror of `{% sections 'header-group' %}` in layout/theme.liquid. */
|
|
84
|
+
export function SectionGroup({ name, sectionData = {}, }) {
|
|
85
|
+
const group = getSectionGroup(name);
|
|
86
|
+
return (_jsx(TemplateProvider, { value: { name, kind: 'group', resources: {}, sectionData }, children: _jsx(RenderSections, { template: group, scope: { kind: 'group', name } }) }));
|
|
87
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ColorScheme, ThemeSettings } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Global theme settings. A lazy proxy so call sites can keep reading
|
|
4
|
+
* `themeSettings.card_density` while resolution waits for installTheme().
|
|
5
|
+
*/
|
|
6
|
+
export declare const themeSettings: ThemeSettings;
|
|
7
|
+
export declare function colorSchemes(): ColorScheme[];
|
|
8
|
+
export declare function getColorScheme(id: string | undefined): ColorScheme;
|