@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.
Files changed (213) hide show
  1. package/LICENSE.md +49 -0
  2. package/README.md +63 -0
  3. package/dist/commerce/countdown.d.ts +18 -0
  4. package/dist/commerce/countdown.js +28 -0
  5. package/dist/commerce/events.d.ts +29 -0
  6. package/dist/commerce/events.js +14 -0
  7. package/dist/commerce/facets.d.ts +27 -0
  8. package/dist/commerce/facets.js +71 -0
  9. package/dist/commerce/index.d.ts +11 -0
  10. package/dist/commerce/index.js +11 -0
  11. package/dist/commerce/money.d.ts +51 -0
  12. package/dist/commerce/money.js +83 -0
  13. package/dist/commerce/product.d.ts +102 -0
  14. package/dist/commerce/product.js +93 -0
  15. package/dist/editor/frame.d.ts +57 -0
  16. package/dist/editor/frame.js +241 -0
  17. package/dist/editor/host.d.ts +31 -0
  18. package/dist/editor/host.js +67 -0
  19. package/dist/editor/index.d.ts +9 -0
  20. package/dist/editor/index.js +9 -0
  21. package/dist/react/adapter.d.ts +49 -0
  22. package/dist/react/adapter.js +23 -0
  23. package/dist/react/blocks/_box.d.ts +10 -0
  24. package/dist/react/blocks/_box.js +17 -0
  25. package/dist/react/blocks/_slide.d.ts +24 -0
  26. package/dist/react/blocks/_slide.js +21 -0
  27. package/dist/react/blocks/_video-card.d.ts +11 -0
  28. package/dist/react/blocks/_video-card.js +39 -0
  29. package/dist/react/blocks/accordion.d.ts +12 -0
  30. package/dist/react/blocks/accordion.js +5 -0
  31. package/dist/react/blocks/button.d.ts +10 -0
  32. package/dist/react/blocks/button.js +15 -0
  33. package/dist/react/blocks/countdown.d.ts +9 -0
  34. package/dist/react/blocks/countdown.js +64 -0
  35. package/dist/react/blocks/custom-liquid.d.ts +11 -0
  36. package/dist/react/blocks/custom-liquid.js +4 -0
  37. package/dist/react/blocks/group.d.ts +9 -0
  38. package/dist/react/blocks/group.js +8 -0
  39. package/dist/react/blocks/image.d.ts +8 -0
  40. package/dist/react/blocks/image.js +7 -0
  41. package/dist/react/blocks/message.d.ts +14 -0
  42. package/dist/react/blocks/message.js +5 -0
  43. package/dist/react/blocks/quote.d.ts +9 -0
  44. package/dist/react/blocks/quote.js +5 -0
  45. package/dist/react/blocks/spacer.d.ts +8 -0
  46. package/dist/react/blocks/spacer.js +5 -0
  47. package/dist/react/blocks/story.d.ts +17 -0
  48. package/dist/react/blocks/story.js +7 -0
  49. package/dist/react/blocks/text.d.ts +9 -0
  50. package/dist/react/blocks/text.js +5 -0
  51. package/dist/react/blocks/video.d.ts +17 -0
  52. package/dist/react/blocks/video.js +56 -0
  53. package/dist/react/components/Facets.d.ts +21 -0
  54. package/dist/react/components/Facets.js +220 -0
  55. package/dist/react/components/Icon.d.ts +12 -0
  56. package/dist/react/components/Icon.js +66 -0
  57. package/dist/react/components/PaymentIcons.d.ts +5 -0
  58. package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
  59. package/dist/react/components/PaymentIcons.generated.js +21 -0
  60. package/dist/react/components/PaymentIcons.js +18 -0
  61. package/dist/react/components/Price.d.ts +24 -0
  62. package/dist/react/components/Price.js +9 -0
  63. package/dist/react/components/ProductBadges.d.ts +24 -0
  64. package/dist/react/components/ProductBadges.js +57 -0
  65. package/dist/react/components/ProductCard.d.ts +17 -0
  66. package/dist/react/components/ProductCard.js +202 -0
  67. package/dist/react/components/Rating.d.ts +11 -0
  68. package/dist/react/components/Rating.js +35 -0
  69. package/dist/react/components/SwatchStyle.d.ts +23 -0
  70. package/dist/react/components/SwatchStyle.js +22 -0
  71. package/dist/react/components/VideoModal.d.ts +55 -0
  72. package/dist/react/components/VideoModal.js +102 -0
  73. package/dist/react/engine/CssVariables.d.ts +1 -0
  74. package/dist/react/engine/CssVariables.js +105 -0
  75. package/dist/react/engine/context.d.ts +43 -0
  76. package/dist/react/engine/context.js +45 -0
  77. package/dist/react/engine/fonts.d.ts +15 -0
  78. package/dist/react/engine/fonts.js +33 -0
  79. package/dist/react/engine/images.d.ts +24 -0
  80. package/dist/react/engine/images.js +40 -0
  81. package/dist/react/engine/render.d.ts +29 -0
  82. package/dist/react/engine/render.js +87 -0
  83. package/dist/react/engine/settings.d.ts +8 -0
  84. package/dist/react/engine/settings.js +68 -0
  85. package/dist/react/engine/store.d.ts +64 -0
  86. package/dist/react/engine/store.js +42 -0
  87. package/dist/react/engine/translate.d.ts +7 -0
  88. package/dist/react/engine/translate.js +33 -0
  89. package/dist/react/engine/types.d.ts +128 -0
  90. package/dist/react/engine/types.js +1 -0
  91. package/dist/react/index.d.ts +38 -0
  92. package/dist/react/index.js +39 -0
  93. package/dist/react/registries.d.ts +10 -0
  94. package/dist/react/registries.js +68 -0
  95. package/dist/react/registries.server.d.ts +2 -0
  96. package/dist/react/registries.server.js +98 -0
  97. package/dist/react/sections/404.d.ts +10 -0
  98. package/dist/react/sections/404.js +10 -0
  99. package/dist/react/sections/announcement-bar.d.ts +14 -0
  100. package/dist/react/sections/announcement-bar.js +84 -0
  101. package/dist/react/sections/article.d.ts +17 -0
  102. package/dist/react/sections/article.js +31 -0
  103. package/dist/react/sections/contact-form.d.ts +16 -0
  104. package/dist/react/sections/contact-form.js +8 -0
  105. package/dist/react/sections/custom-liquid.d.ts +14 -0
  106. package/dist/react/sections/custom-liquid.js +7 -0
  107. package/dist/react/sections/custom-section.d.ts +14 -0
  108. package/dist/react/sections/custom-section.js +9 -0
  109. package/dist/react/sections/featured-collection.d.ts +10 -0
  110. package/dist/react/sections/featured-collection.js +23 -0
  111. package/dist/react/sections/hero.d.ts +6 -0
  112. package/dist/react/sections/hero.js +35 -0
  113. package/dist/react/sections/image-with-text.d.ts +13 -0
  114. package/dist/react/sections/image-with-text.js +9 -0
  115. package/dist/react/sections/multicolumn.d.ts +15 -0
  116. package/dist/react/sections/multicolumn.js +8 -0
  117. package/dist/react/sections/page.d.ts +13 -0
  118. package/dist/react/sections/page.js +11 -0
  119. package/dist/react/sections/password.d.ts +15 -0
  120. package/dist/react/sections/password.js +9 -0
  121. package/dist/react/sections/rich-text.d.ts +12 -0
  122. package/dist/react/sections/rich-text.js +7 -0
  123. package/dist/react/sections/stories.d.ts +18 -0
  124. package/dist/react/sections/stories.js +10 -0
  125. package/dist/react/sections/video-bubble.d.ts +13 -0
  126. package/dist/react/sections/video-bubble.js +54 -0
  127. package/dist/react/sections/video-carousel.d.ts +19 -0
  128. package/dist/react/sections/video-carousel.js +9 -0
  129. package/dist/react/server.d.ts +7 -0
  130. package/dist/react/server.js +42 -0
  131. package/dist/schemas/bridge.d.ts +98 -0
  132. package/dist/schemas/bridge.js +50 -0
  133. package/dist/schemas/data.d.ts +54 -0
  134. package/dist/schemas/data.js +22 -0
  135. package/dist/schemas/index.d.ts +14 -0
  136. package/dist/schemas/index.js +14 -0
  137. package/dist/schemas/manifest.d.ts +63 -0
  138. package/dist/schemas/manifest.js +32 -0
  139. package/dist/schemas/schema.d.ts +159 -0
  140. package/dist/schemas/schema.js +129 -0
  141. package/dist/schemas/validate.d.ts +22 -0
  142. package/dist/schemas/validate.js +199 -0
  143. package/package.json +84 -0
  144. package/src/commerce/countdown.ts +38 -0
  145. package/src/commerce/events.ts +43 -0
  146. package/src/commerce/facets.ts +88 -0
  147. package/src/commerce/index.ts +11 -0
  148. package/src/commerce/money.ts +120 -0
  149. package/src/commerce/product.ts +177 -0
  150. package/src/editor/frame.ts +305 -0
  151. package/src/editor/host.ts +101 -0
  152. package/src/editor/index.ts +9 -0
  153. package/src/react/adapter.tsx +83 -0
  154. package/src/react/blocks/_box.tsx +37 -0
  155. package/src/react/blocks/_slide.tsx +96 -0
  156. package/src/react/blocks/_video-card.tsx +131 -0
  157. package/src/react/blocks/accordion.tsx +33 -0
  158. package/src/react/blocks/button.tsx +36 -0
  159. package/src/react/blocks/countdown.tsx +105 -0
  160. package/src/react/blocks/custom-liquid.tsx +21 -0
  161. package/src/react/blocks/group.tsx +32 -0
  162. package/src/react/blocks/image.tsx +29 -0
  163. package/src/react/blocks/message.tsx +28 -0
  164. package/src/react/blocks/quote.tsx +24 -0
  165. package/src/react/blocks/spacer.tsx +17 -0
  166. package/src/react/blocks/story.tsx +40 -0
  167. package/src/react/blocks/text.tsx +24 -0
  168. package/src/react/blocks/video.tsx +132 -0
  169. package/src/react/components/Facets.tsx +424 -0
  170. package/src/react/components/Icon.tsx +96 -0
  171. package/src/react/components/PaymentIcons.generated.ts +24 -0
  172. package/src/react/components/PaymentIcons.tsx +27 -0
  173. package/src/react/components/Price.tsx +72 -0
  174. package/src/react/components/ProductBadges.tsx +95 -0
  175. package/src/react/components/ProductCard.tsx +417 -0
  176. package/src/react/components/Rating.tsx +59 -0
  177. package/src/react/components/SwatchStyle.tsx +50 -0
  178. package/src/react/components/VideoModal.tsx +236 -0
  179. package/src/react/engine/CssVariables.tsx +142 -0
  180. package/src/react/engine/context.tsx +108 -0
  181. package/src/react/engine/fonts.ts +42 -0
  182. package/src/react/engine/images.tsx +58 -0
  183. package/src/react/engine/render.tsx +190 -0
  184. package/src/react/engine/settings.ts +89 -0
  185. package/src/react/engine/store.ts +103 -0
  186. package/src/react/engine/translate.ts +40 -0
  187. package/src/react/engine/types.ts +142 -0
  188. package/src/react/index.ts +78 -0
  189. package/src/react/registries.server.ts +110 -0
  190. package/src/react/registries.ts +80 -0
  191. package/src/react/sections/404.tsx +37 -0
  192. package/src/react/sections/announcement-bar.tsx +170 -0
  193. package/src/react/sections/article.tsx +97 -0
  194. package/src/react/sections/contact-form.tsx +104 -0
  195. package/src/react/sections/custom-liquid.tsx +39 -0
  196. package/src/react/sections/custom-section.tsx +44 -0
  197. package/src/react/sections/featured-collection.tsx +89 -0
  198. package/src/react/sections/hero.tsx +84 -0
  199. package/src/react/sections/image-with-text.tsx +61 -0
  200. package/src/react/sections/multicolumn.tsx +46 -0
  201. package/src/react/sections/page.tsx +43 -0
  202. package/src/react/sections/password.tsx +60 -0
  203. package/src/react/sections/rich-text.tsx +37 -0
  204. package/src/react/sections/stories.tsx +51 -0
  205. package/src/react/sections/video-bubble.tsx +145 -0
  206. package/src/react/sections/video-carousel.tsx +57 -0
  207. package/src/react/server.ts +60 -0
  208. package/src/schemas/bridge.ts +143 -0
  209. package/src/schemas/data.ts +72 -0
  210. package/src/schemas/index.ts +14 -0
  211. package/src/schemas/manifest.ts +85 -0
  212. package/src/schemas/schema.ts +287 -0
  213. 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
+ }