@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,110 @@
1
+ /**
2
+ * Built-in server loaders, keyed by section type (import from
3
+ * '@zalify/storefront-kit/react/server'). Apps spread these into installTheme()
4
+ * alongside their own app-local loaders.
5
+ *
6
+ * featured-collection: server loader for sections/featured-collection
7
+ * .liquid's data needs — the Liquid `section.settings.collection
8
+ * .products` lookup becomes a Storefront API query. When the collection
9
+ * setting is blank (fresh install), fall back to the first available
10
+ * collection so the section still merchandises real products; if
11
+ * nothing resolves the component renders its placeholder cards (the
12
+ * Liquid onboarding state).
13
+ */
14
+ import {PRODUCT_CARD_FRAGMENT} from './components/ProductCard';
15
+ import type {SectionLoader} from './engine/types';
16
+
17
+ const FEATURED_COLLECTION_QUERY = `#graphql
18
+ query ThemeFeaturedCollection($handle: String!, $first: Int!) {
19
+ collection(handle: $handle) {
20
+ id
21
+ handle
22
+ title
23
+ products(first: $first) {
24
+ nodes {
25
+ ...ProductCard
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ${PRODUCT_CARD_FRAGMENT}
31
+ ` as const;
32
+
33
+ const FIRST_COLLECTIONS_QUERY = `#graphql
34
+ query ThemeFirstCollections($first: Int!) {
35
+ collections(first: 10) {
36
+ nodes {
37
+ id
38
+ handle
39
+ title
40
+ products(first: $first) {
41
+ nodes {
42
+ ...ProductCard
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ${PRODUCT_CARD_FRAGMENT}
49
+ ` as const;
50
+
51
+ const LATEST_PRODUCTS_QUERY = `#graphql
52
+ query ThemeLatestProducts($first: Int!) {
53
+ products(first: $first, sortKey: CREATED_AT, reverse: true) {
54
+ nodes {
55
+ ...ProductCard
56
+ }
57
+ }
58
+ }
59
+ ${PRODUCT_CARD_FRAGMENT}
60
+ ` as const;
61
+
62
+ const featuredCollectionLoader: SectionLoader = async ({
63
+ settings,
64
+ storefront,
65
+ }) => {
66
+ const first = Number(settings.products_to_show ?? 4) || 4;
67
+ const handle =
68
+ typeof settings.collection === 'string' && settings.collection
69
+ ? settings.collection
70
+ : null;
71
+
72
+ if (handle) {
73
+ const data = await storefront.query(FEATURED_COLLECTION_QUERY, {
74
+ variables: {handle, first},
75
+ cache: storefront.CacheLong?.(),
76
+ });
77
+ if (data?.collection) return {collection: data.collection};
78
+ }
79
+
80
+ // Fresh-install fallback: first collection that actually has
81
+ // products, then the latest products as a synthetic collection —
82
+ // placeholders only when the store is truly empty.
83
+ const data = await storefront.query(FIRST_COLLECTIONS_QUERY, {
84
+ variables: {first},
85
+ cache: storefront.CacheLong?.(),
86
+ });
87
+ const nonEmpty = (data?.collections?.nodes ?? []).find(
88
+ (node: {products?: {nodes?: unknown[]}}) => node.products?.nodes?.length,
89
+ );
90
+ if (nonEmpty) return {collection: nonEmpty};
91
+
92
+ const latest = await storefront.query(LATEST_PRODUCTS_QUERY, {
93
+ variables: {first},
94
+ cache: storefront.CacheLong?.(),
95
+ });
96
+ const products = latest?.products?.nodes ?? [];
97
+ if (!products.length) return {collection: null};
98
+ return {
99
+ collection: {
100
+ id: 'latest',
101
+ handle: 'all',
102
+ title: 'Featured products',
103
+ products: {nodes: products},
104
+ },
105
+ };
106
+ };
107
+
108
+ export const builtinSectionLoaders: Record<string, SectionLoader> = {
109
+ 'featured-collection': featuredCollectionLoader,
110
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Built-in component registries. Keys are the theme type names — the
3
+ * section/block `type` values in templates/*.json, which equal the old
4
+ * app/sections/*.tsx and app/blocks/*.tsx file names (including leading
5
+ * underscores and '404'). Apps spread these into installTheme() and add
6
+ * their own app-local sections/blocks on top.
7
+ */
8
+ import type {BlockComponent, SectionComponent} from './engine/types';
9
+
10
+ // Blocks
11
+ import text from './blocks/text';
12
+ import button from './blocks/button';
13
+ import group from './blocks/group';
14
+ import spacer from './blocks/spacer';
15
+ import image from './blocks/image';
16
+ import box from './blocks/_box';
17
+ import slide from './blocks/_slide';
18
+ import story from './blocks/story';
19
+ import message from './blocks/message';
20
+ import quote from './blocks/quote';
21
+ import accordion from './blocks/accordion';
22
+ import countdown from './blocks/countdown';
23
+ import customLiquidBlock from './blocks/custom-liquid';
24
+ import video from './blocks/video';
25
+ import videoCard from './blocks/_video-card';
26
+
27
+ // Sections
28
+ import hero from './sections/hero';
29
+ import stories from './sections/stories';
30
+ import featuredCollection from './sections/featured-collection';
31
+ import multicolumn from './sections/multicolumn';
32
+ import imageWithText from './sections/image-with-text';
33
+ import richText from './sections/rich-text';
34
+ import customSection from './sections/custom-section';
35
+ import customLiquidSection from './sections/custom-liquid';
36
+ import announcementBar from './sections/announcement-bar';
37
+ import notFound from './sections/404';
38
+ import page from './sections/page';
39
+ import article from './sections/article';
40
+ import password from './sections/password';
41
+ import contactForm from './sections/contact-form';
42
+ import videoCarousel from './sections/video-carousel';
43
+ import videoBubble from './sections/video-bubble';
44
+
45
+ export const builtinSections: Record<string, SectionComponent> = {
46
+ hero,
47
+ stories,
48
+ 'featured-collection': featuredCollection,
49
+ multicolumn,
50
+ 'image-with-text': imageWithText,
51
+ 'rich-text': richText,
52
+ 'custom-section': customSection,
53
+ 'custom-liquid': customLiquidSection,
54
+ 'announcement-bar': announcementBar,
55
+ '404': notFound,
56
+ page,
57
+ article,
58
+ password,
59
+ 'contact-form': contactForm,
60
+ 'video-carousel': videoCarousel,
61
+ 'video-bubble': videoBubble,
62
+ };
63
+
64
+ export const builtinBlocks: Record<string, BlockComponent> = {
65
+ text,
66
+ button,
67
+ group,
68
+ spacer,
69
+ image,
70
+ _box: box,
71
+ _slide: slide,
72
+ story,
73
+ message,
74
+ quote,
75
+ accordion,
76
+ countdown,
77
+ 'custom-liquid': customLiquidBlock,
78
+ video,
79
+ '_video-card': videoCard,
80
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Port of sections/404.liquid. CSS: app/styles/components/sections-404.css
3
+ */
4
+ import type {SectionProps} from '../engine/types';
5
+ import {t} from '../engine/translate';
6
+ import {useAdapter} from '../adapter';
7
+
8
+ interface NotFoundSettings {
9
+ color_scheme?: string;
10
+ section_spacing?: string;
11
+ }
12
+
13
+ export default function NotFoundSection({
14
+ settings,
15
+ }: SectionProps<NotFoundSettings>) {
16
+ const {Link} = useAdapter();
17
+ const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
18
+ settings;
19
+ return (
20
+ <div
21
+ className={`not-found section color-${colorScheme} full-width`}
22
+ style={
23
+ {
24
+ '--section-spacing': `var(--space-section-${spacing})`,
25
+ } as React.CSSProperties
26
+ }
27
+ >
28
+ <div className="not-found__inner stack">
29
+ <h1 className="not-found__title">{t('404.title')}</h1>
30
+ <p>{t('404.not_found')}</p>
31
+ <Link to="/collections/all" className="button">
32
+ {t('404.back_to_shopping')}
33
+ </Link>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Port of sections/announcement-bar.liquid — rotating messages bar.
3
+ * The Liquid theme drives rotation with the <announcement-bar> web
4
+ * component (src/entries/announcement.ts); here the same behavior
5
+ * lives in React: autoplay (paused for reduced motion), prev/next,
6
+ * pause/play. data-paused on the host drives the icon swap in CSS.
7
+ * CSS: app/styles/components/sections-announcement-bar.css
8
+ */
9
+ import {useEffect, useRef, useState} from 'react';
10
+ import type {SectionProps} from '../engine/types';
11
+ import {t} from '../engine/translate';
12
+ import {Icon} from '../components/Icon';
13
+
14
+ declare global {
15
+ namespace JSX {
16
+ interface IntrinsicElements {
17
+ /* The extracted CSS targets the announcement-bar element itself
18
+ (announcement-bar[data-paused] …), so the tag name is part of
19
+ the styling contract and must be kept. */
20
+ 'announcement-bar': React.DetailedHTMLProps<
21
+ React.HTMLAttributes<HTMLElement>,
22
+ HTMLElement
23
+ >;
24
+ }
25
+ }
26
+ }
27
+
28
+ interface AnnouncementBarSettings {
29
+ rotation_speed?: number;
30
+ color_scheme?: string;
31
+ }
32
+
33
+ /*
34
+ * The bar's height is a CONSTANT, published as a token so layout math
35
+ * (e.g. the large hero's "viewport minus header stack") can be computed
36
+ * in pure CSS at first paint — mirror of the section's {% style %} tag.
37
+ * Removing this section removes the token (consumers fall back to 0).
38
+ */
39
+ const HEIGHT_TOKEN_CSS = `
40
+ :root {
41
+ --announcement-bar-height: calc(0.6875rem * 2.8 + 2 * var(--space-xs));
42
+ }
43
+ @media (min-width: 48rem) {
44
+ :root {
45
+ --announcement-bar-height: calc(var(--type-xs) * 2.8 + 2 * var(--space-xs));
46
+ }
47
+ }
48
+ `;
49
+
50
+ export default function AnnouncementBarSection({
51
+ section,
52
+ settings,
53
+ children,
54
+ }: SectionProps<AnnouncementBarSettings>) {
55
+ const {rotation_speed: rotationSpeed = 5, color_scheme: colorScheme = 'scheme-4'} =
56
+ settings;
57
+ const messageCount = (section.block_order ?? []).filter((key) => {
58
+ const block = section.blocks?.[key];
59
+ return block && !block.disabled;
60
+ }).length;
61
+
62
+ const hostRef = useRef<HTMLElement>(null);
63
+ const indexRef = useRef(0);
64
+ const timerRef = useRef<ReturnType<typeof setInterval>>();
65
+ const pausedRef = useRef(false);
66
+ const [paused, setPaused] = useState(false);
67
+
68
+ const messages = (): HTMLElement[] =>
69
+ Array.from(
70
+ hostRef.current?.querySelectorAll<HTMLElement>(
71
+ '.announcement-bar__message',
72
+ ) ?? [],
73
+ );
74
+
75
+ const show = (index: number) => {
76
+ const items = messages();
77
+ if (items.length < 2) return;
78
+ indexRef.current = (index + items.length) % items.length;
79
+ items.forEach((message, i) => {
80
+ message.hidden = i !== indexRef.current;
81
+ });
82
+ };
83
+
84
+ const start = () => {
85
+ clearInterval(timerRef.current);
86
+ if (pausedRef.current || messageCount < 2) return;
87
+ const speed = rotationSpeed || 5;
88
+ timerRef.current = setInterval(
89
+ () => show(indexRef.current + 1),
90
+ speed * 1000,
91
+ );
92
+ };
93
+
94
+ const setPausedState = (value: boolean) => {
95
+ pausedRef.current = value;
96
+ setPaused(value);
97
+ };
98
+
99
+ useEffect(() => {
100
+ // Autoplay starts paused when the user prefers reduced motion.
101
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
102
+ setPausedState(true);
103
+ }
104
+ start();
105
+ return () => clearInterval(timerRef.current);
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [messageCount, rotationSpeed]);
108
+
109
+ const previous = () => {
110
+ show(indexRef.current - 1);
111
+ start();
112
+ };
113
+ const next = () => {
114
+ show(indexRef.current + 1);
115
+ start();
116
+ };
117
+ const toggle = () => {
118
+ setPausedState(!pausedRef.current);
119
+ // pausedRef is already updated; restart (or stop) the timer.
120
+ start();
121
+ };
122
+
123
+ return (
124
+ <announcement-bar
125
+ ref={hostRef}
126
+ className={`announcement-bar color-${colorScheme} full-width`}
127
+ data-speed={rotationSpeed}
128
+ data-paused={paused ? '' : undefined}
129
+ >
130
+ <style dangerouslySetInnerHTML={{__html: HEIGHT_TOKEN_CSS}} />
131
+ <div className="announcement-bar__inner">
132
+ <div className="announcement-bar__messages">{children}</div>
133
+
134
+ {messageCount > 1 && (
135
+ <div className="announcement-bar__controls">
136
+ <button
137
+ type="button"
138
+ className="announcement-bar__control"
139
+ onClick={previous}
140
+ aria-label={t('general.previous')}
141
+ >
142
+ <Icon name="icon-chevron-left" />
143
+ </button>
144
+ <button
145
+ type="button"
146
+ className="announcement-bar__control"
147
+ onClick={next}
148
+ aria-label={t('general.next')}
149
+ >
150
+ <Icon name="icon-chevron-right" />
151
+ </button>
152
+ <button
153
+ type="button"
154
+ className="announcement-bar__control announcement-bar__toggle"
155
+ onClick={toggle}
156
+ aria-label={t('general.pause')}
157
+ >
158
+ <span className="announcement-bar__icon-pause">
159
+ <Icon name="icon-pause" />
160
+ </span>
161
+ <span className="announcement-bar__icon-play">
162
+ <Icon name="icon-play" />
163
+ </span>
164
+ </button>
165
+ </div>
166
+ )}
167
+ </div>
168
+ </announcement-bar>
169
+ );
170
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Port of sections/article.liquid. CSS: app/styles/components/sections-article.css
3
+ *
4
+ * Data: useResource('article') — the scaffold ARTICLE query shape
5
+ * ({handle, title, contentHtml, publishedAt, author: {name}, image}),
6
+ * plus useResource('blog') ({handle, title}) for the back link.
7
+ *
8
+ * The Liquid comments block (blog.comments_enabled?, {% form
9
+ * 'new_comment' %}) has no Storefront API mirror and is omitted.
10
+ */
11
+ import type {SectionProps} from '../engine/types';
12
+ import {useResource} from '../engine/context';
13
+ import {t} from '../engine/translate';
14
+ import {imageSrcSet, imageUrl} from '../engine/images';
15
+ import {useAdapter} from '../adapter';
16
+
17
+ interface ArticleSettings {
18
+ color_scheme?: string;
19
+ section_spacing?: string;
20
+ }
21
+
22
+ function articleDateHtml(publishedAt?: string): string {
23
+ if (!publishedAt) return '';
24
+ const formatted = new Intl.DateTimeFormat('en-US', {
25
+ year: 'numeric',
26
+ month: 'long',
27
+ day: 'numeric',
28
+ }).format(new Date(publishedAt));
29
+ return `<time datetime="${publishedAt}">${formatted}</time>`;
30
+ }
31
+
32
+ export default function ArticleSection({
33
+ settings,
34
+ }: SectionProps<ArticleSettings>) {
35
+ const {Link} = useAdapter();
36
+ const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
37
+ settings;
38
+ const article = useResource<any>('article');
39
+ const blog = useResource<any>('blog') ?? article?.blog;
40
+
41
+ if (!article) return null;
42
+
43
+ return (
44
+ <article
45
+ className={`article section color-${colorScheme} full-width`}
46
+ style={
47
+ {
48
+ '--section-spacing': `var(--space-section-${spacing})`,
49
+ } as React.CSSProperties
50
+ }
51
+ >
52
+ <div
53
+ className="article__inner stack"
54
+ style={{'--stack-gap': 'var(--space-lg)'} as React.CSSProperties}
55
+ >
56
+ <header
57
+ className="article__header stack"
58
+ style={{'--stack-gap': 'var(--space-sm)'} as React.CSSProperties}
59
+ >
60
+ {blog?.handle && (
61
+ <Link className="article__back" to={`/blogs/${blog.handle}`}>
62
+ {t('blog.back_to_blog', {blog: blog.title ?? blog.handle})}
63
+ </Link>
64
+ )}
65
+ <p
66
+ className="article__meta"
67
+ dangerouslySetInnerHTML={{
68
+ __html: t('blog.article_metadata_html', {
69
+ date: articleDateHtml(article.publishedAt),
70
+ author: article.author?.name ?? '',
71
+ }),
72
+ }}
73
+ />
74
+ <h1 className="article__title">{article.title}</h1>
75
+ </header>
76
+
77
+ {article.image?.url && (
78
+ <img
79
+ className="article__image"
80
+ src={imageUrl(article.image.url, 1500)}
81
+ srcSet={imageSrcSet(article.image.url)}
82
+ sizes="(min-width: 64rem) 60rem, 100vw"
83
+ loading="eager"
84
+ width={article.image.width ?? undefined}
85
+ height={article.image.height ?? undefined}
86
+ alt={article.image.altText ?? article.title ?? ''}
87
+ />
88
+ )}
89
+
90
+ <div
91
+ className="article__content prose"
92
+ dangerouslySetInnerHTML={{__html: article.contentHtml ?? ''}}
93
+ />
94
+ </div>
95
+ </article>
96
+ );
97
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Port of sections/contact-form.liquid.
3
+ * CSS: app/styles/components/sections-contact-form.css
4
+ *
5
+ * Shopify's native `{% form 'contact' %}` POST endpoint isn't available
6
+ * in Hydrogen — the markup is rendered faithfully but submission is a
7
+ * no-op. TODO: wire the submit to a route action (e.g. a proxy to the
8
+ * Online Store contact endpoint or a third-party form service).
9
+ */
10
+ import type {SectionProps} from '../engine/types';
11
+ import {t} from '../engine/translate';
12
+
13
+ interface ContactFormSettings {
14
+ color_scheme?: string;
15
+ section_spacing?: string;
16
+ }
17
+
18
+ export default function ContactFormSection({
19
+ id,
20
+ settings,
21
+ }: SectionProps<ContactFormSettings>) {
22
+ const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
23
+ settings;
24
+
25
+ return (
26
+ <div
27
+ className={`contact section color-${colorScheme} full-width`}
28
+ style={
29
+ {
30
+ '--section-spacing': `var(--space-section-${spacing})`,
31
+ } as React.CSSProperties
32
+ }
33
+ >
34
+ <div
35
+ className="contact__inner stack"
36
+ style={{'--stack-gap': 'var(--space-lg)'} as React.CSSProperties}
37
+ >
38
+ {/* TODO: POST somewhere useful — Shopify contact form posts aren't available in Hydrogen. */}
39
+ <form
40
+ className="contact__form stack"
41
+ method="post"
42
+ onSubmit={(event) => event.preventDefault()}
43
+ >
44
+ <div className="contact__row">
45
+ <div className="field">
46
+ <label className="field__label" htmlFor={`ContactName-${id}`}>
47
+ {t('contact.name')}
48
+ </label>
49
+ <input
50
+ id={`ContactName-${id}`}
51
+ type="text"
52
+ name="contact[name]"
53
+ autoComplete="name"
54
+ />
55
+ </div>
56
+ <div className="field">
57
+ <label className="field__label" htmlFor={`ContactEmail-${id}`}>
58
+ {t('contact.email')}
59
+ </label>
60
+ <input
61
+ id={`ContactEmail-${id}`}
62
+ type="email"
63
+ name="contact[email]"
64
+ autoComplete="email"
65
+ spellCheck={false}
66
+ autoCapitalize="off"
67
+ required
68
+ />
69
+ </div>
70
+ </div>
71
+
72
+ <div className="field">
73
+ <label className="field__label" htmlFor={`ContactPhone-${id}`}>
74
+ {t('contact.phone')}
75
+ </label>
76
+ <input
77
+ id={`ContactPhone-${id}`}
78
+ type="tel"
79
+ name="contact[phone]"
80
+ autoComplete="tel"
81
+ pattern="[0-9\-]*"
82
+ />
83
+ </div>
84
+
85
+ <div className="field">
86
+ <label className="field__label" htmlFor={`ContactMessage-${id}`}>
87
+ {t('contact.message')}
88
+ </label>
89
+ <textarea
90
+ id={`ContactMessage-${id}`}
91
+ name="contact[body]"
92
+ rows={8}
93
+ required
94
+ />
95
+ </div>
96
+
97
+ <button type="submit" className="button contact__submit">
98
+ {t('contact.send')}
99
+ </button>
100
+ </form>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Port of sections/custom-liquid.liquid — merchant-pasted markup.
3
+ * The setting's raw HTML is injected as-is; any Liquid tags/objects in
4
+ * it cannot execute here (no Liquid runtime), so only the plain-HTML
5
+ * use case survives the mirror. CSS: sections-custom-liquid.css
6
+ */
7
+ import type {SectionProps} from '../engine/types';
8
+
9
+ interface CustomLiquidSettings {
10
+ custom_liquid?: string;
11
+ color_scheme?: string;
12
+ section_spacing?: 'none' | 'sm' | 'md' | 'lg';
13
+ }
14
+
15
+ export default function CustomLiquidSection({
16
+ settings,
17
+ }: SectionProps<CustomLiquidSettings>) {
18
+ const {
19
+ custom_liquid: customLiquid = '',
20
+ color_scheme: colorScheme = 'scheme-1',
21
+ section_spacing: sectionSpacing = 'md',
22
+ } = settings;
23
+
24
+ return (
25
+ <div
26
+ className={`custom-liquid section color-${colorScheme} full-width`}
27
+ style={
28
+ {
29
+ '--section-spacing': `var(--space-section-${sectionSpacing})`,
30
+ } as React.CSSProperties
31
+ }
32
+ >
33
+ <div
34
+ className="custom-liquid__inner"
35
+ dangerouslySetInnerHTML={{__html: customLiquid}}
36
+ />
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Port of sections/custom-section.liquid — the canonical v3 section
3
+ * pattern: color_scheme + section_spacing on the wrapper, optional
4
+ * background image, blocks in a content grid.
5
+ * CSS: app/styles/components/sections-custom-section.css
6
+ */
7
+ import type {SectionProps} from '../engine/types';
8
+ import {imageUrl, settingImageUrl} from '../engine/images';
9
+
10
+ interface CustomSectionSettings {
11
+ background_image?: string;
12
+ color_scheme?: string;
13
+ section_spacing?: 'none' | 'sm' | 'md' | 'lg';
14
+ }
15
+
16
+ export default function CustomSection({
17
+ settings,
18
+ children,
19
+ }: SectionProps<CustomSectionSettings>) {
20
+ const {
21
+ color_scheme: colorScheme = 'scheme-1',
22
+ section_spacing: sectionSpacing = 'md',
23
+ } = settings;
24
+ const backgroundImage = settingImageUrl(settings.background_image);
25
+
26
+ return (
27
+ <div
28
+ className={`custom-section section full-width color-${colorScheme}`}
29
+ style={
30
+ {
31
+ '--section-spacing': `var(--space-section-${sectionSpacing})`,
32
+ } as React.CSSProperties
33
+ }
34
+ >
35
+ {backgroundImage ? (
36
+ <div className="custom-section__background">
37
+ <img src={imageUrl(backgroundImage, 2000)} alt="" />
38
+ </div>
39
+ ) : null}
40
+
41
+ <div className="custom-section__content">{children}</div>
42
+ </div>
43
+ );
44
+ }