@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,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
+ }