@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,37 @@
|
|
|
1
|
+
/** Port of blocks/_box.liquid — hero content box. CSS: blocks-_box.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
|
|
4
|
+
interface BoxSettings {
|
|
5
|
+
position_x?: 'start' | 'center' | 'end';
|
|
6
|
+
position_y?: 'start' | 'center' | 'end';
|
|
7
|
+
color_scheme?: string;
|
|
8
|
+
backdrop?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function BoxBlock({settings, children}: BlockProps<BoxSettings>) {
|
|
12
|
+
const {
|
|
13
|
+
position_x: positionX = 'start',
|
|
14
|
+
position_y: positionY = 'center',
|
|
15
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
16
|
+
backdrop = false,
|
|
17
|
+
} = settings;
|
|
18
|
+
const className = [
|
|
19
|
+
'hero__box',
|
|
20
|
+
`hero__box--x-${positionX}`,
|
|
21
|
+
`hero__box--y-${positionY}`,
|
|
22
|
+
`color-${colorScheme}`,
|
|
23
|
+
backdrop ? 'hero__box--backdrop' : '',
|
|
24
|
+
]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ');
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={className}
|
|
30
|
+
// Inline transparent: the scheme class paints via an unlayered rule
|
|
31
|
+
// that outranks any @layer override (mirror of _box.liquid).
|
|
32
|
+
style={backdrop ? undefined : {backgroundColor: 'transparent'}}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of blocks/_slide.liquid — hero slide: full-bleed media canvas.
|
|
3
|
+
* Content lives in nested _box blocks. CSS: blocks-_slide.css
|
|
4
|
+
*/
|
|
5
|
+
import type {BlockProps} from '../engine/types';
|
|
6
|
+
import {imageUrl, PlaceholderSvg, settingImageUrl} from '../engine/images';
|
|
7
|
+
|
|
8
|
+
interface SlideSettings {
|
|
9
|
+
image?: string;
|
|
10
|
+
mobile_image?: string;
|
|
11
|
+
video?: {
|
|
12
|
+
sources?: Array<{url: string; format: string; height: number; mime_type: string}>;
|
|
13
|
+
preview_image?: {src?: string};
|
|
14
|
+
};
|
|
15
|
+
mobile_video?: SlideSettings['video'];
|
|
16
|
+
link?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function bestMp4(video: SlideSettings['video']) {
|
|
20
|
+
const mp4s = (video?.sources ?? []).filter((s) => s.format === 'mp4');
|
|
21
|
+
return mp4s.sort((a, b) => a.height - b.height).at(-1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function SlideBlock({
|
|
25
|
+
settings,
|
|
26
|
+
index,
|
|
27
|
+
children,
|
|
28
|
+
}: BlockProps<SlideSettings>) {
|
|
29
|
+
// First slide is the LCP candidate: eager + high priority.
|
|
30
|
+
const isFirst = index === 0;
|
|
31
|
+
const image = settingImageUrl(settings.image);
|
|
32
|
+
const mobileImage = settingImageUrl(settings.mobile_image);
|
|
33
|
+
const desktopMp4 = bestMp4(settings.video);
|
|
34
|
+
const mobileMp4 = bestMp4(settings.mobile_video);
|
|
35
|
+
// Poster paints video slides before any video bytes arrive
|
|
36
|
+
const poster =
|
|
37
|
+
image ?? mobileImage ?? settingImageUrl(settings.video?.preview_image?.src);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="hero__slide">
|
|
41
|
+
{desktopMp4 || mobileMp4 ? (
|
|
42
|
+
<video
|
|
43
|
+
className="hero__media"
|
|
44
|
+
autoPlay
|
|
45
|
+
loop
|
|
46
|
+
muted
|
|
47
|
+
playsInline
|
|
48
|
+
preload="metadata"
|
|
49
|
+
poster={poster ? imageUrl(poster, 1800) : undefined}
|
|
50
|
+
>
|
|
51
|
+
{mobileMp4 && (
|
|
52
|
+
<source
|
|
53
|
+
media="(max-width: 47.9375rem)"
|
|
54
|
+
src={mobileMp4.url}
|
|
55
|
+
type={mobileMp4.mime_type}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
{desktopMp4 && (
|
|
59
|
+
<source src={desktopMp4.url} type={desktopMp4.mime_type} />
|
|
60
|
+
)}
|
|
61
|
+
</video>
|
|
62
|
+
) : image ? (
|
|
63
|
+
<picture>
|
|
64
|
+
{mobileImage && (
|
|
65
|
+
<source
|
|
66
|
+
media="(max-width: 47.9375rem)"
|
|
67
|
+
srcSet={[400, 750, 1100, 1500]
|
|
68
|
+
.map((w) => `${imageUrl(mobileImage, w)} ${w}w`)
|
|
69
|
+
.join(', ')}
|
|
70
|
+
sizes="100vw"
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
<img
|
|
74
|
+
className="hero__media"
|
|
75
|
+
src={imageUrl(image, 3000)}
|
|
76
|
+
srcSet={[750, 1100, 1500, 2200, 3000]
|
|
77
|
+
.map((w) => `${imageUrl(image, w)} ${w}w`)
|
|
78
|
+
.join(', ')}
|
|
79
|
+
sizes="100vw"
|
|
80
|
+
loading={isFirst ? 'eager' : 'lazy'}
|
|
81
|
+
fetchPriority={isFirst ? 'high' : 'auto'}
|
|
82
|
+
alt=""
|
|
83
|
+
/>
|
|
84
|
+
</picture>
|
|
85
|
+
) : (
|
|
86
|
+
<PlaceholderSvg className="hero__media hero__media--placeholder" />
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{settings.link ? (
|
|
90
|
+
<a className="hero__media-link" href={settings.link} aria-label="View" />
|
|
91
|
+
) : null}
|
|
92
|
+
|
|
93
|
+
<div className="hero__content">{children}</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of blocks/_video-card.liquid — video carousel card: a muted
|
|
3
|
+
* preview loop that expands into the shared video modal (main video
|
|
4
|
+
* with sound + optional product). Private block of
|
|
5
|
+
* sections/video-carousel.liquid. Behavior from the <video-carousel>
|
|
6
|
+
* island in src/entries/video.ts.
|
|
7
|
+
* CSS: app/styles/components/blocks-_video-card.css
|
|
8
|
+
*/
|
|
9
|
+
import {useState} from 'react';
|
|
10
|
+
import type {BlockProps} from '../engine/types';
|
|
11
|
+
import {t} from '../engine/translate';
|
|
12
|
+
import {Icon} from '../components/Icon';
|
|
13
|
+
import {imageUrl, PlaceholderSvg, settingImageUrl} from '../engine/images';
|
|
14
|
+
import {formatMoney} from '../../commerce/index.ts';
|
|
15
|
+
import {
|
|
16
|
+
mp4Sources,
|
|
17
|
+
productImageUrl,
|
|
18
|
+
resolveVideoProduct,
|
|
19
|
+
useViewportPlayback,
|
|
20
|
+
VideoModal,
|
|
21
|
+
videoPreviewImage,
|
|
22
|
+
type ThemeVideo,
|
|
23
|
+
} from '../components/VideoModal';
|
|
24
|
+
|
|
25
|
+
interface VideoCardSettings {
|
|
26
|
+
preview_video?: ThemeVideo;
|
|
27
|
+
video?: ThemeVideo;
|
|
28
|
+
image?: string;
|
|
29
|
+
product?: unknown;
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cards render ~300px wide: the smallest rendition from 480p up is
|
|
35
|
+
* plenty and keeps a many-card strip light (mirror of the Liquid loop).
|
|
36
|
+
*/
|
|
37
|
+
function previewMp4For(video: ThemeVideo | undefined) {
|
|
38
|
+
const sources = mp4Sources(video);
|
|
39
|
+
return sources.find((s) => (s.height ?? 0) >= 480) ?? sources.at(-1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function VideoCardBlock({
|
|
43
|
+
settings,
|
|
44
|
+
}: BlockProps<VideoCardSettings>) {
|
|
45
|
+
const preview = settings.preview_video;
|
|
46
|
+
const mainVideo = settings.video ?? preview;
|
|
47
|
+
const poster =
|
|
48
|
+
settingImageUrl(settings.image) ??
|
|
49
|
+
videoPreviewImage(preview) ??
|
|
50
|
+
videoPreviewImage(mainVideo);
|
|
51
|
+
const previewMp4 = previewMp4For(preview);
|
|
52
|
+
const product = resolveVideoProduct(settings.product);
|
|
53
|
+
const productImage = productImageUrl(product?.featured_image);
|
|
54
|
+
const label = settings.label;
|
|
55
|
+
|
|
56
|
+
const previewRef = useViewportPlayback();
|
|
57
|
+
const [open, setOpen] = useState(false);
|
|
58
|
+
|
|
59
|
+
const triggerLabel =
|
|
60
|
+
t('general.play_video') +
|
|
61
|
+
(product?.title ? `: ${product.title}` : label ? `: ${label}` : '');
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="video-card">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className="video-card__trigger"
|
|
68
|
+
onClick={() => setOpen(true)}
|
|
69
|
+
aria-label={triggerLabel}
|
|
70
|
+
>
|
|
71
|
+
{previewMp4 ? (
|
|
72
|
+
<video
|
|
73
|
+
ref={previewRef}
|
|
74
|
+
className="video-card__media"
|
|
75
|
+
autoPlay
|
|
76
|
+
loop
|
|
77
|
+
muted
|
|
78
|
+
playsInline
|
|
79
|
+
preload="metadata"
|
|
80
|
+
data-video-preview=""
|
|
81
|
+
poster={poster ? imageUrl(poster, 800) : undefined}
|
|
82
|
+
>
|
|
83
|
+
<source src={previewMp4.url} type={previewMp4.mime_type} />
|
|
84
|
+
</video>
|
|
85
|
+
) : poster ? (
|
|
86
|
+
<img
|
|
87
|
+
className="video-card__media"
|
|
88
|
+
src={imageUrl(poster, 800)}
|
|
89
|
+
loading="lazy"
|
|
90
|
+
alt=""
|
|
91
|
+
/>
|
|
92
|
+
) : (
|
|
93
|
+
<PlaceholderSvg className="video-card__media video-card__media--placeholder" />
|
|
94
|
+
)}
|
|
95
|
+
<span className="video-card__play">
|
|
96
|
+
<Icon name="icon-play" />
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{product ? (
|
|
101
|
+
<a className="video-card__product" href={product.url}>
|
|
102
|
+
{productImage ? (
|
|
103
|
+
<img
|
|
104
|
+
className="video-card__product-image"
|
|
105
|
+
src={imageUrl(productImage, 96)}
|
|
106
|
+
loading="lazy"
|
|
107
|
+
alt=""
|
|
108
|
+
/>
|
|
109
|
+
) : null}
|
|
110
|
+
<span className="video-card__product-info">
|
|
111
|
+
<span className="video-card__product-title">{product.title}</span>
|
|
112
|
+
<span className="video-card__product-price">
|
|
113
|
+
{formatMoney(product.price)}
|
|
114
|
+
</span>
|
|
115
|
+
</span>
|
|
116
|
+
</a>
|
|
117
|
+
) : label ? (
|
|
118
|
+
<span className="video-card__label">{label}</span>
|
|
119
|
+
) : null}
|
|
120
|
+
|
|
121
|
+
<VideoModal
|
|
122
|
+
open={open}
|
|
123
|
+
onClose={() => setOpen(false)}
|
|
124
|
+
video={mainVideo}
|
|
125
|
+
poster={poster}
|
|
126
|
+
product={product}
|
|
127
|
+
label={label}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of blocks/accordion.liquid — a <details>-based accordion with a
|
|
3
|
+
* heading and rich-text content.
|
|
4
|
+
* CSS: app/styles/components/blocks-accordion.css
|
|
5
|
+
*/
|
|
6
|
+
import type {BlockProps} from '../engine/types';
|
|
7
|
+
|
|
8
|
+
interface AccordionSettings {
|
|
9
|
+
heading?: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function AccordionBlock({
|
|
14
|
+
settings,
|
|
15
|
+
}: BlockProps<AccordionSettings>) {
|
|
16
|
+
const {
|
|
17
|
+
heading = 'Details',
|
|
18
|
+
content = '<p>Share details about ingredients, sizing, shipping, or care.</p>',
|
|
19
|
+
} = settings;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<details className="accordion">
|
|
23
|
+
<summary className="accordion__summary">
|
|
24
|
+
<span>{heading}</span>
|
|
25
|
+
<span className="accordion__icon" aria-hidden="true" />
|
|
26
|
+
</summary>
|
|
27
|
+
<div
|
|
28
|
+
className="accordion__content"
|
|
29
|
+
dangerouslySetInnerHTML={{__html: content}}
|
|
30
|
+
/>
|
|
31
|
+
</details>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Port of blocks/button.liquid (button styles live in critical.css). */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
import {useAdapter} from '../adapter';
|
|
4
|
+
|
|
5
|
+
interface ButtonSettings {
|
|
6
|
+
label?: string;
|
|
7
|
+
link?: string;
|
|
8
|
+
style?: '' | 'button--secondary';
|
|
9
|
+
size?: '' | 'button--sm' | 'button--lg';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ButtonBlock({settings}: BlockProps<ButtonSettings>) {
|
|
13
|
+
const {Link} = useAdapter();
|
|
14
|
+
const {label = 'Shop now', link, style = '', size = ''} = settings;
|
|
15
|
+
const className = `button ${style} ${size}`.replace(/\s+/g, ' ').trim();
|
|
16
|
+
if (!link) {
|
|
17
|
+
return (
|
|
18
|
+
<a className={className} role="link" aria-disabled="true">
|
|
19
|
+
{label}
|
|
20
|
+
</a>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const isExternal = /^https?:\/\//.test(link);
|
|
24
|
+
if (isExternal) {
|
|
25
|
+
return (
|
|
26
|
+
<a className={className} href={link}>
|
|
27
|
+
{label}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
<Link className={className} to={link}>
|
|
33
|
+
{label}
|
|
34
|
+
</Link>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of blocks/countdown.liquid + the <countdown-timer> island
|
|
3
|
+
* (src/entries/countdown.ts): server renders dash placeholders, a
|
|
4
|
+
* 1-second tick fills the digit slots and flips visibility when the
|
|
5
|
+
* deadline passes. CSS: app/styles/components/blocks-countdown.css
|
|
6
|
+
*/
|
|
7
|
+
import {useEffect, useState} from 'react';
|
|
8
|
+
// Clock math shared with the theme's <countdown-timer> island.
|
|
9
|
+
import {
|
|
10
|
+
pad2 as pad,
|
|
11
|
+
parseEndTime as parseEnd,
|
|
12
|
+
secondsUntil,
|
|
13
|
+
} from '../../commerce/index.ts';
|
|
14
|
+
import type {BlockProps} from '../engine/types';
|
|
15
|
+
import {t} from '../engine/translate';
|
|
16
|
+
|
|
17
|
+
interface CountdownSettings {
|
|
18
|
+
end_time?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
completed_text?: string;
|
|
21
|
+
alignment?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function CountdownBlock({
|
|
25
|
+
settings,
|
|
26
|
+
}: BlockProps<CountdownSettings>) {
|
|
27
|
+
const {label = 'Ends in', completed_text: completedText, alignment = 'left'} =
|
|
28
|
+
settings;
|
|
29
|
+
const endTime = (settings.end_time ?? '').trim();
|
|
30
|
+
|
|
31
|
+
// null until the first client tick — SSR shows the "–" placeholders,
|
|
32
|
+
// exactly like the Liquid markup before countdown.js runs.
|
|
33
|
+
const [remain, setRemain] = useState<number | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!endTime) return;
|
|
37
|
+
const end = parseEnd(endTime);
|
|
38
|
+
if (Number.isNaN(end)) return;
|
|
39
|
+
let interval = 0;
|
|
40
|
+
const tick = () => {
|
|
41
|
+
const next = secondsUntil(end);
|
|
42
|
+
setRemain(next);
|
|
43
|
+
if (next <= 0 && interval) clearInterval(interval);
|
|
44
|
+
return next;
|
|
45
|
+
};
|
|
46
|
+
if (tick() > 0) interval = window.setInterval(tick, 1000);
|
|
47
|
+
return () => clearInterval(interval);
|
|
48
|
+
}, [endTime]);
|
|
49
|
+
|
|
50
|
+
if (!endTime) return <div hidden />;
|
|
51
|
+
|
|
52
|
+
const done = remain !== null && remain <= 0;
|
|
53
|
+
const days = remain === null ? null : Math.floor(remain / 86400);
|
|
54
|
+
|
|
55
|
+
// No completion message configured: the block just goes away.
|
|
56
|
+
if (done && !completedText) return <div className="countdown" hidden />;
|
|
57
|
+
|
|
58
|
+
const units: Array<{value: string; caption: string; hidden?: boolean}> = [
|
|
59
|
+
{
|
|
60
|
+
value: days === null ? '–' : pad(days),
|
|
61
|
+
caption: t('general.countdown.days'),
|
|
62
|
+
// Under a day, the days unit is dead weight — drop it
|
|
63
|
+
hidden: days === 0,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: remain === null ? '–' : pad(Math.floor(remain / 3600) % 24),
|
|
67
|
+
caption: t('general.countdown.hours'),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
value: remain === null ? '–' : pad(Math.floor(remain / 60) % 60),
|
|
71
|
+
caption: t('general.countdown.minutes'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
value: remain === null ? '–' : pad(remain % 60),
|
|
75
|
+
caption: t('general.countdown.seconds'),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className="countdown"
|
|
82
|
+
style={{'--countdown-justify': alignment} as React.CSSProperties}
|
|
83
|
+
role="timer"
|
|
84
|
+
>
|
|
85
|
+
{label ? <span className="countdown__label">{label}</span> : null}
|
|
86
|
+
<span className="countdown__units" hidden={done}>
|
|
87
|
+
{units.map((unit) => (
|
|
88
|
+
<span
|
|
89
|
+
key={unit.caption}
|
|
90
|
+
className="countdown__unit"
|
|
91
|
+
hidden={unit.hidden}
|
|
92
|
+
>
|
|
93
|
+
<span className="countdown__value">{unit.value}</span>
|
|
94
|
+
<span className="countdown__caption">{unit.caption}</span>
|
|
95
|
+
</span>
|
|
96
|
+
))}
|
|
97
|
+
</span>
|
|
98
|
+
{completedText ? (
|
|
99
|
+
<span className="countdown__completed" hidden={!done}>
|
|
100
|
+
{completedText}
|
|
101
|
+
</span>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of blocks/custom-liquid.liquid — merchant-pasted markup inside
|
|
3
|
+
* any section that accepts theme blocks. Raw HTML is injected as-is;
|
|
4
|
+
* Liquid tags/objects in it cannot execute here (no Liquid runtime).
|
|
5
|
+
*/
|
|
6
|
+
import type {BlockProps} from '../engine/types';
|
|
7
|
+
|
|
8
|
+
interface CustomLiquidBlockSettings {
|
|
9
|
+
custom_liquid?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function CustomLiquidBlock({
|
|
13
|
+
settings,
|
|
14
|
+
}: BlockProps<CustomLiquidBlockSettings>) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="custom-liquid-block"
|
|
18
|
+
dangerouslySetInnerHTML={{__html: settings.custom_liquid ?? ''}}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Port of blocks/group.liquid. CSS: app/styles/components/blocks-group.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
|
|
4
|
+
interface GroupSettings {
|
|
5
|
+
layout_direction?: 'group--horizontal' | 'group--vertical';
|
|
6
|
+
alignment?: string;
|
|
7
|
+
padding?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function GroupBlock({
|
|
11
|
+
settings,
|
|
12
|
+
children,
|
|
13
|
+
}: BlockProps<GroupSettings>) {
|
|
14
|
+
const {
|
|
15
|
+
layout_direction: direction = 'group--vertical',
|
|
16
|
+
alignment = 'flex-start',
|
|
17
|
+
padding = 0,
|
|
18
|
+
} = settings;
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`group ${direction}`}
|
|
22
|
+
style={
|
|
23
|
+
{
|
|
24
|
+
'--padding': `${padding}px`,
|
|
25
|
+
'--alignment': alignment,
|
|
26
|
+
} as React.CSSProperties
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Port of blocks/image.liquid. CSS: app/styles/components/blocks-image.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
import {imageSrcSet, imageUrl, PlaceholderSvg, settingImageUrl} from '../engine/images';
|
|
4
|
+
|
|
5
|
+
interface ImageSettings {
|
|
6
|
+
image?: string;
|
|
7
|
+
aspect?: 'auto' | 'square' | 'portrait' | 'landscape';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ImageBlock({settings}: BlockProps<ImageSettings>) {
|
|
11
|
+
const {aspect = 'auto'} = settings;
|
|
12
|
+
const src = settingImageUrl(settings.image);
|
|
13
|
+
return (
|
|
14
|
+
<div className={`image-block image-block--${aspect}`}>
|
|
15
|
+
{src ? (
|
|
16
|
+
<img
|
|
17
|
+
className="image-block__image"
|
|
18
|
+
src={imageUrl(src, 1500)}
|
|
19
|
+
srcSet={imageSrcSet(src)}
|
|
20
|
+
sizes="(min-width: 48rem) 50vw, 100vw"
|
|
21
|
+
loading="lazy"
|
|
22
|
+
alt=""
|
|
23
|
+
/>
|
|
24
|
+
) : (
|
|
25
|
+
<PlaceholderSvg className="image-block__image image-block__placeholder" />
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The announcement bar's section-local 'message' block (defined inline
|
|
3
|
+
* in sections/announcement-bar.liquid's {% schema %}; data shape in
|
|
4
|
+
* sections/header-group.json). Only the first message is visible at
|
|
5
|
+
* SSR; the announcement-bar section rotates `hidden` afterwards.
|
|
6
|
+
* CSS: app/styles/components/sections-announcement-bar.css
|
|
7
|
+
*/
|
|
8
|
+
import type {BlockProps} from '../engine/types';
|
|
9
|
+
|
|
10
|
+
interface MessageSettings {
|
|
11
|
+
/** inline_richtext */
|
|
12
|
+
text?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function MessageBlock({
|
|
16
|
+
settings,
|
|
17
|
+
index,
|
|
18
|
+
}: BlockProps<MessageSettings>) {
|
|
19
|
+
const {text = 'Free shipping on orders over $50'} = settings;
|
|
20
|
+
return (
|
|
21
|
+
<p className="announcement-bar__message" hidden={index !== 0}>
|
|
22
|
+
<span
|
|
23
|
+
className="announcement-bar__text"
|
|
24
|
+
dangerouslySetInnerHTML={{__html: text}}
|
|
25
|
+
/>
|
|
26
|
+
</p>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Port of blocks/quote.liquid. CSS: app/styles/components/blocks-quote.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
|
|
4
|
+
interface QuoteSettings {
|
|
5
|
+
/** Richtext setting — HTML string. */
|
|
6
|
+
quote?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function QuoteBlock({settings}: BlockProps<QuoteSettings>) {
|
|
11
|
+
const {
|
|
12
|
+
quote = '<p>Add a customer quote here to build trust.</p>',
|
|
13
|
+
author,
|
|
14
|
+
} = settings;
|
|
15
|
+
return (
|
|
16
|
+
<figure className="quote stack">
|
|
17
|
+
<blockquote
|
|
18
|
+
className="quote__text"
|
|
19
|
+
dangerouslySetInnerHTML={{__html: quote}}
|
|
20
|
+
/>
|
|
21
|
+
{author ? <figcaption className="quote__author">{author}</figcaption> : null}
|
|
22
|
+
</figure>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Port of blocks/spacer.liquid. CSS: app/styles/components/blocks-spacer.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
|
|
4
|
+
interface SpacerSettings {
|
|
5
|
+
size?: number;
|
|
6
|
+
fill?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function SpacerBlock({settings}: BlockProps<SpacerSettings>) {
|
|
10
|
+
const {size = 40, fill = false} = settings;
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={`spacer${fill ? ' spacer--fill' : ''}`}
|
|
14
|
+
style={{'--spacer-size': `${size}px`} as React.CSSProperties}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of the section-local `story` block from sections/stories.liquid:
|
|
3
|
+
* a circular/square thumbnail with an optional label and link.
|
|
4
|
+
* CSS: app/styles/components/sections-stories.css (.story rules)
|
|
5
|
+
*
|
|
6
|
+
* The Liquid renders the image at width = desktop_size * 2; block props
|
|
7
|
+
* don't carry section settings, so a fixed 2x ladder around the maximum
|
|
8
|
+
* desktop size (240px) is used instead.
|
|
9
|
+
*/
|
|
10
|
+
import type {BlockProps} from '../engine/types';
|
|
11
|
+
import {imageUrl, PlaceholderSvg, settingImageUrl} from '../engine/images';
|
|
12
|
+
|
|
13
|
+
interface StorySettings {
|
|
14
|
+
image?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
link?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function StoryBlock({settings}: BlockProps<StorySettings>) {
|
|
20
|
+
const image = settingImageUrl(settings.image);
|
|
21
|
+
const label = settings.label ?? '';
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<a className="story" role="listitem" href={settings.link || undefined}>
|
|
25
|
+
<span className="story__media">
|
|
26
|
+
{image ? (
|
|
27
|
+
<img
|
|
28
|
+
className="story__image"
|
|
29
|
+
src={imageUrl(image, 480)}
|
|
30
|
+
loading="lazy"
|
|
31
|
+
alt={label}
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<PlaceholderSvg className="story__image story__image--placeholder" />
|
|
35
|
+
)}
|
|
36
|
+
</span>
|
|
37
|
+
{label ? <span className="story__label">{label}</span> : null}
|
|
38
|
+
</a>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Port of blocks/text.liquid. CSS: app/styles/components/blocks-text.css */
|
|
2
|
+
import type {BlockProps} from '../engine/types';
|
|
3
|
+
|
|
4
|
+
interface TextSettings {
|
|
5
|
+
text?: string;
|
|
6
|
+
text_style?: string;
|
|
7
|
+
alignment?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function TextBlock({settings}: BlockProps<TextSettings>) {
|
|
11
|
+
const {
|
|
12
|
+
text = 'Text',
|
|
13
|
+
text_style: textStyle = 'text--title',
|
|
14
|
+
alignment = 'left',
|
|
15
|
+
} = settings;
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={`text ${textStyle}`}
|
|
19
|
+
style={{'--text-align': alignment} as React.CSSProperties}
|
|
20
|
+
>
|
|
21
|
+
{text}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|