acinguiux-preact-components 0.0.1

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 (313) hide show
  1. package/package.json +56 -0
  2. package/src/content/themes/theme-acinguiux-amg/theme-acinguiux-amg.css +23 -0
  3. package/src/content/themes/theme-acinguiux-cafe/theme-acinguiux-cafe.css +47 -0
  4. package/src/content/themes/theme-acinguiux-energy/theme-acinguiux-energy.css +45 -0
  5. package/src/content/themes/theme-acinguiux-livewire/theme-acinguiux-livewire.css +22 -0
  6. package/src/content/themes/theme-acinguiux-livewire-italy/theme-acinguiux-livewire-italy.css +22 -0
  7. package/src/content/themes/theme-acinguiux-recharge/theme-acinguiux-recharge.css +49 -0
  8. package/src/content/themes/theme-allon/theme-allon.css +25 -0
  9. package/src/content/themes/theme-atlas/theme-atlas.css +31 -0
  10. package/src/content/themes/theme-aurvana/resources/favicon/apple-touch-icon.png +0 -0
  11. package/src/content/themes/theme-aurvana/resources/favicon/favico.ico +0 -0
  12. package/src/content/themes/theme-aurvana/resources/favicon/favicon-96x96.png +0 -0
  13. package/src/content/themes/theme-aurvana/resources/favicon/favicon.ico +0 -0
  14. package/src/content/themes/theme-aurvana/resources/favicon/favicon.png +0 -0
  15. package/src/content/themes/theme-aurvana/resources/favicon/favicon.svg +13 -0
  16. package/src/content/themes/theme-aurvana/resources/favicon/google-touch-icon.png +0 -0
  17. package/src/content/themes/theme-aurvana/resources/favicon/manifest.json +14 -0
  18. package/src/content/themes/theme-aurvana/resources/favicon/site.webmanifest +21 -0
  19. package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-192x192.png +0 -0
  20. package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-512x512.png +0 -0
  21. package/src/content/themes/theme-aurvana/theme-aurvana.css +49 -0
  22. package/src/content/themes/theme-base/theme-base.css +49 -0
  23. package/src/content/themes/theme-base2/resources/favicon/android-chrome-192x192.png +0 -0
  24. package/src/content/themes/theme-base2/resources/favicon/android-chrome-512x512.png +0 -0
  25. package/src/content/themes/theme-base2/resources/favicon/apple-touch-icon.png +0 -0
  26. package/src/content/themes/theme-base2/resources/favicon/favico.ico +0 -0
  27. package/src/content/themes/theme-base2/resources/favicon/favicon-16x16.png +0 -0
  28. package/src/content/themes/theme-base2/resources/favicon/favicon-32x32.png +0 -0
  29. package/src/content/themes/theme-base2/resources/favicon/favicon-96x96.png +0 -0
  30. package/src/content/themes/theme-base2/resources/favicon/favicon.ico +0 -0
  31. package/src/content/themes/theme-base2/resources/favicon/favicon.png +0 -0
  32. package/src/content/themes/theme-base2/resources/favicon/favicon.svg +9 -0
  33. package/src/content/themes/theme-base2/resources/favicon/google-touch-icon.png +0 -0
  34. package/src/content/themes/theme-base2/resources/favicon/manifest.json +14 -0
  35. package/src/content/themes/theme-base2/resources/favicon/site.webmanifest +1 -0
  36. package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-192x192.png +0 -0
  37. package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-512x512.png +0 -0
  38. package/src/content/themes/theme-base2/resources/fonts/acinguiux-typeface-la-heavy-221208.woff2 +0 -0
  39. package/src/content/themes/theme-base2/theme-base2.css +47 -0
  40. package/src/content/themes/theme-eco-marathon/theme-eco-marathon.css +22 -0
  41. package/src/content/themes/theme-energy-transition-campus-amsterdam/theme-energy-transition-campus-amsterdam.css +26 -0
  42. package/src/content/themes/theme-evpass/theme-evpass.css +46 -0
  43. package/src/content/themes/theme-nam-2025/resources/favicon/apple-touch-icon.png +0 -0
  44. package/src/content/themes/theme-nam-2025/resources/favicon/favico.ico +0 -0
  45. package/src/content/themes/theme-nam-2025/resources/favicon/favicon-96x96.png +0 -0
  46. package/src/content/themes/theme-nam-2025/resources/favicon/favicon.ico +0 -0
  47. package/src/content/themes/theme-nam-2025/resources/favicon/favicon.png +0 -0
  48. package/src/content/themes/theme-nam-2025/resources/favicon/favicon.svg +9 -0
  49. package/src/content/themes/theme-nam-2025/resources/favicon/google-touch-icon.png +0 -0
  50. package/src/content/themes/theme-nam-2025/resources/favicon/manifest.json +14 -0
  51. package/src/content/themes/theme-nam-2025/resources/favicon/site.webmanifest +21 -0
  52. package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-192x192.png +0 -0
  53. package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-512x512.png +0 -0
  54. package/src/content/themes/theme-nam-2025/theme-nam-2025.css +47 -0
  55. package/src/content/themes/theme-pennzoil/theme-pennzoil.css +36 -0
  56. package/src/content/themes/theme-quaker-state/theme-quaker-state.css +63 -0
  57. package/src/content/themes/theme-tafawoq/theme-tafawoq.css +26 -0
  58. package/src/content/themes/theme-vegetable/resources/favicon/apple-touch-icon.png +0 -0
  59. package/src/content/themes/theme-vegetable/resources/favicon/favico.ico +0 -0
  60. package/src/content/themes/theme-vegetable/resources/favicon/favicon-96x96.png +0 -0
  61. package/src/content/themes/theme-vegetable/resources/favicon/favicon.ico +0 -0
  62. package/src/content/themes/theme-vegetable/resources/favicon/favicon.png +0 -0
  63. package/src/content/themes/theme-vegetable/resources/favicon/favicon.svg +13 -0
  64. package/src/content/themes/theme-vegetable/resources/favicon/google-touch-icon.png +0 -0
  65. package/src/content/themes/theme-vegetable/resources/favicon/manifest.json +14 -0
  66. package/src/content/themes/theme-vegetable/resources/favicon/site.webmanifest +21 -0
  67. package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-192x192.png +0 -0
  68. package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-512x512.png +0 -0
  69. package/src/content/themes/theme-vegetable/theme-vegetable.css +49 -0
  70. package/src/content/themes/theme-zeolyst/resources/fonts/type-ar-medium.woff2 +0 -0
  71. package/src/content/themes/theme-zeolyst/theme-zeolyst.css +29 -0
  72. package/src/main/atoms/audio.js +16 -0
  73. package/src/main/atoms/box.js +5 -0
  74. package/src/main/atoms/button.js +40 -0
  75. package/src/main/atoms/card.js +22 -0
  76. package/src/main/atoms/form.js +30 -0
  77. package/src/main/atoms/heading.js +17 -0
  78. package/src/main/atoms/icon.js +24 -0
  79. package/src/main/atoms/img.js +131 -0
  80. package/src/main/atoms/input.js +55 -0
  81. package/src/main/atoms/link-text.js +21 -0
  82. package/src/main/atoms/link.js +60 -0
  83. package/src/main/atoms/list.js +12 -0
  84. package/src/main/atoms/logo.js +9 -0
  85. package/src/main/atoms/menu.js +10 -0
  86. package/src/main/atoms/message.js +5 -0
  87. package/src/main/atoms/nav-link.js +49 -0
  88. package/src/main/atoms/popup.js +47 -0
  89. package/src/main/atoms/rich-text.js +128 -0
  90. package/src/main/atoms/scroller.js +224 -0
  91. package/src/main/atoms/svg.js +65 -0
  92. package/src/main/atoms/table.js +32 -0
  93. package/src/main/atoms/textarea.js +10 -0
  94. package/src/main/atoms/time.js +12 -0
  95. package/src/main/atoms/video.js +100 -0
  96. package/src/main/export-main.js +12 -0
  97. package/src/main/export-matter.js +86 -0
  98. package/src/main/export-preact-hooks.js +1 -0
  99. package/src/main/export-preact.js +1 -0
  100. package/src/main/index.js +13 -0
  101. package/src/main/molecules/asset.js +23 -0
  102. package/src/main/molecules/glossary.js +44 -0
  103. package/src/main/molecules/links.js +23 -0
  104. package/src/main/molecules/promo-text.js +27 -0
  105. package/src/main/molecules/tags.js +15 -0
  106. package/src/main/molecules/tree.js +51 -0
  107. package/src/main/organisms/accordion-item.js +106 -0
  108. package/src/main/organisms/author.js +29 -0
  109. package/src/main/organisms/breadcrumb.js +69 -0
  110. package/src/main/organisms/call-to-action.js +24 -0
  111. package/src/main/organisms/carousel.js +178 -0
  112. package/src/main/organisms/cart-item.js +156 -0
  113. package/src/main/organisms/cart.js +162 -0
  114. package/src/main/organisms/contact-form.js +141 -0
  115. package/src/main/organisms/container/ab-test.js +47 -0
  116. package/src/main/organisms/container/default.js +6 -0
  117. package/src/main/organisms/container/filtered-section.js +293 -0
  118. package/src/main/organisms/container/footer.js +12 -0
  119. package/src/main/organisms/container/grid.js +44 -0
  120. package/src/main/organisms/container/header.js +13 -0
  121. package/src/main/organisms/container/list.js +7 -0
  122. package/src/main/organisms/container/main.js +6 -0
  123. package/src/main/organisms/container/raw.js +7 -0
  124. package/src/main/organisms/container/section.js +28 -0
  125. package/src/main/organisms/container.js +29 -0
  126. package/src/main/organisms/content-owner.js +15 -0
  127. package/src/main/organisms/date-entry.js +56 -0
  128. package/src/main/organisms/external-search.js +73 -0
  129. package/src/main/organisms/filtered-item.js +163 -0
  130. package/src/main/organisms/footer-item.js +17 -0
  131. package/src/main/organisms/image-gallery.js +164 -0
  132. package/src/main/organisms/last-modified.js +20 -0
  133. package/src/main/organisms/legal-footer.js +16 -0
  134. package/src/main/organisms/list-item.js +48 -0
  135. package/src/main/organisms/metadata.js +11 -0
  136. package/src/main/organisms/navigation.js +232 -0
  137. package/src/main/organisms/notification.js +87 -0
  138. package/src/main/organisms/order-tracker.js +203 -0
  139. package/src/main/organisms/page-header-banner.js +26 -0
  140. package/src/main/organisms/page-header.js +33 -0
  141. package/src/main/organisms/page-tags.js +14 -0
  142. package/src/main/organisms/page.js +260 -0
  143. package/src/main/organisms/press-release.js +24 -0
  144. package/src/main/organisms/product-admin.js +204 -0
  145. package/src/main/organisms/promo-banner.js +28 -0
  146. package/src/main/organisms/promo-bottom.js +23 -0
  147. package/src/main/organisms/promo-button.js +8 -0
  148. package/src/main/organisms/promo-card-cover.js +35 -0
  149. package/src/main/organisms/promo-card.js +33 -0
  150. package/src/main/organisms/promo-full.js +20 -0
  151. package/src/main/organisms/promo-image.js +22 -0
  152. package/src/main/organisms/promo-lure.js +22 -0
  153. package/src/main/organisms/promo-product-card.js +187 -0
  154. package/src/main/organisms/promo-product-full.js +293 -0
  155. package/src/main/organisms/promo-simple.js +23 -0
  156. package/src/main/organisms/quote.js +21 -0
  157. package/src/main/organisms/search-form.js +42 -0
  158. package/src/main/organisms/search-nav.js +66 -0
  159. package/src/main/organisms/search-result.js +53 -0
  160. package/src/main/organisms/slider.js +26 -0
  161. package/src/main/organisms/standalone-asset.js +22 -0
  162. package/src/main/organisms/tabs.js +277 -0
  163. package/src/main/organisms/topbar.js +83 -0
  164. package/src/main/organisms/web-component.js +53 -0
  165. package/src/main/routing/annotation.js +9 -0
  166. package/src/main/routing/component.js +138 -0
  167. package/src/main/routing/empty.js +5 -0
  168. package/src/main/routing/error-handler.js +64 -0
  169. package/src/main/routing/placeholder-image.svg +5 -0
  170. package/src/main/routing/router.js +219 -0
  171. package/src/main/shared/analytics.js +677 -0
  172. package/src/main/shared/bubble-event.js +11 -0
  173. package/src/main/shared/custom-element.js +21 -0
  174. package/src/main/shared/deep-selector.js +28 -0
  175. package/src/main/shared/disable-transparency.js +10 -0
  176. package/src/main/shared/format-time.js +8 -0
  177. package/src/main/shared/get-id.js +5 -0
  178. package/src/main/shared/get-meta.js +3 -0
  179. package/src/main/shared/get-size-class.js +3 -0
  180. package/src/main/shared/get-size.js +11 -0
  181. package/src/main/shared/h.js +88 -0
  182. package/src/main/shared/hash-jump.js +33 -0
  183. package/src/main/shared/icons/arrow-back.svg +1 -0
  184. package/src/main/shared/icons/arrow-down.svg +1 -0
  185. package/src/main/shared/icons/arrow-next.svg +1 -0
  186. package/src/main/shared/icons/arrow-tail-right.svg +1 -0
  187. package/src/main/shared/icons/arrow-tail-up.svg +1 -0
  188. package/src/main/shared/icons/arrow-up.svg +1 -0
  189. package/src/main/shared/icons/asset-download.svg +1 -0
  190. package/src/main/shared/icons/logo.svg +5 -0
  191. package/src/main/shared/icons/low-carbon-placeholder.svg +9 -0
  192. package/src/main/shared/icons/media-pause.svg +1 -0
  193. package/src/main/shared/icons/media-play.svg +1 -0
  194. package/src/main/shared/icons/navigation-burger.svg +1 -0
  195. package/src/main/shared/icons/navigation-close.svg +1 -0
  196. package/src/main/shared/icons/navigation-link.svg +1 -0
  197. package/src/main/shared/icons/navigation-refresh.svg +1 -0
  198. package/src/main/shared/icons/navigation-search.svg +1 -0
  199. package/src/main/shared/icons/navigation-share.svg +1 -0
  200. package/src/main/shared/icons/toggle-newwindow.svg +1 -0
  201. package/src/main/shared/icons.js +18 -0
  202. package/src/main/shared/id-from-string.js +5 -0
  203. package/src/main/shared/mark-selection.js +19 -0
  204. package/src/main/shared/register.js +26 -0
  205. package/src/main/shared/renderer.js +43 -0
  206. package/src/main/shared/simple-consent-api.js +70 -0
  207. package/src/main/shared/split-links.js +11 -0
  208. package/src/main/shared/t.js +60 -0
  209. package/src/main/shared/twind.js +837 -0
  210. package/src/main/shared/update-head.js +34 -0
  211. package/src/main/shared/update-scrollbar-width.js +30 -0
  212. package/src/main/shared/use-link.js +151 -0
  213. package/src/main/shared/use-persistent-state.js +42 -0
  214. package/src/main/shared/wait-for-dom-ready.js +6 -0
  215. package/src/main/shared/wcm-mode.js +4 -0
  216. package/src/wcs/components/acinguiux-preact-doc/acinguiux-preact-doc.js +207 -0
  217. package/src/wcs/components/admin-dashboard/admin-dashboard.js +487 -0
  218. package/src/wcs/components/admin-login/admin-login.js +91 -0
  219. package/src/wcs/components/bazaar-voice/bazaar-voice.js +56 -0
  220. package/src/wcs/components/chatbot-koreai/chatbot-koreai.js +176 -0
  221. package/src/wcs/components/chatbot-koreai/koreai-transport.js +217 -0
  222. package/src/wcs/components/chatbot-ms/chatbot-ms.js +210 -0
  223. package/src/wcs/components/chatbot-test/chatbot-test.js +44 -0
  224. package/src/wcs/components/comparison-chart/comparison-chart.js +111 -0
  225. package/src/wcs/components/consent-banner/consent-banner.js +248 -0
  226. package/src/wcs/components/consent-banner/icons/ccpa.svg +6 -0
  227. package/src/wcs/components/consent-banner/icons/info.svg +1 -0
  228. package/src/wcs/components/consent-banner/provider-onetrust.js +131 -0
  229. package/src/wcs/components/decision-tree/arrow-back.svg +3 -0
  230. package/src/wcs/components/decision-tree/badges.js +37 -0
  231. package/src/wcs/components/decision-tree/decision-tree.js +162 -0
  232. package/src/wcs/components/dynamic-contact-details/dynamic-contact-details.js +111 -0
  233. package/src/wcs/components/example-accordion/example-accordion.js +10 -0
  234. package/src/wcs/components/example-asset/example-asset.js +12 -0
  235. package/src/wcs/components/example-form/example-form.js +59 -0
  236. package/src/wcs/components/example-nested/example-nested.js +10 -0
  237. package/src/wcs/components/example-routing/example-routing.js +51 -0
  238. package/src/wcs/components/example-rtl/example-rtl.js +28 -0
  239. package/src/wcs/components/example-tabs/example-tabs.js +12 -0
  240. package/src/wcs/components/example-web-component/example-web-component.js +34 -0
  241. package/src/wcs/components/floating-button/floating-button.js +17 -0
  242. package/src/wcs/components/formstack-form/fields/address.js +38 -0
  243. package/src/wcs/components/formstack-form/fields/checkbox.js +42 -0
  244. package/src/wcs/components/formstack-form/fields/date.js +22 -0
  245. package/src/wcs/components/formstack-form/fields/description.js +8 -0
  246. package/src/wcs/components/formstack-form/fields/input.js +8 -0
  247. package/src/wcs/components/formstack-form/fields/name.js +39 -0
  248. package/src/wcs/components/formstack-form/fields/radio.js +24 -0
  249. package/src/wcs/components/formstack-form/fields/rating.js +53 -0
  250. package/src/wcs/components/formstack-form/fields/section.js +8 -0
  251. package/src/wcs/components/formstack-form/fields/select.js +10 -0
  252. package/src/wcs/components/formstack-form/fields/textarea.js +8 -0
  253. package/src/wcs/components/formstack-form/fields/wrapper.js +11 -0
  254. package/src/wcs/components/formstack-form/formstack-form.js +280 -0
  255. package/src/wcs/components/fuel-prices/fuel-prices.js +45 -0
  256. package/src/wcs/components/furniture-overview/furniture-overview.js +115 -0
  257. package/src/wcs/components/gauge-value/gauge-value.js +65 -0
  258. package/src/wcs/components/help-centre/api.js +150 -0
  259. package/src/wcs/components/help-centre/help-centre.js +400 -0
  260. package/src/wcs/components/help-centre/icon-search.svg +1 -0
  261. package/src/wcs/components/image-gen/admin-panel.js +248 -0
  262. package/src/wcs/components/image-gen/image-gen.js +385 -0
  263. package/src/wcs/components/image-gen/labels.js +37 -0
  264. package/src/wcs/components/image-gen/use-api.js +392 -0
  265. package/src/wcs/components/inspired-gallery/inspired-gallery.js +118 -0
  266. package/src/wcs/components/launch-container/launch-container.js +95 -0
  267. package/src/wcs/components/launch-container/ledger.js +140 -0
  268. package/src/wcs/components/lng-map/lng-map.js +44 -0
  269. package/src/wcs/components/mouseflow-analytics/mouseflow-analytics.js +39 -0
  270. package/src/wcs/components/msds-search/msds-search.js +127 -0
  271. package/src/wcs/components/msds-search/navigation-search.svg +3 -0
  272. package/src/wcs/components/product-catalogue/icon-back.svg +3 -0
  273. package/src/wcs/components/product-catalogue/icon-cart.svg +3 -0
  274. package/src/wcs/components/product-catalogue/icon-close.svg +3 -0
  275. package/src/wcs/components/product-catalogue/product-catalogue.js +215 -0
  276. package/src/wcs/components/product-links/icon-cart.svg +3 -0
  277. package/src/wcs/components/product-links/product-links.js +43 -0
  278. package/src/wcs/components/rio-iframe/rio-iframe.js +137 -0
  279. package/src/wcs/components/salsify-products/filter-tools.js +60 -0
  280. package/src/wcs/components/salsify-products/icon-cart.svg +3 -0
  281. package/src/wcs/components/salsify-products/process-products.js +53 -0
  282. package/src/wcs/components/salsify-products/route-tools.js +54 -0
  283. package/src/wcs/components/salsify-products/salsify-products.js +281 -0
  284. package/src/wcs/components/shout-out/shout-out.js +51 -0
  285. package/src/wcs/components/simple-chart/simple-chart.js +53 -0
  286. package/src/wcs/components/single-stat/single-stat.js +85 -0
  287. package/src/wcs/components/site-feedback/site-feedback.js +56 -0
  288. package/src/wcs/components/skds-search/navigation-search.svg +3 -0
  289. package/src/wcs/components/skds-search/skds-search.js +103 -0
  290. package/src/wcs/components/smart-banner/smart-banner.js +104 -0
  291. package/src/wcs/components/standalone-table/arrow-up-down.svg +3 -0
  292. package/src/wcs/components/standalone-table/arrow-up.svg +3 -0
  293. package/src/wcs/components/standalone-table/standalone-table.js +440 -0
  294. package/src/wcs/components/station-locator/station-locator.js +49 -0
  295. package/src/wcs/components/store-badges/badges.js +60 -0
  296. package/src/wcs/components/store-badges/store-badges.js +93 -0
  297. package/src/wcs/components/topbar-button/person.svg +1 -0
  298. package/src/wcs/components/topbar-button/topbar-button.js +22 -0
  299. package/src/wcs/components/universal-gallery/universal-gallery.js +308 -0
  300. package/src/wcs/components/zendesk-chat/zendesk-chat.js +133 -0
  301. package/src/wcs/shared/chat-bot/README.md +61 -0
  302. package/src/wcs/shared/chat-bot/chat-bot.js +216 -0
  303. package/src/wcs/shared/chat-bot/resources/arrow-next.svg +1 -0
  304. package/src/wcs/shared/chat-bot/resources/navigation-close.svg +1 -0
  305. package/src/wcs/shared/chat-bot/resources/person.svg +1 -0
  306. package/src/wcs/shared/chat-bot/resources/upload.svg +1 -0
  307. package/src/wcs/shared/filtered-data/README.md +52 -0
  308. package/src/wcs/shared/filtered-data/fetch-data.js +33 -0
  309. package/src/wcs/shared/filtered-data/filtered-data.js +337 -0
  310. package/src/wcs/shared/promo-with-popup/icon-close.svg +3 -0
  311. package/src/wcs/shared/promo-with-popup/icon-next.svg +3 -0
  312. package/src/wcs/shared/promo-with-popup/icon-prev.svg +3 -0
  313. package/src/wcs/shared/promo-with-popup/promo-with-popup.js +93 -0
@@ -0,0 +1,48 @@
1
+ import { Icon } from '../atoms/icon.js'
2
+ import { Asset } from '../molecules/asset.js'
3
+ import { PromoText } from '../molecules/promo-text.js'
4
+ import { ValidationError } from '../routing/error-handler.js'
5
+ import h from '../shared/h.js'
6
+
7
+ export function ListItem ({ _model, ...props }) {
8
+ // Title and either asset or icon are required.
9
+ const { title, asset, links } = _model
10
+ if (!title || !(asset.src || links[0]?.icon)) {
11
+ throw new ValidationError()
12
+ }
13
+
14
+ return h('div', {
15
+ ...props,
16
+ className: 'bg-bga text-txa rounded-2xl flex flex-row h-full',
17
+ children: [
18
+ h(AssetOrIcon, { _model }),
19
+ h(PromoText, {
20
+ _model: {
21
+ ..._model,
22
+ links: links.filter(l => l.name).map(l => ({ ...l, icon: null }))
23
+ },
24
+ _variant: 'full'
25
+ })
26
+ ]
27
+ })
28
+ }
29
+
30
+ function AssetOrIcon ({ _model }) {
31
+ const { links, asset } = _model
32
+ const hasAsset = asset.src
33
+ const icon = links[0]?.icon
34
+
35
+ if (hasAsset) {
36
+ return h('div', { className: 'w-1/3 grow-0 shrink-0 py-6 ps-6' },
37
+ h('div', { className: 'rounded-lg overflow-hidden' },
38
+ h(Asset, { _model, role: 'presentation' })
39
+ )
40
+ )
41
+ }
42
+
43
+ return h('div', { className: 'shrink-0 ps-6 py-6' },
44
+ h('div', { className: 'bg-bgb text-txb w-16 h-16 flex items-center justify-center rounded-full' },
45
+ h(Icon, icon)
46
+ )
47
+ )
48
+ }
@@ -0,0 +1,11 @@
1
+ import { Glossary } from '../molecules/glossary.js'
2
+ import h from '../shared/h.js'
3
+ import { isPublish } from '../shared/wcm-mode.js'
4
+
5
+ export function Metadata (props) {
6
+ if (isPublish) {
7
+ return null
8
+ }
9
+
10
+ return h(Glossary, { ...props, _color: 'system-solid' })
11
+ }
@@ -0,0 +1,232 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
2
+ import { Link } from '../atoms/link.js'
3
+ import { Logo } from '../atoms/logo.js'
4
+ import { Menu } from '../atoms/menu.js'
5
+ import { NavLink } from '../atoms/nav-link.js'
6
+ import { Tree } from '../molecules/tree.js'
7
+ import getId from '../shared/get-id.js'
8
+ import h from '../shared/h.js'
9
+ import { NAVIGATION_BURGER, NAVIGATION_CLOSE } from '../shared/icons.js'
10
+ import markSelection from '../shared/mark-selection.js'
11
+ import splitLinks from '../shared/split-links.js'
12
+ import t from '../shared/t.js'
13
+
14
+ //
15
+ // Main navigation.
16
+ //
17
+
18
+ export function Navigation ({ _model, ...props }) {
19
+ const [homeLink, mainLinks, toolLinks] = useMemo(() => getLinks(_model), [_model])
20
+ markSelection(mainLinks)
21
+
22
+ return h('div', {
23
+ ...props,
24
+ className: 'w-full relative border-txa/20 border-b text-txa bg-bga',
25
+ children: h(NavigationMain, { title: _model.title, homeLink, mainLinks, toolLinks })
26
+ })
27
+ }
28
+
29
+ function NavigationMain ({ title, homeLink, mainLinks, toolLinks }) {
30
+ // Make sure only one link is expanded at once.
31
+ const [expandedIndex, setExpandedIndex] = useState()
32
+ const [getIndexedId] = useIndexedIds()
33
+ const { isBurgerEmpty, closeBurger, ...burgerProps } = useBurger(mainLinks)
34
+
35
+ // Temporary fix to force close the menu on route change if the instance is the same.
36
+ useEffect(() => {
37
+ const onRoute = () => {
38
+ setExpandedIndex(null)
39
+ closeBurger()
40
+ }
41
+ globalThis.addEventListener('route', onRoute)
42
+ return () => globalThis.removeEventListener('route', onRoute)
43
+ }, [])
44
+
45
+ // Close on escape.
46
+ const mainLinksWrapperProps = {
47
+ className: 'flex',
48
+ role: 'list',
49
+ onKeyUp: event => event.key === 'Escape' && setExpandedIndex(null)
50
+ }
51
+
52
+ return h('nav', { className: 'max-w-page m-auto' },
53
+
54
+ // Main links.
55
+ h('div', { className: 'flex items-center' },
56
+ // Logo.
57
+ h(Link, {
58
+ 'aria-label': homeLink.value && homeLink.name,
59
+ _model: { value: homeLink.value },
60
+ children: h('div', { className: 'min-h-16 border-txa flex' },
61
+ h('div', { className: 'flex shrink-0 grow-0' },
62
+ h(Logo)
63
+ ),
64
+ title && h('div', { className: 'grow flex items-center tracking-tight font-bold max-w-52 pr-9 ' }, title)
65
+ )
66
+ }),
67
+
68
+ // Main links - close on escape.
69
+ mainLinks.length > 0 && h('div', mainLinksWrapperProps, mainLinks.map((link, index) => {
70
+ const isExpanded = expandedIndex === index
71
+ const hasMenu = Boolean(link.children)
72
+ const menuId = (hasMenu && getIndexedId(index)) || undefined
73
+
74
+ // Menu should be shown in the right variant when open,
75
+ // and when closed it is just an empty div (so that id exists).
76
+ let menu
77
+ if (isExpanded) {
78
+ menu = h(Menu, {
79
+ id: menuId,
80
+ children: h(Tree, {
81
+ _model: {
82
+ links: [{ name: t('Go to: {0}', link.name), value: link.value }, ...link.children ?? []]
83
+ }
84
+ })
85
+ })
86
+ } else {
87
+ // Empty div is necessary for the aria-controls reference.
88
+ menu = h('div', { id: menuId, className: 'hidden' })
89
+ }
90
+
91
+ return h('div', { className: 'shrink sm:hidden md:hidden tracking-tight relative', role: 'listitem' }, hasMenu
92
+ ? [
93
+ h(NavLink, {
94
+ _selection: 'bottom',
95
+ _size: 'lg',
96
+ _highlighted: isExpanded,
97
+ _expanded: false,
98
+ _model: { ...link },
99
+ role: 'button',
100
+ 'aria-expanded': isExpanded,
101
+ 'aria-controls': menuId,
102
+ ...getAriaCurrentPage(link),
103
+ onClick: event => {
104
+ event.preventDefault()
105
+ setExpandedIndex(expandedIndex === index ? null : index)
106
+ }
107
+ }),
108
+ // Show menu.
109
+ menu
110
+ ]
111
+ : h(NavLink, { _selection: 'bottom', _size: 'lg', _model: link, ...getAriaCurrentPage(link) })
112
+ )
113
+ })),
114
+
115
+ // Spacer.
116
+ h('div', { className: 'grow' }),
117
+
118
+ // Tool links.
119
+ toolLinks.length > 0 && h('div', { role: 'list', className: 'flex shrink' }, toolLinks.map(link =>
120
+ h('div', { role: 'listitem' },
121
+ h(NavLink, { _size: 'lg', _textless: true, _model: link })
122
+ )
123
+ )),
124
+
125
+ // Burger button (mobile and tablet).
126
+ !isBurgerEmpty && h('div', { className: 'lg:hidden' },
127
+ h(BurgerMenuButton, burgerProps)
128
+ )
129
+ ),
130
+
131
+ // Burger menu.
132
+ h(BurgerMenu, burgerProps)
133
+ )
134
+ }
135
+
136
+ // Helper indicating current page attribute.
137
+ function getAriaCurrentPage (link) {
138
+ return link.selected && globalThis.location.href.includes(link.value) ? { 'aria-current': 'page' } : {}
139
+ }
140
+
141
+ //
142
+ // Menu handling.
143
+ //
144
+
145
+ // Helper hook: index-based generated element ids.
146
+ function useIndexedIds () {
147
+ const indexedIds = useRef([])
148
+ const getIndexedId = (index = 0) => {
149
+ if (!indexedIds.current[index]) {
150
+ indexedIds.current[index] = getId()
151
+ }
152
+ return indexedIds.current[index]
153
+ }
154
+
155
+ return [getIndexedId, indexedIds.current]
156
+ }
157
+
158
+ //
159
+ // Burger menu.
160
+ //
161
+
162
+ function useBurger (mainLinks) {
163
+ const [isExpanded, setIsExpanded] = useState(false)
164
+ const ids = useRef({ ownerId: getId(), ownedId: getId() })
165
+ const isBurgerEmpty = useMemo(() => mainLinks.length === 0, [mainLinks])
166
+ const escapeHandler = event => {
167
+ if (event.key.startsWith('Esc')) {
168
+ setIsExpanded(false)
169
+ document.getElementById(ids.current.ownerId).focus()
170
+ }
171
+ }
172
+
173
+ return {
174
+ mainLinks,
175
+ isExpanded,
176
+ setIsExpanded,
177
+ ids,
178
+ isBurgerEmpty,
179
+ closeBurger: () => setIsExpanded(false),
180
+ escapeHandler
181
+ }
182
+ }
183
+
184
+ export function BurgerMenuButton ({ isExpanded, setIsExpanded, ids, escapeHandler }) {
185
+ return h(NavLink, {
186
+ onClick: event => {
187
+ setIsExpanded(!isExpanded)
188
+ event.preventDefault()
189
+ },
190
+ onKeyUp: escapeHandler,
191
+ id: ids.current.ownerId,
192
+ 'aria-controls': ids.current.ownedId,
193
+ 'aria-expanded': isExpanded.toString(),
194
+ 'aria-haspopup': true,
195
+ _highlighted: isExpanded,
196
+ _model: { name: 'Menu', icon: isExpanded ? NAVIGATION_CLOSE : NAVIGATION_BURGER },
197
+ _textless: true,
198
+ _size: 'lg'
199
+ })
200
+ }
201
+
202
+ function BurgerMenu ({ mainLinks, isExpanded, ids, escapeHandler }) {
203
+ const menuProps = {
204
+ className: 'animate-expand bg-bga lg:hidden border-txa/20 border-t',
205
+ id: ids.current.ownedId,
206
+ 'aria-labelledby': ids.current.ownerId,
207
+ onKeyUp: escapeHandler
208
+ }
209
+
210
+ return isExpanded && h('div', menuProps,
211
+ h(Tree, { _model: { links: mainLinks } })
212
+ )
213
+ }
214
+
215
+ //
216
+ // Helpers.
217
+ //
218
+
219
+ function getLinks (_model) {
220
+ const { links } = _model
221
+ const nestedLinks = splitLinks(links)
222
+
223
+ // Ensure length.
224
+ while (nestedLinks.length < 4) {
225
+ nestedLinks.push([])
226
+ }
227
+
228
+ const [homeLinks, mainLinks, toolLinks] = nestedLinks
229
+ const homeLink = homeLinks.pop() || {}
230
+
231
+ return [homeLink, mainLinks, toolLinks]
232
+ }
@@ -0,0 +1,87 @@
1
+ import { useLayoutEffect, useState } from 'preact/hooks'
2
+ import { Button } from '../atoms/button.js'
3
+ import { Heading } from '../atoms/heading.js'
4
+ import { Popup } from '../atoms/popup.js'
5
+ import { RichText } from '../atoms/rich-text.js'
6
+ import { Asset } from '../molecules/asset.js'
7
+ import { Links } from '../molecules/links.js'
8
+ import h from '../shared/h.js'
9
+ import { NAVIGATION_CLOSE } from '../shared/icons.js'
10
+ import t from '../shared/t.js'
11
+ import usePersistentState from '../shared/use-persistent-state.js'
12
+
13
+ const RETENTION = 8_640_000_000 // Expire after 100 days (the value is in milliseconds).
14
+ const STORAGE_KEY = 'suppressed_notifications'
15
+
16
+ export function Notification ({ _model, _variant, ...props }) {
17
+ const { title, text, asset } = _model
18
+ if (!title && !text) {
19
+ return null
20
+ }
21
+
22
+ const [notifications, setNotifications] = usePersistentState(STORAGE_KEY, {})
23
+ const [show, setShow] = useState(false)
24
+
25
+ useLayoutEffect(() => {
26
+ (async () => {
27
+ const currentHash = await getHash(text)
28
+ const updatedNotifications = { ...notifications }
29
+
30
+ // Clean up expired notifications.
31
+ for (const [hash, date] of Object.entries(notifications)) {
32
+ if (new Date(date).getTime() + RETENTION < Date.now()) {
33
+ delete updatedNotifications[hash]
34
+ }
35
+ }
36
+
37
+ if (!updatedNotifications[currentHash]) {
38
+ setShow(true)
39
+ updatedNotifications[currentHash] = new Date().toISOString()
40
+ }
41
+
42
+ setNotifications(() => updatedNotifications)
43
+ })().catch(console.error)
44
+ }, [text])
45
+
46
+ const _layout = _variant === 'center' ? 'center' : 'toast'
47
+ const _modal = _variant === 'center'
48
+
49
+ return h(Popup, { _show: show, _layout, _modal, 'aria-label': title, ...props },
50
+ h('div', { className: 'flex gap-2' },
51
+ h('div',
52
+ h(Button, {
53
+ _variant: 'plain',
54
+ _textless: true,
55
+ _size: 'xs',
56
+ _model: {
57
+ name: t('Dismiss'),
58
+ value: '#',
59
+ icon: NAVIGATION_CLOSE
60
+ },
61
+ onClick: event => {
62
+ event.preventDefault()
63
+ setShow(false)
64
+ }
65
+ })
66
+ ),
67
+ h('div', { className: 'grow order-first' },
68
+ h(Heading, { _level: '3' }, title)
69
+ )
70
+ ),
71
+ _layout === 'center' && asset.src && h('figure', { className: 'col-span-2 space-y-1' },
72
+ h('div', { className: 'rounded-lg overflow-hidden' },
73
+ h(Asset, { _model })
74
+ ),
75
+ asset.caption && h('figcaption', asset.caption)
76
+ ),
77
+ h(RichText, text),
78
+ h(Links, { _model })
79
+ )
80
+ }
81
+
82
+ async function getHash (str) {
83
+ // crypto.subtle is only available on HTTPS (or literal localhost).
84
+ const buf = await crypto?.subtle?.digest?.('SHA-256', new TextEncoder().encode(str))
85
+ const arr = Array.from(new Uint8Array(buf))
86
+ return arr.map(num => num.toString(16).padStart(2, '0')).join('')
87
+ }
@@ -0,0 +1,203 @@
1
+ import { Img } from '../atoms/img.js'
2
+ import { ValidationError } from '../routing/error-handler.js'
3
+ import h from '../shared/h.js'
4
+
5
+ const ORDER_STEPS = ['Placed', 'In progress', 'Packed', 'On the way', 'Delivered']
6
+
7
+ export function OrderTracker ({ _model, ...props }) {
8
+ const {
9
+ orderNumber,
10
+ status,
11
+ steps = ORDER_STEPS,
12
+ estimatedTime,
13
+ address,
14
+ paymentMode,
15
+ paymentStatus,
16
+ items = [],
17
+ summary = {},
18
+ links
19
+ } = _model
20
+
21
+ if (!orderNumber) {
22
+ throw new ValidationError()
23
+ }
24
+
25
+ const currentStepIndex = steps.findIndex(step => step.toLowerCase() === (status || '').toLowerCase())
26
+ const activeStep = currentStepIndex >= 0 ? currentStepIndex : 0
27
+
28
+ return h('div', {
29
+ ...props,
30
+ className: 'bg-bga text-txa w-full space-y-4'
31
+ },
32
+
33
+ // Order header
34
+ h('div', { className: 'bg-bga border border-txa/10 rounded-lg p-3 lg:p-4' },
35
+ h('div', { className: 'flex flex-wrap items-center gap-2 lg:gap-4' },
36
+ h('span', { className: 'text-xs lg:text-sm text-txa/60' }, 'Order No:'),
37
+ h('span', { className: 'text-xs lg:text-sm font-bold' }, orderNumber),
38
+ h('span', { className: 'text-xs lg:text-sm text-txa/60 sm:ml-2 lg:ml-4' }, 'Status:'),
39
+ h('span', { className: 'inline-flex items-center rounded-sm bg-bgb/10 text-bgb px-2 py-0.5 text-xs lg:text-sm font-semibold' }, status || 'Unknown'),
40
+ h('div', { className: 'flex-1 flex justify-end' },
41
+ h('button', {
42
+ type: 'button',
43
+ className: 'clickable border border-txa/30 rounded-md px-3 lg:px-5 py-1.5 lg:py-2 text-xs lg:text-sm font-semibold hover:bg-txa hover:text-bga transition',
44
+ 'aria-label': 'Track order'
45
+ }, 'Track Order')
46
+ )
47
+ ),
48
+
49
+ // Progress tracker
50
+ h('div', { className: 'flex items-start justify-between mt-4 lg:mt-6 px-1' },
51
+ steps.map((step, index) =>
52
+ h('div', { key: step, className: 'flex flex-col items-center flex-1 relative' },
53
+ // Connector line (before this step)
54
+ index > 0 && h('div', {
55
+ className: `absolute top-3 right-1/2 w-full h-px ${index <= activeStep ? 'border-t border-dashed border-bgb' : 'border-t border-solid border-txa/20'}`,
56
+ style: 'z-index: 0;'
57
+ }),
58
+ // Step circle
59
+ h('div', {
60
+ className: `relative z-10 flex items-center justify-center w-6 h-6 lg:w-7 lg:h-7 rounded-full ${index <= activeStep ? 'bg-bgb text-txb' : 'bg-txa/10 text-txa/40'}`
61
+ },
62
+ index <= activeStep
63
+ ? h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none' },
64
+ h('path', {
65
+ 'fill-rule': 'evenodd',
66
+ 'clip-rule': 'evenodd',
67
+ d: 'M6.575 12a.667.667 0 01-.486-.21L2.847 8.337a.666.666 0 11.972-.912l2.75 2.927 5.605-6.135a.667.667 0 01.985.899l-6.092 6.667A.665.665 0 016.58 12h-.005z',
68
+ fill: 'currentColor'
69
+ })
70
+ )
71
+ : h('span', { className: 'text-[10px] lg:text-xs font-bold' }, index + 1)
72
+ ),
73
+ // Step label
74
+ h('span', { className: `mt-1 text-[9px] lg:text-xs text-center leading-tight ${index <= activeStep ? 'font-semibold text-txa' : 'text-txa/50'}` }, step)
75
+ )
76
+ )
77
+ )
78
+ ),
79
+
80
+ // Order details grid
81
+ h('div', { className: 'bg-bga border border-txa/10 rounded-lg p-3 lg:p-4 grid sm:grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6' },
82
+
83
+ // Delivery address
84
+ address && h('div', { className: 'flex flex-col gap-1 lg:border-r lg:border-txa/10 lg:pr-4' },
85
+ h('span', { className: 'text-[10px] lg:text-xs font-semibold text-txa/60 uppercase' }, 'Delivery Address'),
86
+ address.name && h('span', { className: 'text-xs lg:text-sm font-bold' }, address.name),
87
+ address.line && h('span', { className: 'text-[10px] lg:text-xs text-txa/70 leading-relaxed' }, address.line)
88
+ ),
89
+
90
+ // Delivery time & payment
91
+ h('div', { className: 'flex flex-col gap-2' },
92
+ estimatedTime && h('div', { className: 'flex flex-col gap-0.5' },
93
+ h('span', { className: 'text-[10px] lg:text-xs font-semibold text-txa/60 uppercase' }, 'Estimated Delivery'),
94
+ h('div', { className: 'flex items-center gap-1' },
95
+ h('svg', { width: 14, height: 14, viewBox: '0 0 18 18', fill: 'none' },
96
+ h('path', {
97
+ 'fill-rule': 'evenodd',
98
+ 'clip-rule': 'evenodd',
99
+ d: 'M12 8.25H9.75V6C9.75 5.586 9.414 5.25 9 5.25C8.586 5.25 8.25 5.586 8.25 6V9C8.25 9.414 8.586 9.75 9 9.75H12C12.414 9.75 12.75 9.414 12.75 9C12.75 8.586 12.414 8.25 12 8.25ZM9 15C5.692 15 3 12.308 3 9C3 5.692 5.692 3 9 3C12.308 3 15 5.692 15 9C15 12.308 12.308 15 9 15ZM9 1.5C4.864 1.5 1.5 4.864 1.5 9C1.5 13.136 4.864 16.5 9 16.5C13.136 16.5 16.5 13.136 16.5 9C16.5 4.864 13.136 1.5 9 1.5Z',
100
+ fill: 'currentColor',
101
+ opacity: '0.6'
102
+ })
103
+ ),
104
+ h('span', { className: 'text-xs lg:text-sm font-semibold' }, estimatedTime)
105
+ )
106
+ ),
107
+ paymentStatus && h('div', { className: 'flex flex-col gap-0.5' },
108
+ h('span', { className: 'text-[10px] lg:text-xs font-semibold text-txa/60 uppercase' }, 'Payment Status'),
109
+ h('span', { className: 'text-xs lg:text-sm font-bold' }, paymentStatus)
110
+ ),
111
+ paymentMode && h('div', { className: 'flex flex-col gap-0.5' },
112
+ h('span', { className: 'text-[10px] lg:text-xs font-semibold text-txa/60 uppercase' }, 'Payment Mode'),
113
+ h('span', { className: 'text-xs lg:text-sm' }, paymentMode)
114
+ )
115
+ ),
116
+
117
+ // Order summary
118
+ summary.total != null && h('div', { className: 'bg-txa/5 rounded-md p-3 space-y-2' },
119
+ h('span', { className: 'text-xs lg:text-sm font-bold block' }, 'Order Summary'),
120
+ summary.subtotal != null && renderSummaryRow('Subtotal', formatPrice(summary.subtotal, summary.currency)),
121
+ summary.delivery != null && renderSummaryRow('Delivery', summary.delivery === 0 ? 'FREE' : formatPrice(summary.delivery, summary.currency), summary.delivery === 0),
122
+ summary.handling != null && renderSummaryRow('Handling', formatPrice(summary.handling, summary.currency)),
123
+ h('div', { className: 'border-t border-dashed border-txa/20 my-1' }),
124
+ renderSummaryRow('Total', formatPrice(summary.total, summary.currency), false, true),
125
+ summary.savings > 0 && h('div', { className: 'bg-bgb/10 rounded-sm px-2 py-1 flex justify-between items-center' },
126
+ h('span', { className: 'text-[10px] lg:text-xs font-semibold text-bgb' }, 'Total Savings'),
127
+ h('span', { className: 'text-[10px] lg:text-xs font-bold text-bgb' }, formatPrice(summary.savings, summary.currency))
128
+ )
129
+ )
130
+ ),
131
+
132
+ // Action buttons
133
+ h('div', { className: 'flex gap-2' },
134
+ links?.find(l => l.name === 'Pay now') && h('button', {
135
+ type: 'button',
136
+ className: 'clickable bg-bgb text-txb px-3 lg:px-4 py-1.5 lg:py-2 rounded-md text-xs lg:text-sm font-semibold',
137
+ 'aria-label': 'Pay now'
138
+ }, 'Pay now'),
139
+ links?.find(l => l.name === 'Cancel Order') && h('button', {
140
+ type: 'button',
141
+ className: 'clickable border border-txa/30 px-3 lg:px-4 py-1.5 lg:py-2 rounded-md text-xs lg:text-sm font-semibold hover:bg-txa hover:text-bga transition',
142
+ 'aria-label': 'Cancel order'
143
+ }, 'Cancel Order')
144
+ ),
145
+
146
+ // Items list
147
+ items.length > 0 && h('div', { className: 'bg-bga border border-txa/10 rounded-lg p-3 lg:p-4' },
148
+ h('div', { className: 'flex justify-between items-center pb-3 border-b border-txa/10' },
149
+ h('span', { className: 'text-sm lg:text-base font-bold' }, `Items purchased (${items.length})`),
150
+ links?.find(l => l.name === 'Shop from order') && h('button', {
151
+ type: 'button',
152
+ className: 'clickable bg-bgb text-txb px-3 lg:px-4 py-1.5 lg:py-2 rounded-md text-xs lg:text-sm font-semibold',
153
+ 'aria-label': 'Shop from this order'
154
+ }, 'Shop from this Order')
155
+ ),
156
+ h('ul', { className: 'divide-y divide-txa/10' },
157
+ items.map((item, index) =>
158
+ h('li', { key: index, className: 'flex items-center gap-3 lg:gap-4 py-3' },
159
+ // Item image
160
+ h('div', { className: 'shrink-0 w-14 h-14 lg:w-20 lg:h-20 rounded-md overflow-hidden bg-txa/5' },
161
+ item.image?.src
162
+ ? h(Img, {
163
+ _model: item.image,
164
+ _fit: 'contain',
165
+ className: 'w-full h-full object-contain'
166
+ })
167
+ : h('div', { className: 'w-full h-full bg-txa/5' })
168
+ ),
169
+ // Item info
170
+ h('div', { className: 'flex-1 min-w-0' },
171
+ h('span', { className: 'block text-xs lg:text-sm font-medium truncate' }, item.title)
172
+ ),
173
+ // Price & qty
174
+ h('div', { className: 'flex flex-col items-end shrink-0' },
175
+ h('div', { className: 'flex items-center gap-1 text-xs lg:text-sm' },
176
+ h('span', { className: 'font-semibold' }, formatPrice(item.price, summary.currency)),
177
+ h('span', { className: 'text-txa/40' }, '|'),
178
+ h('span', null, `Qty: ${item.quantity || 1}`)
179
+ ),
180
+ item.savings > 0 && h('span', { className: 'text-[10px] lg:text-xs text-bgb font-semibold mt-0.5' }, `Saved: ${formatPrice(item.savings, summary.currency)}`)
181
+ )
182
+ )
183
+ )
184
+ )
185
+ )
186
+ )
187
+ }
188
+
189
+ function renderSummaryRow (label, value, isFree = false, isBold = false) {
190
+ return h('div', { className: 'flex justify-between items-center' },
191
+ h('span', { className: `text-[10px] lg:text-xs ${isBold ? 'font-bold' : 'text-txa/70'}` }, label),
192
+ h('span', { className: `text-[10px] lg:text-xs ${isBold ? 'font-bold' : ''} ${isFree ? 'text-bgb font-semibold' : ''}` }, value)
193
+ )
194
+ }
195
+
196
+ function formatPrice (value, currency = '\u20b9') {
197
+ if (typeof value === 'number') {
198
+ return `${currency}${value % 1 === 0 ? value : value.toFixed(1)}`
199
+ }
200
+
201
+ if (typeof value === 'string') return value
202
+ return `${currency}0`
203
+ }
@@ -0,0 +1,26 @@
1
+ import { Asset } from '../molecules/asset.js'
2
+ import { PromoText } from '../molecules/promo-text.js'
3
+ import { ValidationError } from '../routing/error-handler.js'
4
+ import disableTransparency from '../shared/disable-transparency.js'
5
+ import h from '../shared/h.js'
6
+
7
+ export function PageHeaderBanner ({ _model, ...props }) {
8
+ const { title, asset, image, video } = _model
9
+ if (!title || !asset.src) {
10
+ throw new ValidationError()
11
+ }
12
+
13
+ return h('div', { ...props, className: `relative h-full overflow-hidden lg:grid lg:grid-cols-12 bg-bga text-txa gap-x-5 rounded-2xl ${disableTransparency(props._color)}` },
14
+ h('div', { className: 'row-start-1 col-start-1 col-span-12 lg:h-full' },
15
+ h(Asset, {
16
+ _model: { video, image: { ...image, caption: null } },
17
+ _tall: true
18
+ })
19
+ ),
20
+ h('div', { className: 'row-start-1 col-start-2 col-span-5 flex items-center lg:overflow-hidden' },
21
+ h('div', { className: 'rounded-xl bg-bga text-txa relative' },
22
+ h(PromoText, { _model: { ..._model, date: null }, _level: 1 })
23
+ )
24
+ )
25
+ )
26
+ }
@@ -0,0 +1,33 @@
1
+ import { Box } from '../atoms/box.js'
2
+ import { Heading } from '../atoms/heading.js'
3
+ import { RichText } from '../atoms/rich-text.js'
4
+ import { Time } from '../atoms/time.js'
5
+ import { Asset } from '../molecules/asset.js'
6
+ import { ValidationError } from '../routing/error-handler.js'
7
+ import h from '../shared/h.js'
8
+
9
+ export function PageHeader ({ _model, _variant, ...props }) {
10
+ const { asset, image, title, text, date } = _model
11
+ if (!title) {
12
+ throw new ValidationError()
13
+ }
14
+
15
+ const hasAsset = asset.src
16
+
17
+ return h('div', { ...props, className: 'relative lg:grid lg:grid-cols-12 lg:gap-x-5 w-full md:rounded-2xl sm:rounded-2xl overflow-hidden' },
18
+ hasAsset && h('div', { className: 'lg:col-span-12 overflow-hidden lg:rounded-2xl', style: 'aspect-ratio: 4 / 1' },
19
+ h(Asset, {
20
+ _model: { ..._model, image: { ...image, caption: null } },
21
+ _tall: true,
22
+ })
23
+ ),
24
+ // The z-0 is required for the text to appear on top of a <video>.
25
+ h(Box, { className: `bg-bga text-txa z-0 lg:rounded-tr-2xl lg:rounded-bl-2xl lg:rounded-br-2xl lg:col-start-1 lg:col-span-8 ${hasAsset ? 'lg:-mt-16' : 'lg:rounded-tl-2xl'}` },
26
+ h(Heading, { _level: 1 }, title),
27
+ date && _variant === 'date' && h(Time, { _time: date }),
28
+ text && h('div', { className: 'text-2xl' },
29
+ h(RichText, text)
30
+ )
31
+ )
32
+ )
33
+ }
@@ -0,0 +1,14 @@
1
+ import { Box } from '../atoms/box.js'
2
+ import { Tags } from '../molecules/tags.js'
3
+ import { ValidationError } from '../routing/error-handler.js'
4
+ import h from '../shared/h.js'
5
+
6
+ export function PageTags ({ _model, ...props }) {
7
+ if (_model.tags.length < 1) {
8
+ throw new ValidationError()
9
+ }
10
+
11
+ return h(Box, { ...props, className: 'rounded-2xl bg-bga text-txa' },
12
+ h(Tags, { _model })
13
+ )
14
+ }