@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,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAdapter } from '../adapter';
3
+ export default function ButtonBlock({ settings }) {
4
+ const { Link } = useAdapter();
5
+ const { label = 'Shop now', link, style = '', size = '' } = settings;
6
+ const className = `button ${style} ${size}`.replace(/\s+/g, ' ').trim();
7
+ if (!link) {
8
+ return (_jsx("a", { className: className, role: "link", "aria-disabled": "true", children: label }));
9
+ }
10
+ const isExternal = /^https?:\/\//.test(link);
11
+ if (isExternal) {
12
+ return (_jsx("a", { className: className, href: link, children: label }));
13
+ }
14
+ return (_jsx(Link, { className: className, to: link, children: label }));
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlockProps } from '../engine/types';
2
+ interface CountdownSettings {
3
+ end_time?: string;
4
+ label?: string;
5
+ completed_text?: string;
6
+ alignment?: string;
7
+ }
8
+ export default function CountdownBlock({ settings, }: BlockProps<CountdownSettings>): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Port of blocks/countdown.liquid + the <countdown-timer> island
4
+ * (src/entries/countdown.ts): server renders dash placeholders, a
5
+ * 1-second tick fills the digit slots and flips visibility when the
6
+ * deadline passes. CSS: app/styles/components/blocks-countdown.css
7
+ */
8
+ import { useEffect, useState } from 'react';
9
+ // Clock math shared with the theme's <countdown-timer> island.
10
+ import { pad2 as pad, parseEndTime as parseEnd, secondsUntil, } from "../../commerce/index.js";
11
+ import { t } from '../engine/translate';
12
+ export default function CountdownBlock({ settings, }) {
13
+ const { label = 'Ends in', completed_text: completedText, alignment = 'left' } = settings;
14
+ const endTime = (settings.end_time ?? '').trim();
15
+ // null until the first client tick — SSR shows the "–" placeholders,
16
+ // exactly like the Liquid markup before countdown.js runs.
17
+ const [remain, setRemain] = useState(null);
18
+ useEffect(() => {
19
+ if (!endTime)
20
+ return;
21
+ const end = parseEnd(endTime);
22
+ if (Number.isNaN(end))
23
+ return;
24
+ let interval = 0;
25
+ const tick = () => {
26
+ const next = secondsUntil(end);
27
+ setRemain(next);
28
+ if (next <= 0 && interval)
29
+ clearInterval(interval);
30
+ return next;
31
+ };
32
+ if (tick() > 0)
33
+ interval = window.setInterval(tick, 1000);
34
+ return () => clearInterval(interval);
35
+ }, [endTime]);
36
+ if (!endTime)
37
+ return _jsx("div", { hidden: true });
38
+ const done = remain !== null && remain <= 0;
39
+ const days = remain === null ? null : Math.floor(remain / 86400);
40
+ // No completion message configured: the block just goes away.
41
+ if (done && !completedText)
42
+ return _jsx("div", { className: "countdown", hidden: true });
43
+ const units = [
44
+ {
45
+ value: days === null ? '–' : pad(days),
46
+ caption: t('general.countdown.days'),
47
+ // Under a day, the days unit is dead weight — drop it
48
+ hidden: days === 0,
49
+ },
50
+ {
51
+ value: remain === null ? '–' : pad(Math.floor(remain / 3600) % 24),
52
+ caption: t('general.countdown.hours'),
53
+ },
54
+ {
55
+ value: remain === null ? '–' : pad(Math.floor(remain / 60) % 60),
56
+ caption: t('general.countdown.minutes'),
57
+ },
58
+ {
59
+ value: remain === null ? '–' : pad(remain % 60),
60
+ caption: t('general.countdown.seconds'),
61
+ },
62
+ ];
63
+ return (_jsxs("div", { className: "countdown", style: { '--countdown-justify': alignment }, role: "timer", children: [label ? _jsx("span", { className: "countdown__label", children: label }) : null, _jsx("span", { className: "countdown__units", hidden: done, children: units.map((unit) => (_jsxs("span", { className: "countdown__unit", hidden: unit.hidden, children: [_jsx("span", { className: "countdown__value", children: unit.value }), _jsx("span", { className: "countdown__caption", children: unit.caption })] }, unit.caption))) }), completedText ? (_jsx("span", { className: "countdown__completed", hidden: !done, children: completedText })) : null] }));
64
+ }
@@ -0,0 +1,11 @@
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
+ interface CustomLiquidBlockSettings {
8
+ custom_liquid?: string;
9
+ }
10
+ export default function CustomLiquidBlock({ settings, }: BlockProps<CustomLiquidBlockSettings>): import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function CustomLiquidBlock({ settings, }) {
3
+ return (_jsx("div", { className: "custom-liquid-block", dangerouslySetInnerHTML: { __html: settings.custom_liquid ?? '' } }));
4
+ }
@@ -0,0 +1,9 @@
1
+ /** Port of blocks/group.liquid. CSS: app/styles/components/blocks-group.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface GroupSettings {
4
+ layout_direction?: 'group--horizontal' | 'group--vertical';
5
+ alignment?: string;
6
+ padding?: number;
7
+ }
8
+ export default function GroupBlock({ settings, children, }: BlockProps<GroupSettings>): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function GroupBlock({ settings, children, }) {
3
+ const { layout_direction: direction = 'group--vertical', alignment = 'flex-start', padding = 0, } = settings;
4
+ return (_jsx("div", { className: `group ${direction}`, style: {
5
+ '--padding': `${padding}px`,
6
+ '--alignment': alignment,
7
+ }, children: children }));
8
+ }
@@ -0,0 +1,8 @@
1
+ /** Port of blocks/image.liquid. CSS: app/styles/components/blocks-image.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface ImageSettings {
4
+ image?: string;
5
+ aspect?: 'auto' | 'square' | 'portrait' | 'landscape';
6
+ }
7
+ export default function ImageBlock({ settings }: BlockProps<ImageSettings>): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { imageSrcSet, imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
+ export default function ImageBlock({ settings }) {
4
+ const { aspect = 'auto' } = settings;
5
+ const src = settingImageUrl(settings.image);
6
+ return (_jsx("div", { className: `image-block image-block--${aspect}`, children: src ? (_jsx("img", { className: "image-block__image", src: imageUrl(src, 1500), srcSet: imageSrcSet(src), sizes: "(min-width: 48rem) 50vw, 100vw", loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "image-block__image image-block__placeholder" })) }));
7
+ }
@@ -0,0 +1,14 @@
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
+ interface MessageSettings {
10
+ /** inline_richtext */
11
+ text?: string;
12
+ }
13
+ export default function MessageBlock({ settings, index, }: BlockProps<MessageSettings>): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function MessageBlock({ settings, index, }) {
3
+ const { text = 'Free shipping on orders over $50' } = settings;
4
+ return (_jsx("p", { className: "announcement-bar__message", hidden: index !== 0, children: _jsx("span", { className: "announcement-bar__text", dangerouslySetInnerHTML: { __html: text } }) }));
5
+ }
@@ -0,0 +1,9 @@
1
+ /** Port of blocks/quote.liquid. CSS: app/styles/components/blocks-quote.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface QuoteSettings {
4
+ /** Richtext setting — HTML string. */
5
+ quote?: string;
6
+ author?: string;
7
+ }
8
+ export default function QuoteBlock({ settings }: BlockProps<QuoteSettings>): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export default function QuoteBlock({ settings }) {
3
+ const { quote = '<p>Add a customer quote here to build trust.</p>', author, } = settings;
4
+ return (_jsxs("figure", { className: "quote stack", children: [_jsx("blockquote", { className: "quote__text", dangerouslySetInnerHTML: { __html: quote } }), author ? _jsx("figcaption", { className: "quote__author", children: author }) : null] }));
5
+ }
@@ -0,0 +1,8 @@
1
+ /** Port of blocks/spacer.liquid. CSS: app/styles/components/blocks-spacer.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface SpacerSettings {
4
+ size?: number;
5
+ fill?: boolean;
6
+ }
7
+ export default function SpacerBlock({ settings }: BlockProps<SpacerSettings>): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function SpacerBlock({ settings }) {
3
+ const { size = 40, fill = false } = settings;
4
+ return (_jsx("div", { className: `spacer${fill ? ' spacer--fill' : ''}`, style: { '--spacer-size': `${size}px` } }));
5
+ }
@@ -0,0 +1,17 @@
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
+ interface StorySettings {
12
+ image?: string;
13
+ label?: string;
14
+ link?: string;
15
+ }
16
+ export default function StoryBlock({ settings }: BlockProps<StorySettings>): import("react").JSX.Element;
17
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
3
+ export default function StoryBlock({ settings }) {
4
+ const image = settingImageUrl(settings.image);
5
+ const label = settings.label ?? '';
6
+ return (_jsxs("a", { className: "story", role: "listitem", href: settings.link || undefined, children: [_jsx("span", { className: "story__media", children: image ? (_jsx("img", { className: "story__image", src: imageUrl(image, 480), loading: "lazy", alt: label })) : (_jsx(PlaceholderSvg, { className: "story__image story__image--placeholder" })) }), label ? _jsx("span", { className: "story__label", children: label }) : null] }));
7
+ }
@@ -0,0 +1,9 @@
1
+ /** Port of blocks/text.liquid. CSS: app/styles/components/blocks-text.css */
2
+ import type { BlockProps } from '../engine/types';
3
+ interface TextSettings {
4
+ text?: string;
5
+ text_style?: string;
6
+ alignment?: string;
7
+ }
8
+ export default function TextBlock({ settings }: BlockProps<TextSettings>): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function TextBlock({ settings }) {
3
+ const { text = 'Text', text_style: textStyle = 'text--title', alignment = 'left', } = settings;
4
+ return (_jsx("div", { className: `text ${textStyle}`, style: { '--text-align': alignment }, children: text }));
5
+ }
@@ -0,0 +1,17 @@
1
+ import type { BlockProps } from '../engine/types';
2
+ import { type ThemeVideo } from '../components/VideoModal';
3
+ interface VideoSettings {
4
+ video?: ThemeVideo;
5
+ /** `video_url` setting: JSON stores the raw URL string; the Liquid
6
+ * runtime's {type, id} object shape is accepted too. */
7
+ video_url?: string | {
8
+ type?: string;
9
+ id?: string;
10
+ url?: string;
11
+ };
12
+ image?: string;
13
+ autoplay?: boolean;
14
+ aspect?: 'auto' | 'square' | 'portrait' | 'landscape';
15
+ }
16
+ export default function VideoBlock({ settings }: BlockProps<VideoSettings>): import("react").JSX.Element;
17
+ export {};
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Port of blocks/video.liquid — general video block: a Shopify-hosted
4
+ * file (ambient loop or click-to-play with native controls) or a
5
+ * YouTube/Vimeo link behind a cover-image facade that loads nothing
6
+ * until clicked. Behavior from the <video-player> island in
7
+ * src/entries/video.ts. CSS: app/styles/components/blocks-video.css
8
+ */
9
+ import { useState } from 'react';
10
+ import { t } from '../engine/translate';
11
+ import { Icon } from '../components/Icon';
12
+ import { imageUrl, PlaceholderSvg, settingImageUrl } from '../engine/images';
13
+ import { mp4Sources, useViewportPlayback, videoPreviewImage, } from '../components/VideoModal';
14
+ /** Mirror of Liquid's external video_url object (type + id). */
15
+ function parseExternal(value) {
16
+ let url = '';
17
+ if (typeof value === 'object' && value !== null) {
18
+ if ((value.type === 'youtube' || value.type === 'vimeo') && value.id) {
19
+ return { type: value.type, id: value.id };
20
+ }
21
+ url = value.url ?? '';
22
+ }
23
+ else if (typeof value === 'string') {
24
+ url = value;
25
+ }
26
+ if (!url)
27
+ return null;
28
+ const youtube = url.match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);
29
+ if (youtube)
30
+ return { type: 'youtube', id: youtube[1] };
31
+ const vimeo = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
32
+ if (vimeo)
33
+ return { type: 'vimeo', id: vimeo[1] };
34
+ return null;
35
+ }
36
+ export default function VideoBlock({ settings }) {
37
+ const { autoplay = true, aspect = 'auto' } = settings;
38
+ const hostedMp4 = mp4Sources(settings.video).at(-1);
39
+ const external = parseExternal(settings.video_url);
40
+ const poster = settingImageUrl(settings.image) ?? videoPreviewImage(settings.video);
41
+ // Ambient loops play only while visible (watchPreviews in video.ts).
42
+ const previewRef = useViewportPlayback();
43
+ // External embed facade: the iframe gets its src only on click.
44
+ const [playing, setPlaying] = useState(false);
45
+ const frameClass = hostedMp4 ? '' : ' video-block--frame';
46
+ const embedUrl = external
47
+ ? external.type === 'youtube'
48
+ ? `https://www.youtube-nocookie.com/embed/${external.id}?autoplay=1&playsinline=1&rel=0`
49
+ : `https://player.vimeo.com/video/${external.id}?autoplay=1`
50
+ : null;
51
+ return (_jsx("div", { className: `video-block video-block--${aspect}${frameClass}`, children: hostedMp4 ? (_jsx("video", { ref: autoplay ? previewRef : undefined, className: "video-block__media", playsInline: true, preload: "metadata", ...(autoplay
52
+ ? { autoPlay: true, loop: true, muted: true, 'data-video-preview': '' }
53
+ : { controls: true }), poster: poster ? imageUrl(poster, 1500) : undefined, children: _jsx("source", { src: hostedMp4.url, type: hostedMp4.mime_type }) })) : embedUrl ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "video-block__facade", onClick: () => setPlaying(true), "aria-label": t('general.play_video'), hidden: playing, children: [poster ? (_jsx("img", { className: "video-block__media", src: imageUrl(poster, 1500), srcSet: [400, 700, 1000, 1500]
54
+ .map((w) => `${imageUrl(poster, w)} ${w}w`)
55
+ .join(', '), loading: "lazy", alt: "" })) : (_jsx(PlaceholderSvg, { className: "video-block__media video-block__placeholder" })), _jsx("span", { className: "video-block__play", children: _jsx(Icon, { name: "icon-play" }) })] }), _jsx("iframe", { className: "video-block__media", src: playing ? embedUrl : undefined, title: t('general.play_video'), allow: "autoplay; fullscreen; encrypted-media; picture-in-picture", allowFullScreen: true, hidden: !playing })] })) : (_jsxs(_Fragment, { children: [_jsx(PlaceholderSvg, { className: "video-block__media video-block__placeholder" }), _jsx("span", { className: "video-block__play", children: _jsx(Icon, { name: "icon-play" }) })] })) }));
56
+ }
@@ -0,0 +1,21 @@
1
+ import type React from 'react';
2
+ export interface SortOption {
3
+ value: string;
4
+ label: string;
5
+ }
6
+ /** Shopify's canonical collection sort handles (Liquid collection.sort_options). */
7
+ export declare const COLLECTION_SORT_OPTIONS: SortOption[];
8
+ /** Shopify's canonical search sort handles (Liquid search.sort_options). */
9
+ export declare const SEARCH_SORT_OPTIONS: SortOption[];
10
+ export interface FacetsProps {
11
+ /** Storefront API filters (collection.products.filters / search filters). */
12
+ filters: any[];
13
+ /** Base URL the clear-all link points at (mirror of results_url). */
14
+ resultsUrl: string;
15
+ resultsCount: number;
16
+ sortOptions?: SortOption[];
17
+ defaultSortBy?: string;
18
+ /** Search terms, preserved as a hidden q input. */
19
+ terms?: string;
20
+ }
21
+ export declare function Facets({ filters, resultsUrl, resultsCount, sortOptions, defaultSortBy, terms, }: FacetsProps): React.JSX.Element;
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Port of snippets/facets.liquid + src/entries/facets.ts (<facets-form>).
4
+ * CSS: app/styles/components/snippets-facets.css
5
+ *
6
+ * One GET-style form holds the toolbar (sort popover) and the Filters
7
+ * modal. Sort changes apply immediately; filter changes apply on
8
+ * "Apply". Closing the dialog without applying resets the form to the
9
+ * currently applied (= URL) state. Applying navigates with the host's
10
+ * router (via the adapter seam), updating the URL search params (the
11
+ * mirror of the Liquid island's Section Rendering swap + pushState).
12
+ *
13
+ * URL contract (what the route loader should parse):
14
+ * sort_by=<value> – sort handle (see sort options)
15
+ * filter.<key>=<json> – one param per applied filter
16
+ * value, where <key> is the ProductFilter key from the Storefront
17
+ * filter value's `input` (e.g. filter.variantOption={"name":"color",
18
+ * "value":"Red"}), and price as filter.price={"min":10,"max":50}
19
+ * q=<terms> – preserved search terms
20
+ */
21
+ import { useEffect, useRef } from 'react';
22
+ import { cleanFormParams } from "../../commerce/index.js";
23
+ import { useAdapter } from '../adapter';
24
+ import { t } from '../engine/translate';
25
+ import { Icon } from './Icon';
26
+ const FILTER_PREFIX = 'filter.';
27
+ /** Shopify's canonical collection sort handles (Liquid collection.sort_options). */
28
+ export const COLLECTION_SORT_OPTIONS = [
29
+ { value: 'manual', label: 'Featured' },
30
+ { value: 'best-selling', label: 'Best selling' },
31
+ { value: 'title-ascending', label: 'Alphabetically, A-Z' },
32
+ { value: 'title-descending', label: 'Alphabetically, Z-A' },
33
+ { value: 'price-ascending', label: 'Price, low to high' },
34
+ { value: 'price-descending', label: 'Price, high to low' },
35
+ { value: 'created-ascending', label: 'Date, old to new' },
36
+ { value: 'created-descending', label: 'Date, new to old' },
37
+ ];
38
+ /** Shopify's canonical search sort handles (Liquid search.sort_options). */
39
+ export const SEARCH_SORT_OPTIONS = [
40
+ { value: 'relevance', label: 'Relevance' },
41
+ { value: 'price-ascending', label: 'Price, low to high' },
42
+ { value: 'price-descending', label: 'Price, high to low' },
43
+ ];
44
+ /** Vetted CSS color keywords — mirror of snippets/swatch-style.liquid. */
45
+ const CSS_COLORS = ',aliceblue,antiquewhite,aqua,aquamarine,azure,beige,bisque,black,blanchedalmond,blue,blueviolet,brown,burlywood,cadetblue,chartreuse,chocolate,coral,cornflowerblue,cornsilk,crimson,cyan,darkblue,darkcyan,darkgoldenrod,darkgray,darkgreen,darkgrey,darkkhaki,darkmagenta,darkolivegreen,darkorange,darkorchid,darkred,darksalmon,darkseagreen,darkslateblue,darkslategray,darkslategrey,darkturquoise,darkviolet,deeppink,deepskyblue,dimgray,dimgrey,dodgerblue,firebrick,floralwhite,forestgreen,fuchsia,gainsboro,ghostwhite,gold,goldenrod,gray,green,greenyellow,grey,honeydew,hotpink,indianred,indigo,ivory,khaki,lavender,lavenderblush,lawngreen,lemonchiffon,lightblue,lightcoral,lightcyan,lightgoldenrodyellow,lightgray,lightgreen,lightgrey,lightpink,lightsalmon,lightseagreen,lightskyblue,lightslategray,lightslategrey,lightsteelblue,lightyellow,lime,limegreen,linen,magenta,maroon,mediumaquamarine,mediumblue,mediumorchid,mediumpurple,mediumseagreen,mediumslateblue,mediumspringgreen,mediumturquoise,mediumvioletred,midnightblue,mintcream,mistyrose,moccasin,navajowhite,navy,oldlace,olive,olivedrab,orange,orangered,orchid,palegoldenrod,palegreen,paleturquoise,palevioletred,papayawhip,peachpuff,peru,pink,plum,powderblue,purple,rebeccapurple,red,rosybrown,royalblue,saddlebrown,salmon,sandybrown,seagreen,seashell,sienna,silver,skyblue,slateblue,slategray,slategrey,snow,springgreen,steelblue,tan,teal,thistle,tomato,turquoise,violet,wheat,white,whitesmoke,yellow,yellowgreen,';
46
+ /**
47
+ * Mirror of snippets/swatch-style.liquid: resolve the `--swatch` fill
48
+ * from the value's native swatch, else the value name as a vetted CSS
49
+ * keyword. (The metaobject-based swatch_color lookup has no Storefront
50
+ * mirror here.)
51
+ */
52
+ function swatchStyle(swatch, name) {
53
+ const imageUrl = swatch?.image?.previewImage?.url ??
54
+ swatch?.image?.image?.url ??
55
+ (typeof swatch?.image === 'string' ? swatch.image : undefined);
56
+ if (imageUrl)
57
+ return { '--swatch': `url(${imageUrl})` };
58
+ if (swatch?.color)
59
+ return { '--swatch': swatch.color };
60
+ const probe = String(name ?? '')
61
+ .toLowerCase()
62
+ .trim()
63
+ .replace(/[\s-]/g, '');
64
+ if (probe && CSS_COLORS.includes(`,${probe},`))
65
+ return { '--swatch': probe };
66
+ return {};
67
+ }
68
+ /** filter value → URL param, from the Storefront `input` JSON. */
69
+ function valueParam(value) {
70
+ try {
71
+ const input = typeof value?.input === 'string' ? JSON.parse(value.input) : value?.input;
72
+ if (!input || typeof input !== 'object')
73
+ return null;
74
+ const key = Object.keys(input)[0];
75
+ if (!key)
76
+ return null;
77
+ return { name: `${FILTER_PREFIX}${key}`, value: JSON.stringify(input[key]) };
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function isPriceFilter(filter) {
84
+ const type = String(filter?.type ?? '').toLowerCase();
85
+ return type === 'price_range';
86
+ }
87
+ export function Facets({ filters, resultsUrl, resultsCount, sortOptions = COLLECTION_SORT_OPTIONS, defaultSortBy = 'manual', terms, }) {
88
+ const adapter = useAdapter();
89
+ const { Link } = adapter;
90
+ const navigate = adapter.useNavigate();
91
+ const searchParams = adapter.useSearchParams();
92
+ const pathname = adapter.usePathname();
93
+ const rootRef = useRef(null);
94
+ const formRef = useRef(null);
95
+ const dialogRef = useRef(null);
96
+ const timerRef = useRef();
97
+ // Applied state comes straight from the URL (the mirror of the
98
+ // server-rendered form state in Liquid).
99
+ const activeCount = [...searchParams.keys()].filter((key) => key.startsWith(FILTER_PREFIX)).length;
100
+ const currentSort = searchParams.get('sort_by') ?? defaultSortBy;
101
+ const sortLabel = sortOptions.find((option) => option.value === currentSort)?.label ?? '';
102
+ let appliedPrice = {};
103
+ try {
104
+ appliedPrice = JSON.parse(searchParams.get('filter.price') ?? 'null') ?? {};
105
+ }
106
+ catch {
107
+ appliedPrice = {};
108
+ }
109
+ /** Mirror of facets.ts #formUrl(): FormData → clean search params
110
+ * (shared cleanFormParams from commerce-core), with the price pair
111
+ * merged into one filter.price JSON param. */
112
+ const formUrl = () => {
113
+ const form = formRef.current;
114
+ if (!form)
115
+ return pathname;
116
+ const entries = [];
117
+ let priceMin = '';
118
+ let priceMax = '';
119
+ for (const [name, raw] of new FormData(form)) {
120
+ const value = typeof raw === 'string' ? raw : '';
121
+ if (name === 'price.min') {
122
+ priceMin = value;
123
+ continue;
124
+ }
125
+ if (name === 'price.max') {
126
+ priceMax = value;
127
+ continue;
128
+ }
129
+ entries.push([name, value]);
130
+ }
131
+ const params = cleanFormParams(entries);
132
+ if (priceMin !== '' || priceMax !== '') {
133
+ const price = {};
134
+ if (priceMin !== '')
135
+ price.min = Number(priceMin);
136
+ if (priceMax !== '')
137
+ price.max = Number(priceMax);
138
+ params.append('filter.price', JSON.stringify(price));
139
+ }
140
+ return `${pathname}?${params}`;
141
+ };
142
+ const closePanels = () => {
143
+ for (const panel of rootRef.current?.querySelectorAll('details[open]') ??
144
+ []) {
145
+ panel.removeAttribute('open');
146
+ }
147
+ };
148
+ const openFilters = () => {
149
+ const dialog = dialogRef.current;
150
+ if (dialog && !dialog.open)
151
+ dialog.showModal();
152
+ };
153
+ const cancelFilters = () => dialogRef.current?.close();
154
+ /** Backdrop light-dismiss: clicks on the <dialog> itself hit the backdrop. */
155
+ const onDialogClick = (event) => {
156
+ if (event.target === dialogRef.current)
157
+ dialogRef.current?.close();
158
+ };
159
+ /** Any close path (Cancel, X, Esc, backdrop) discards unapplied edits. */
160
+ const onDialogClose = () => formRef.current?.reset();
161
+ /** Sort radios apply immediately. */
162
+ const onSortChange = () => {
163
+ clearTimeout(timerRef.current);
164
+ timerRef.current = setTimeout(() => void navigate(formUrl()), 100);
165
+ };
166
+ const onSubmit = (event) => {
167
+ event.preventDefault();
168
+ // Capture the form state BEFORE closing — close resets the form
169
+ const url = formUrl();
170
+ dialogRef.current?.close();
171
+ void navigate(url);
172
+ };
173
+ useEffect(() => {
174
+ const onDocumentClick = (event) => {
175
+ if (event.target instanceof Node && rootRef.current?.contains(event.target))
176
+ return;
177
+ closePanels();
178
+ };
179
+ const onKeydown = (event) => {
180
+ if (event.key === 'Escape')
181
+ closePanels();
182
+ };
183
+ document.addEventListener('click', onDocumentClick);
184
+ document.addEventListener('keydown', onKeydown);
185
+ return () => {
186
+ document.removeEventListener('click', onDocumentClick);
187
+ document.removeEventListener('keydown', onKeydown);
188
+ clearTimeout(timerRef.current);
189
+ };
190
+ }, []);
191
+ const clearAllHref = `${resultsUrl}${terms ? `?q=${encodeURIComponent(terms)}` : ''}`;
192
+ const colorNames = t('products.color_option_match')
193
+ .toLowerCase()
194
+ .split(',')
195
+ .map((name) => name.trim())
196
+ .filter(Boolean);
197
+ return (_jsx("div", { ref: rootRef, children: _jsxs("form", { ref: formRef, action: resultsUrl, method: "get", onSubmit: onSubmit, children: [terms ? _jsx("input", { type: "hidden", name: "q", value: terms }) : null, _jsxs("div", { className: "facets-toolbar", children: [_jsx("span", { className: "facets-toolbar__count", children: t(resultsCount === 1 ? 'facets.items.one' : 'facets.items.other', {
198
+ count: resultsCount,
199
+ }) }), _jsxs("div", { className: "facets-toolbar__actions", children: [_jsxs("span", { className: "facets-toolbar__group", children: [_jsxs("button", { type: "button", className: "facets-toolbar__button", onClick: openFilters, children: [t('facets.filter'), _jsxs("span", { className: "facets-toolbar__value", children: ["( ", activeCount, " )"] })] }), activeCount > 0 && (_jsx(Link, { className: "facets-toolbar__clear", to: clearAllHref, "aria-label": t('facets.clear_all'), children: _jsx(Icon, { name: "icon-close" }) }))] }), _jsxs("details", { className: "facet-sort", children: [_jsxs("summary", { className: "facets-toolbar__button", children: [t('facets.sort'), _jsxs("span", { className: "facets-toolbar__value", children: ["( ", sortLabel, " )"] })] }), _jsx("div", { className: "facet-sort__panel", children: sortOptions.map((option) => (_jsxs("label", { className: "facet-sort__option", children: [_jsx("input", { type: "radio", className: "visually-hidden", name: "sort_by", value: option.value, defaultChecked: option.value === currentSort, onChange: onSortChange }), _jsx("span", { children: option.label })] }, option.value))) })] })] })] }), _jsxs("dialog", { className: "facets-dialog", ref: dialogRef, "scroll-lock": "", onClick: onDialogClick, onClose: onDialogClose, "aria-label": t('facets.filters'), children: [_jsxs("header", { className: "facets-dialog__header", children: [_jsx("h2", { className: "facets-dialog__title", children: t('facets.filters') }), _jsx("button", { type: "button", className: "facets-dialog__close", onClick: cancelFilters, "aria-label": t('general.close'), children: _jsx(Icon, { name: "icon-close" }) })] }), _jsx("div", { className: "facets-dialog__body", children: filters.map((filter) => {
200
+ if (isPriceFilter(filter)) {
201
+ return (_jsxs("fieldset", { className: "facets-group", children: [_jsx("legend", { className: "facets-group__label", children: filter.label }), _jsxs("div", { className: "facets-group__price cluster", children: [_jsxs("label", { children: [_jsx("span", { className: "visually-hidden", children: t('facets.from') }), _jsx("input", { type: "number", name: "price.min", placeholder: t('facets.from'), min: "0", defaultValue: appliedPrice.min ?? '' })] }), _jsxs("label", { children: [_jsx("span", { className: "visually-hidden", children: t('facets.to') }), _jsx("input", { type: "number", name: "price.max", placeholder: t('facets.to'), min: "0", defaultValue: appliedPrice.max ?? '' })] })] })] }, filter.id ?? filter.label));
202
+ }
203
+ // Color filters get swatch chips: native swatch data on any
204
+ // value, or a filter named like a color
205
+ const values = filter.values ?? [];
206
+ const filterLabelDown = String(filter.label ?? '').toLowerCase();
207
+ const filterIsColor = Boolean(values[0]?.swatch) ||
208
+ colorNames.some((name) => filterLabelDown.includes(name));
209
+ return (_jsxs("fieldset", { className: "facets-group", children: [_jsx("legend", { className: "facets-group__label", children: filter.label }), _jsx("div", { className: "facets-group__values", children: values.map((value) => {
210
+ const param = valueParam(value);
211
+ if (!param)
212
+ return null;
213
+ const active = searchParams
214
+ .getAll(param.name)
215
+ .includes(param.value);
216
+ const disabled = value.count === 0 && !active;
217
+ return (_jsxs("label", { className: `facet-pill${disabled ? ' facet-pill--disabled' : ''}`, children: [_jsx("input", { type: "checkbox", className: "visually-hidden", name: param.name, value: param.value, defaultChecked: active, disabled: disabled }), _jsxs("span", { className: "facet-pill__label", children: [filterIsColor && (_jsx("span", { className: "swatch facet-pill__swatch", style: swatchStyle(value.swatch, value.label) })), value.label] })] }, value.id ?? param.value));
218
+ }) })] }, filter.id ?? filter.label));
219
+ }) }), _jsxs("footer", { className: "facets-dialog__footer", children: [_jsx("button", { type: "button", className: "facets-dialog__cancel", onClick: cancelFilters, children: t('facets.cancel') }), _jsx("button", { type: "submit", className: "button", children: t('facets.apply') })] })] }), _jsx("noscript", { children: _jsx("button", { type: "submit", className: "button button--secondary", children: t('facets.apply') }) })] }, searchParams.toString()) }));
220
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mirror of `{{ 'icon-*.svg' | inline_asset_content }}`: inlines the
3
+ * theme's SVG icons (copied from the theme's assets/) so they inherit
4
+ * currentColor. The SVG markup is inlined as string constants (copied
5
+ * verbatim from the theme's assets/icons/*.svg) so the package stays
6
+ * bundler-agnostic — no import.meta.glob.
7
+ */
8
+ export type IconName = 'icon-account' | 'icon-cart' | 'icon-chevron-left' | 'icon-chevron-right' | 'icon-close' | 'icon-facebook' | 'icon-instagram' | 'icon-pause' | 'icon-pinterest' | 'icon-play' | 'icon-search' | 'icon-star' | 'icon-tiktok' | 'icon-x' | 'icon-youtube';
9
+ export declare function Icon({ name, className }: {
10
+ name: IconName;
11
+ className?: string;
12
+ }): import("react").JSX.Element | null;