@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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/featured-collection.liquid — heading + product grid.
|
|
3
|
+
* Products come from this section's server loader
|
|
4
|
+
* (featured-collection.server.ts) via useSectionData(); without data it
|
|
5
|
+
* renders the Liquid onboarding placeholders.
|
|
6
|
+
* CSS: app/styles/components/sections-featured-collection.css
|
|
7
|
+
*/
|
|
8
|
+
import {ProductCard} from '../components/ProductCard';
|
|
9
|
+
import type {CardProduct} from '../../commerce/index.ts';
|
|
10
|
+
import {useSectionData} from '../engine/context';
|
|
11
|
+
import {PlaceholderSvg} from '../engine/images';
|
|
12
|
+
import {t} from '../engine/translate';
|
|
13
|
+
import type {SectionProps} from '../engine/types';
|
|
14
|
+
|
|
15
|
+
interface FeaturedCollectionSettings {
|
|
16
|
+
heading?: string;
|
|
17
|
+
collection?: string;
|
|
18
|
+
products_to_show?: number;
|
|
19
|
+
color_scheme?: string;
|
|
20
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FeaturedCollectionData {
|
|
24
|
+
collection?: {
|
|
25
|
+
handle: string;
|
|
26
|
+
title: string;
|
|
27
|
+
products?: {nodes?: CardProduct[]};
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function FeaturedCollectionSection({
|
|
32
|
+
settings,
|
|
33
|
+
}: SectionProps<FeaturedCollectionSettings>) {
|
|
34
|
+
const {
|
|
35
|
+
heading = 'Featured collection',
|
|
36
|
+
products_to_show: productsToShow = 4,
|
|
37
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
38
|
+
section_spacing: sectionSpacing = 'md',
|
|
39
|
+
} = settings;
|
|
40
|
+
const data = useSectionData<FeaturedCollectionData>();
|
|
41
|
+
const collection = data?.collection ?? null;
|
|
42
|
+
const products = (collection?.products?.nodes ?? []).slice(0, productsToShow);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`featured-collection section color-${colorScheme} full-width`}
|
|
47
|
+
style={
|
|
48
|
+
{
|
|
49
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
50
|
+
} as React.CSSProperties
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
className="featured-collection__inner stack"
|
|
55
|
+
style={{'--stack-gap': 'var(--space-xl)'} as React.CSSProperties}
|
|
56
|
+
>
|
|
57
|
+
<header className="featured-collection__header cluster">
|
|
58
|
+
{heading ? <h2>{heading}</h2> : null}
|
|
59
|
+
{collection ? (
|
|
60
|
+
<a
|
|
61
|
+
className="featured-collection__link"
|
|
62
|
+
href={`/collections/${collection.handle}`}
|
|
63
|
+
>
|
|
64
|
+
{t('collections.view_all')}
|
|
65
|
+
</a>
|
|
66
|
+
) : null}
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
<ul className="product-grid" role="list">
|
|
70
|
+
{products.length > 0
|
|
71
|
+
? products.map((product) => (
|
|
72
|
+
<li key={product.id}>
|
|
73
|
+
<ProductCard product={product} />
|
|
74
|
+
</li>
|
|
75
|
+
))
|
|
76
|
+
: Array.from({length: productsToShow}, (_, i) => (
|
|
77
|
+
<li key={i}>
|
|
78
|
+
<article className="product-card">
|
|
79
|
+
<div className="product-card__media">
|
|
80
|
+
<PlaceholderSvg className="product-card__placeholder" />
|
|
81
|
+
</div>
|
|
82
|
+
</article>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/hero.liquid — scroll-snap slideshow. The Liquid
|
|
3
|
+
* theme drives this with the <hero-slideshow> web component
|
|
4
|
+
* (src/entries/hero.ts); here the same behavior lives in React.
|
|
5
|
+
* CSS: app/styles/components/sections-hero.css
|
|
6
|
+
*/
|
|
7
|
+
import {useEffect, useRef, useState} from 'react';
|
|
8
|
+
import type {SectionProps} from '../engine/types';
|
|
9
|
+
import {t} from '../engine/translate';
|
|
10
|
+
import {Icon} from '../components/Icon';
|
|
11
|
+
|
|
12
|
+
interface HeroSettings {
|
|
13
|
+
height?: 'sm' | 'md' | 'lg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function HeroSection({
|
|
17
|
+
section,
|
|
18
|
+
settings,
|
|
19
|
+
children,
|
|
20
|
+
}: SectionProps<HeroSettings>) {
|
|
21
|
+
const {height = 'md'} = settings;
|
|
22
|
+
const slideCount = section.block_order?.length ?? 0;
|
|
23
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const [current, setCurrent] = useState(0);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const track = trackRef.current;
|
|
28
|
+
if (!track || slideCount <= 1) return;
|
|
29
|
+
const onScroll = () => {
|
|
30
|
+
const index = Math.round(track.scrollLeft / track.clientWidth);
|
|
31
|
+
setCurrent(Math.max(0, Math.min(slideCount - 1, index)));
|
|
32
|
+
};
|
|
33
|
+
track.addEventListener('scroll', onScroll, {passive: true});
|
|
34
|
+
return () => track.removeEventListener('scroll', onScroll);
|
|
35
|
+
}, [slideCount]);
|
|
36
|
+
|
|
37
|
+
const goTo = (index: number) => {
|
|
38
|
+
const track = trackRef.current;
|
|
39
|
+
if (!track) return;
|
|
40
|
+
const clamped = ((index % slideCount) + slideCount) % slideCount;
|
|
41
|
+
track.scrollTo({left: clamped * track.clientWidth, behavior: 'smooth'});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section className={`hero hero--${height} full-width`}>
|
|
46
|
+
<div className="hero__track" ref={trackRef}>
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{slideCount > 1 && (
|
|
51
|
+
<>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="hero__arrow hero__arrow--prev"
|
|
55
|
+
onClick={() => goTo(current - 1)}
|
|
56
|
+
aria-label={t('general.previous')}
|
|
57
|
+
>
|
|
58
|
+
<Icon name="icon-chevron-left" />
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="hero__arrow hero__arrow--next"
|
|
63
|
+
onClick={() => goTo(current + 1)}
|
|
64
|
+
aria-label={t('general.next')}
|
|
65
|
+
>
|
|
66
|
+
<Icon name="icon-chevron-right" />
|
|
67
|
+
</button>
|
|
68
|
+
<div className="hero__dots">
|
|
69
|
+
{Array.from({length: slideCount}, (_, i) => (
|
|
70
|
+
<button
|
|
71
|
+
key={i}
|
|
72
|
+
type="button"
|
|
73
|
+
className="hero__dot"
|
|
74
|
+
onClick={() => goTo(i)}
|
|
75
|
+
aria-current={i === current || undefined}
|
|
76
|
+
aria-label={t('general.go_to_slide', {number: i + 1})}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/image-with-text.liquid — media column + block stack.
|
|
3
|
+
* CSS: app/styles/components/sections-image-with-text.css
|
|
4
|
+
*/
|
|
5
|
+
import type {SectionProps} from '../engine/types';
|
|
6
|
+
import {
|
|
7
|
+
imageSrcSet,
|
|
8
|
+
imageUrl,
|
|
9
|
+
PlaceholderSvg,
|
|
10
|
+
settingImageUrl,
|
|
11
|
+
} from '../engine/images';
|
|
12
|
+
|
|
13
|
+
interface ImageWithTextSettings {
|
|
14
|
+
image?: string;
|
|
15
|
+
image_position?: 'left' | 'right';
|
|
16
|
+
color_scheme?: string;
|
|
17
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function ImageWithTextSection({
|
|
21
|
+
settings,
|
|
22
|
+
children,
|
|
23
|
+
}: SectionProps<ImageWithTextSettings>) {
|
|
24
|
+
const {
|
|
25
|
+
image_position: imagePosition = 'left',
|
|
26
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
27
|
+
section_spacing: sectionSpacing = 'lg',
|
|
28
|
+
} = settings;
|
|
29
|
+
const image = settingImageUrl(settings.image);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={`image-with-text section color-${colorScheme} full-width`}
|
|
34
|
+
style={
|
|
35
|
+
{
|
|
36
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
37
|
+
} as React.CSSProperties
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
className={`image-with-text__inner image-with-text--media-${imagePosition}`}
|
|
42
|
+
>
|
|
43
|
+
<div className="image-with-text__media">
|
|
44
|
+
{image ? (
|
|
45
|
+
<img
|
|
46
|
+
className="image-with-text__image"
|
|
47
|
+
src={imageUrl(image, 1500)}
|
|
48
|
+
srcSet={imageSrcSet(image, [400, 700, 1000, 1500])}
|
|
49
|
+
sizes="(min-width: 48rem) 50vw, 100vw"
|
|
50
|
+
loading="lazy"
|
|
51
|
+
alt=""
|
|
52
|
+
/>
|
|
53
|
+
) : (
|
|
54
|
+
<PlaceholderSvg className="image-with-text__image image-with-text__placeholder" />
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="image-with-text__content stack">{children}</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/multicolumn.liquid — optional heading + responsive
|
|
3
|
+
* column grid of theme blocks (image, quote, text …) rendered via
|
|
4
|
+
* `children`. Defines no section-local blocks.
|
|
5
|
+
* CSS: app/styles/components/sections-multicolumn.css
|
|
6
|
+
*/
|
|
7
|
+
import type {SectionProps} from '../engine/types';
|
|
8
|
+
|
|
9
|
+
interface MulticolumnSettings {
|
|
10
|
+
heading?: string;
|
|
11
|
+
columns?: number;
|
|
12
|
+
color_scheme?: string;
|
|
13
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function MulticolumnSection({
|
|
17
|
+
settings,
|
|
18
|
+
children,
|
|
19
|
+
}: SectionProps<MulticolumnSettings>) {
|
|
20
|
+
const {
|
|
21
|
+
heading = '',
|
|
22
|
+
columns = 3,
|
|
23
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
24
|
+
section_spacing: sectionSpacing = 'lg',
|
|
25
|
+
} = settings;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={`multicolumn section color-${colorScheme} full-width`}
|
|
30
|
+
style={
|
|
31
|
+
{
|
|
32
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
33
|
+
'--columns': columns,
|
|
34
|
+
} as React.CSSProperties
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
className="multicolumn__inner stack"
|
|
39
|
+
style={{'--stack-gap': 'var(--space-xl)'} as React.CSSProperties}
|
|
40
|
+
>
|
|
41
|
+
{heading ? <h2 className="multicolumn__heading">{heading}</h2> : null}
|
|
42
|
+
<div className="multicolumn__grid">{children}</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/page.liquid. CSS: app/styles/components/sections-page.css
|
|
3
|
+
*
|
|
4
|
+
* Data: useResource('page') — the scaffold PAGE query shape
|
|
5
|
+
* ({handle, id, title, body}).
|
|
6
|
+
*/
|
|
7
|
+
import type {SectionProps} from '../engine/types';
|
|
8
|
+
import {useResource} from '../engine/context';
|
|
9
|
+
|
|
10
|
+
interface PageSettings {
|
|
11
|
+
color_scheme?: string;
|
|
12
|
+
section_spacing?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function PageSection({settings}: SectionProps<PageSettings>) {
|
|
16
|
+
const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
|
|
17
|
+
settings;
|
|
18
|
+
const page = useResource<any>('page');
|
|
19
|
+
|
|
20
|
+
if (!page) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`page section color-${colorScheme} full-width`}
|
|
25
|
+
style={
|
|
26
|
+
{
|
|
27
|
+
'--section-spacing': `var(--space-section-${spacing})`,
|
|
28
|
+
} as React.CSSProperties
|
|
29
|
+
}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
className="page__inner stack"
|
|
33
|
+
style={{'--stack-gap': 'var(--space-lg)'} as React.CSSProperties}
|
|
34
|
+
>
|
|
35
|
+
<h1 className="page__title">{page.title}</h1>
|
|
36
|
+
<div
|
|
37
|
+
className="prose"
|
|
38
|
+
dangerouslySetInnerHTML={{__html: page.body ?? ''}}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/password.liquid.
|
|
3
|
+
* CSS: app/styles/components/sections-password.css
|
|
4
|
+
*
|
|
5
|
+
* `{% form 'storefront_password' %}` posts to the Online Store password
|
|
6
|
+
* endpoint, which has no Hydrogen equivalent — the markup renders
|
|
7
|
+
* faithfully but submission is a no-op. TODO: wire to a route action if
|
|
8
|
+
* a storefront-password flow is ever needed.
|
|
9
|
+
*/
|
|
10
|
+
import type {SectionProps} from '../engine/types';
|
|
11
|
+
import {useThemeGlobals} from '../engine/context';
|
|
12
|
+
import {t} from '../engine/translate';
|
|
13
|
+
|
|
14
|
+
interface PasswordSettings {
|
|
15
|
+
color_scheme?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function PasswordSection({
|
|
19
|
+
settings,
|
|
20
|
+
}: SectionProps<PasswordSettings>) {
|
|
21
|
+
const {color_scheme: colorScheme = 'scheme-3'} = settings;
|
|
22
|
+
const globals = useThemeGlobals() as any;
|
|
23
|
+
const shop = globals.shop ?? globals.header?.shop;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={`password-page section color-${colorScheme} full-width`}>
|
|
27
|
+
<div className="password-page__card stack">
|
|
28
|
+
<p className="password-page__shop">{shop?.name ?? ''}</p>
|
|
29
|
+
<h1 className="password-page__title">{t('password.title')}</h1>
|
|
30
|
+
|
|
31
|
+
{shop?.passwordMessage ? (
|
|
32
|
+
<p className="password-page__message">{shop.passwordMessage}</p>
|
|
33
|
+
) : null}
|
|
34
|
+
|
|
35
|
+
{/* TODO: no storefront_password endpoint in Hydrogen — submission is a no-op. */}
|
|
36
|
+
<form
|
|
37
|
+
className="password-page__form stack"
|
|
38
|
+
method="post"
|
|
39
|
+
onSubmit={(event) => event.preventDefault()}
|
|
40
|
+
>
|
|
41
|
+
<label className="visually-hidden" htmlFor="PasswordInput">
|
|
42
|
+
{t('password.password')}
|
|
43
|
+
</label>
|
|
44
|
+
<div className="password-page__row">
|
|
45
|
+
<input
|
|
46
|
+
id="PasswordInput"
|
|
47
|
+
type="password"
|
|
48
|
+
name="password"
|
|
49
|
+
placeholder={t('password.password')}
|
|
50
|
+
autoComplete="current-password"
|
|
51
|
+
/>
|
|
52
|
+
<button type="submit" className="button">
|
|
53
|
+
{t('password.enter')}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</form>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/rich-text.liquid — narrow centered/left block stack.
|
|
3
|
+
* CSS: app/styles/components/sections-rich-text.css
|
|
4
|
+
*/
|
|
5
|
+
import type {SectionProps} from '../engine/types';
|
|
6
|
+
|
|
7
|
+
interface RichTextSettings {
|
|
8
|
+
alignment?: 'left' | 'center';
|
|
9
|
+
color_scheme?: string;
|
|
10
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function RichTextSection({
|
|
14
|
+
settings,
|
|
15
|
+
children,
|
|
16
|
+
}: SectionProps<RichTextSettings>) {
|
|
17
|
+
const {
|
|
18
|
+
alignment = 'center',
|
|
19
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
20
|
+
section_spacing: sectionSpacing = 'lg',
|
|
21
|
+
} = settings;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={`rich-text section color-${colorScheme} full-width`}
|
|
26
|
+
style={
|
|
27
|
+
{
|
|
28
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
29
|
+
} as React.CSSProperties
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<div className={`rich-text__inner rich-text--${alignment} stack`}>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/stories.liquid — horizontally scrolling story strip.
|
|
3
|
+
* Story items are section-local blocks (app/blocks/story.tsx) rendered
|
|
4
|
+
* via `children`. Pure JSON-settings section: story blocks only carry
|
|
5
|
+
* image/label/link (no collection references), so no server loader.
|
|
6
|
+
* CSS: app/styles/components/sections-stories.css
|
|
7
|
+
*/
|
|
8
|
+
import type {SectionProps} from '../engine/types';
|
|
9
|
+
|
|
10
|
+
interface StoriesSettings {
|
|
11
|
+
shape?: 'stories--circle' | 'stories--square';
|
|
12
|
+
rows?: string | number;
|
|
13
|
+
desktop_size?: number;
|
|
14
|
+
mobile_size?: number;
|
|
15
|
+
color_scheme?: string;
|
|
16
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function StoriesSection({
|
|
20
|
+
settings,
|
|
21
|
+
children,
|
|
22
|
+
}: SectionProps<StoriesSettings>) {
|
|
23
|
+
const {
|
|
24
|
+
shape = 'stories--circle',
|
|
25
|
+
rows = '1',
|
|
26
|
+
desktop_size: desktopSize = 96,
|
|
27
|
+
mobile_size: mobileSize = 72,
|
|
28
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
29
|
+
section_spacing: sectionSpacing = 'md',
|
|
30
|
+
} = settings;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={`stories ${shape} color-${colorScheme} section full-width`}
|
|
35
|
+
style={
|
|
36
|
+
{
|
|
37
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
38
|
+
'--story-size-desktop': `${desktopSize}px`,
|
|
39
|
+
'--story-size-mobile': `${mobileSize}px`,
|
|
40
|
+
'--story-rows': `${rows}`,
|
|
41
|
+
} as React.CSSProperties
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<div className="stories__inner">
|
|
45
|
+
<div className="stories__list" role="list">
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/video-bubble.liquid — floating video bubble: a small
|
|
3
|
+
* muted preview loop pinned to a bottom corner that expands into the
|
|
4
|
+
* shared video modal (main video + optional product). Dismissible per
|
|
5
|
+
* browsing session. Behavior from the <video-bubble> island in
|
|
6
|
+
* src/entries/video.ts (viewport playback, session dismissal) plus the
|
|
7
|
+
* Liquid pre-paint script that keeps a dismissed bubble from flashing.
|
|
8
|
+
* CSS: app/styles/components/sections-video-bubble.css
|
|
9
|
+
*/
|
|
10
|
+
import {useEffect, useState} from 'react';
|
|
11
|
+
import type {SectionProps} from '../engine/types';
|
|
12
|
+
import {t} from '../engine/translate';
|
|
13
|
+
import {Icon} from '../components/Icon';
|
|
14
|
+
import {imageUrl, PlaceholderSvg, settingImageUrl} from '../engine/images';
|
|
15
|
+
import {
|
|
16
|
+
mp4Sources,
|
|
17
|
+
resolveVideoProduct,
|
|
18
|
+
useViewportPlayback,
|
|
19
|
+
VideoModal,
|
|
20
|
+
videoPreviewImage,
|
|
21
|
+
type ThemeVideo,
|
|
22
|
+
} from '../components/VideoModal';
|
|
23
|
+
|
|
24
|
+
interface VideoBubbleSettings {
|
|
25
|
+
preview_video?: ThemeVideo;
|
|
26
|
+
video?: ThemeVideo;
|
|
27
|
+
image?: string;
|
|
28
|
+
product?: unknown;
|
|
29
|
+
shape?: 'portrait' | 'circle';
|
|
30
|
+
position?: 'left' | 'right';
|
|
31
|
+
size?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function VideoBubbleSection({
|
|
35
|
+
id,
|
|
36
|
+
settings,
|
|
37
|
+
}: SectionProps<VideoBubbleSettings>) {
|
|
38
|
+
const {shape = 'portrait', position = 'right', size = 120} = settings;
|
|
39
|
+
const preview = settings.preview_video;
|
|
40
|
+
const mainVideo = settings.video ?? preview;
|
|
41
|
+
const poster =
|
|
42
|
+
settingImageUrl(settings.image) ??
|
|
43
|
+
videoPreviewImage(preview) ??
|
|
44
|
+
videoPreviewImage(mainVideo);
|
|
45
|
+
// The bubble renders tiny — the smallest rendition is always enough
|
|
46
|
+
const previewMp4 = mp4Sources(preview)[0];
|
|
47
|
+
const product = resolveVideoProduct(settings.product);
|
|
48
|
+
|
|
49
|
+
const dismissKey = `zalify:video-bubble:${id}`;
|
|
50
|
+
const [dismissed, setDismissed] = useState(false);
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const previewRef = useViewportPlayback();
|
|
53
|
+
|
|
54
|
+
// A bubble dismissed this session stays gone (≙ connectedCallback).
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
try {
|
|
57
|
+
if (sessionStorage.getItem(dismissKey)) setDismissed(true);
|
|
58
|
+
} catch {
|
|
59
|
+
// Storage unavailable: leave the bubble visible
|
|
60
|
+
}
|
|
61
|
+
}, [dismissKey]);
|
|
62
|
+
|
|
63
|
+
if (dismissed) return null;
|
|
64
|
+
|
|
65
|
+
const dismiss = () => {
|
|
66
|
+
setDismissed(true);
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(dismissKey, '1');
|
|
69
|
+
} catch {
|
|
70
|
+
// Storage can be unavailable (private mode); hiding is enough
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<div
|
|
77
|
+
className={`video-bubble video-bubble--${position} video-bubble--${shape}`}
|
|
78
|
+
style={{'--bubble-size': `${size}px`} as React.CSSProperties}
|
|
79
|
+
data-dismiss-key={id}
|
|
80
|
+
>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="video-bubble__dismiss"
|
|
84
|
+
onClick={dismiss}
|
|
85
|
+
aria-label={t('general.close')}
|
|
86
|
+
>
|
|
87
|
+
<Icon name="icon-close" />
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className="video-bubble__trigger"
|
|
92
|
+
onClick={() => setOpen(true)}
|
|
93
|
+
aria-label={t('general.play_video')}
|
|
94
|
+
>
|
|
95
|
+
{previewMp4 ? (
|
|
96
|
+
<video
|
|
97
|
+
ref={previewRef}
|
|
98
|
+
className="video-bubble__media"
|
|
99
|
+
autoPlay
|
|
100
|
+
loop
|
|
101
|
+
muted
|
|
102
|
+
playsInline
|
|
103
|
+
preload="metadata"
|
|
104
|
+
data-video-preview=""
|
|
105
|
+
poster={poster ? imageUrl(poster, 400) : undefined}
|
|
106
|
+
>
|
|
107
|
+
<source src={previewMp4.url} type={previewMp4.mime_type} />
|
|
108
|
+
</video>
|
|
109
|
+
) : poster ? (
|
|
110
|
+
<img
|
|
111
|
+
className="video-bubble__media"
|
|
112
|
+
src={imageUrl(poster, 400)}
|
|
113
|
+
loading="lazy"
|
|
114
|
+
alt=""
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<PlaceholderSvg className="video-bubble__media video-bubble__media--placeholder" />
|
|
118
|
+
)}
|
|
119
|
+
<span className="video-bubble__play">
|
|
120
|
+
<Icon name="icon-play" />
|
|
121
|
+
</span>
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
<VideoModal
|
|
125
|
+
open={open}
|
|
126
|
+
onClose={() => setOpen(false)}
|
|
127
|
+
video={mainVideo}
|
|
128
|
+
poster={poster}
|
|
129
|
+
product={product}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Pre-paint: a bubble dismissed this session must not flash in
|
|
134
|
+
before the effect above runs (mirror of the Liquid inline
|
|
135
|
+
script). */}
|
|
136
|
+
<script
|
|
137
|
+
dangerouslySetInnerHTML={{
|
|
138
|
+
__html: `(function(){try{if(sessionStorage.getItem(${JSON.stringify(
|
|
139
|
+
dismissKey,
|
|
140
|
+
)})){document.currentScript.previousElementSibling.hidden=true;}}catch(e){}})();`,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/video-carousel.liquid — a scroll-snap strip of
|
|
3
|
+
* _video-card blocks. The carousel itself is pure CSS (scroll-snap);
|
|
4
|
+
* the <video-carousel> island in src/entries/video.ts only wired
|
|
5
|
+
* preview playback and dialog pausing, which now live inside each
|
|
6
|
+
* card / the shared VideoModal. (The shopify:block:select editor
|
|
7
|
+
* scroll has no mirror equivalent.)
|
|
8
|
+
* CSS: app/styles/components/sections-video-carousel.css
|
|
9
|
+
*/
|
|
10
|
+
import type {SectionProps} from '../engine/types';
|
|
11
|
+
|
|
12
|
+
interface VideoCarouselSettings {
|
|
13
|
+
heading?: string;
|
|
14
|
+
desktop_size?: number;
|
|
15
|
+
mobile_size?: number;
|
|
16
|
+
color_scheme?: string;
|
|
17
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function VideoCarouselSection({
|
|
21
|
+
settings,
|
|
22
|
+
children,
|
|
23
|
+
}: SectionProps<VideoCarouselSettings>) {
|
|
24
|
+
const {
|
|
25
|
+
heading = 'Shop the look',
|
|
26
|
+
desktop_size: desktopSize = 280,
|
|
27
|
+
mobile_size: mobileSize = 200,
|
|
28
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
29
|
+
section_spacing: sectionSpacing = 'md',
|
|
30
|
+
} = settings;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={`video-carousel section color-${colorScheme} full-width`}
|
|
35
|
+
style={
|
|
36
|
+
{
|
|
37
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
38
|
+
'--video-card-size-desktop': `${desktopSize}px`,
|
|
39
|
+
'--video-card-size-mobile': `${mobileSize}px`,
|
|
40
|
+
} as React.CSSProperties
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className="video-carousel__inner stack"
|
|
45
|
+
style={{'--stack-gap': 'var(--space-xl)'} as React.CSSProperties}
|
|
46
|
+
>
|
|
47
|
+
{heading ? <h2>{heading}</h2> : null}
|
|
48
|
+
|
|
49
|
+
{/* Direct children are the block wrappers (.shopify-block),
|
|
50
|
+
mirroring the editor wrappers the CSS flexes on. */}
|
|
51
|
+
<div className="video-carousel__list" role="list">
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|