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,216 @@
1
+ import ICON_ARROW from './resources/arrow-next.svg'
2
+ import ICON_CLOSE from './resources/navigation-close.svg'
3
+ import ICON_PERSON from './resources/person.svg'
4
+ import ICON_UPLOAD from './resources/upload.svg'
5
+
6
+ const { h, matter, preactHooks } = globalThis.ami
7
+ const { useEffect, useLayoutEffect, useRef, useState } = preactHooks
8
+ const { Button, Icon, Input } = matter
9
+
10
+ const IS_PUBLISH = document.documentElement.dataset.mode === 'publish'
11
+
12
+ export default function ChatBot ({
13
+ closeText = 'Close window',
14
+ buttonText = 'Ask us',
15
+ headerText = 'Chatbot',
16
+ sendText = 'Send',
17
+ attachText = 'Attach',
18
+ inputText = 'Type your message',
19
+ icon,
20
+ messages,
21
+ forceOpen,
22
+ isBusy,
23
+ onInit,
24
+ onSend,
25
+ onAttach,
26
+ onClose
27
+ }) {
28
+ // Show only on publish.
29
+ if (!IS_PUBLISH) {
30
+ return null
31
+ }
32
+
33
+ const [isVisible, setVisible] = useState(forceOpen)
34
+ const [isInitialized, setInitialized] = useState(false)
35
+ const [prevBusy, setPrevBusy] = useState(isBusy)
36
+ const ref = useRef()
37
+
38
+ // Trigger close on before unload.
39
+ useEffect(() => {
40
+ window.addEventListener('beforeunload', onClose)
41
+ return () => window.removeEventListener('beforeunload', onClose)
42
+ }, [])
43
+
44
+ // Set focus when recovering from busy state.
45
+ useLayoutEffect(() => {
46
+ if (!isBusy && prevBusy) {
47
+ ref.current?.base?.focus?.()
48
+ }
49
+
50
+ if (isBusy !== prevBusy) {
51
+ setPrevBusy(isBusy)
52
+ }
53
+ }, [isBusy, prevBusy])
54
+
55
+ // Trigger onInit event.
56
+ if (isVisible && !isInitialized) {
57
+ onInit?.()
58
+ setInitialized(true)
59
+ }
60
+
61
+ // Chat button
62
+ if (!isVisible) {
63
+ return h('div', { className: 'fixed end-20 bottom-6 z-20 cursor-pointer shadow rounded-xl animate-fade print:hidden' },
64
+ h(Button, {
65
+ _size: 'sm',
66
+ _rtlFlip: true,
67
+ _model: { name: buttonText, icon },
68
+ onClick () {
69
+ setVisible(true)
70
+ }
71
+ })
72
+ )
73
+ }
74
+
75
+ // Chat window.
76
+ return [
77
+ h('div', {
78
+ // Z-index must be below 30 (popups) and above 20 (floating elements).
79
+ _style: id => `
80
+ ${id} { z-index: 21 }
81
+ @media screen and (min-width: 960px) {
82
+ ${id} { width: 500px; height: 700px; max-height: calc(100vh - 48px) }
83
+ }
84
+ `,
85
+ className: 'shadow-lg fixed end-20 bottom-6 bg-bga text-txa border-txa/20 flex flex-col animate-toast lg:rounded-xl sm:mr-0 md:mr-0 sm:inset-0 md:inset-0',
86
+ children: [
87
+ h('div', { className: 'text-txb bg-bgb flex lg:rounded-tl-xl lg:rounded-tr-xl' },
88
+ h('div', { className: 'flex grow font-bold justify-center items-center ms-10' },
89
+ headerText
90
+ ),
91
+ h('button', {
92
+ className: 'p-2 grow-0 cursor-pointer',
93
+ children: h(Icon, ICON_CLOSE),
94
+ title: closeText,
95
+ onClick () {
96
+ setVisible(false)
97
+ }
98
+ })
99
+ ),
100
+ h(Messages, { messages }),
101
+ h('form', {
102
+ className: 'p-2 flex space-x-2',
103
+ children: [
104
+ onAttach && h('div', { className: 'shrink-0' },
105
+ AttachButton({ attachText, onAttach, isBusy })
106
+ ),
107
+ h(Input, {
108
+ ref,
109
+ disabled: isBusy,
110
+ placeholder: inputText,
111
+ className: 'grow',
112
+ }),
113
+ h('div', { className: 'shrink-0' },
114
+ h(Button, {
115
+ disabled: isBusy,
116
+ type: 'submit',
117
+ onClick (event) {
118
+ event.preventDefault()
119
+ const input = event.currentTarget.parentElement.previousElementSibling
120
+ const text = input.value.trim()
121
+ if (text) {
122
+ onSend?.(text)
123
+ input.value = ''
124
+ }
125
+ },
126
+ _textless: true,
127
+ _model: { name: sendText, icon: ICON_ARROW }
128
+ })
129
+ )
130
+ ]
131
+ })
132
+ ]
133
+ })
134
+ ]
135
+ }
136
+
137
+ function Messages ({ messages }) {
138
+ const ref = useRef(null)
139
+
140
+ // Scroll to the last message.
141
+ // ResizeObserver is necessary due to the unknow size of images.
142
+ useEffect(() => {
143
+ const observer = new globalThis.ResizeObserver(entries => {
144
+ for (const entry of entries) {
145
+ const parent = entry.target.parentElement
146
+ parent.scrollTop = parent.scrollHeight
147
+ }
148
+ })
149
+
150
+ observer.observe(ref.current)
151
+ }, [])
152
+
153
+ // Render messages.
154
+ const locale = document.documentElement.lang || navigator.language
155
+
156
+ return h('div', { role: 'log', className: 'grow overflow-y-auto' },
157
+ h('div', { ref, className: 'space-y-2 pl-2 pr-2 pt-2' },
158
+ messages.map(m =>
159
+ h('div',
160
+ // Timestamp.
161
+ m.timestamp && h('time', {
162
+ className: m.isBot ? 'ml-8 flex text-sm' : 'flex justify-end text-sm',
163
+ children: m.timestamp.toLocaleString(locale),
164
+ }),
165
+
166
+ // Message.
167
+ h('div', { className: m.isBot ? 'flex' : 'flex justify-end' },
168
+ // Person icon or space.
169
+ h('div', { className: 'flex items-end w-8 shrink-0 grow-0' },
170
+ m.isBot && h(Icon, ICON_PERSON)
171
+ ),
172
+
173
+ // Bubble.
174
+ h('div', {
175
+ style: 'max-width: calc(100% - 32px); overflow-wrap: break-word',
176
+ className: m.isBot
177
+ ? 'rounded-tl-lg rounded-tr-lg rounded-br-lg inline-block p-2 border-txa/20 border space-y-2'
178
+ : 'rounded-tl-lg rounded-tr-lg rounded-bl-lg inline-block p-2 text-txb bg-bgb',
179
+ children: m.children
180
+ })
181
+ )
182
+ )
183
+ )
184
+ )
185
+ )
186
+ }
187
+
188
+ function AttachButton ({ attachText, onAttach, isBusy }) {
189
+ const ref = useRef(null)
190
+
191
+ useEffect(() => {
192
+ const onChange = event => {
193
+ const file = event.target.files[0]
194
+ file && onAttach(file)
195
+ event.target.value = '' // Clear after use. Otherwise reattaching the same file won't work.
196
+ }
197
+ ref.current?.addEventListener?.('change', onChange)
198
+
199
+ return () => {
200
+ ref.current?.removeEventListener?.('change', onChange)
201
+ }
202
+ }, [])
203
+
204
+ return [
205
+ h('input', { type: 'file', className: 'hidden', ref }),
206
+ h(Button, {
207
+ disabled: isBusy,
208
+ _textless: true,
209
+ _model: { name: attachText, icon: ICON_UPLOAD },
210
+ type: 'button',
211
+ onClick () {
212
+ ref?.current?.click?.()
213
+ }
214
+ })
215
+ ]
216
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><polygon fill="#595959" points="7.374 4.324 15.219 12.952 7.407 20.773 8.822 22.187 17.981 13.017 8.854 2.979"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><polygon fill="#595959" points="19.293 3.293 20.707 4.707 13.414 12 20.707 19.293 19.293 20.707 12 13.414 4.707 20.707 3.293 19.293 10.586 12 3.293 4.707 4.707 3.293 12 10.586"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 12C14.2133 12 16 10.2133 16 8C16 5.78667 14.2133 4 12 4C9.78667 4 8 5.78667 8 8C8 10.2133 9.78667 12 12 12ZM12 14C9.33333 14 4 15.6098 4 18.5366V19.5C4 19.7761 4.22386 20 4.5 20H19.5C19.7761 20 20 19.7761 20 19.5V18.5366C20 15.6098 14.6667 14 12 14Z" fill="#343434"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#404040"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.97 9.344a.5.5 0 0 1-.334.456l-.967.341A2.502 2.502 0 0 0 4.5 15H9v2H4.5a4.5 4.5 0 0 1-1.656-8.685.265.265 0 0 0 .167-.232 7.5 7.5 0 0 1 14.07-3.182c.052.093.154.146.26.135A6 6 0 1 1 18 17h-3.001v-2h3a4 4 0 1 0-.56-7.961l-1.05.146a.5.5 0 0 1-.513-.266l-.488-.942A5.5 5.5 0 0 0 5.003 8.319L4.97 9.344Zm7.384-2.198a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708l.708.707a.5.5 0 0 0 .707 0L11 10.62v10.086a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5V10.621l1.94 1.94a.5.5 0 0 0 .706 0l.708-.707a.5.5 0 0 0 0-.708l-4-4Z"/></svg>
@@ -0,0 +1,52 @@
1
+ # FilteredData
2
+
3
+ ## Overview
4
+
5
+ - A generic library for rendering a filtered list of items (not a standalone web component).
6
+ - Currently used by several web components, e.g. Furniture overview, Inspired gallery, Product catalogue, MSDS Search.
7
+
8
+ ## Arguments
9
+
10
+ - `getData (props)` - Asynchronous function responsible for loading data, receives all additional props passed to the main component. The data should be returned as a list of objects. Keys should be proper names, translated if necessary (e.g. Categorie, Titel). This is important as the data will be used for the filters. Example:
11
+
12
+ ```js
13
+ ;[
14
+ {
15
+ Category: 'Foo',
16
+ Title: 'Bar',
17
+ Text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Eademne, quae restincta siti',
18
+ 'Last Updated': '2024-07-23T11:14:12.001Z',
19
+ 'Multiple values': ['Foo', 'Bar']
20
+ },
21
+ {
22
+ // ...
23
+ }
24
+ ]
25
+ ```
26
+
27
+ - `filters` - A list of filters (or a function to return them, receiving `items`). Each filter is an object with three mandatory keys: `type`, `label` and `keys`:
28
+
29
+ ```js
30
+ ;[
31
+ {
32
+ type: 'text', // One of 'text', 'select' or 'order'.
33
+ label: 'Search', // Display name, defaults to first existing field name from the "keys" entry.
34
+ keys: ['Title', 'Text', 'Category'] // Data fields to include in the filter. Non-existing data field (including empty string) creates a placeholder label for dropdowns. The last entry is used for hash navigation, ensure it is unique among all keys.
35
+ }
36
+ ]
37
+ ```
38
+
39
+ - `Items ({ items })` - Preact component to render the item list.
40
+ - `i18n` - An object for translations with the following structure (set to `null` to disable particular feature):
41
+
42
+ ```js
43
+ {
44
+ loading: 'Loading...',
45
+ loadingError: 'Failed to load data.',
46
+ loadMore: 'Load more',
47
+ resetFilters: 'Reset filters',
48
+ ascending: 'ascending',
49
+ descending: 'descending',
50
+ noResults: 'No results found.'
51
+ }
52
+ ```
@@ -0,0 +1,33 @@
1
+ const CSV_PARSER = /(,|\r?\n|\r|^|^)(?:"([^"]*(?:""[^"]*)*)"|([^,\r\n]*))/gi
2
+
3
+ // CSV fetch and parse.
4
+ export async function fetchCsv (src) {
5
+ const res = await fetch(src)
6
+ if (!res.ok) {
7
+ throw new Error(`Failed to load ${src} (${res.status})`)
8
+ }
9
+ const text = await res.text()
10
+ return parseCsv(text || '')
11
+ }
12
+
13
+ // Parse CSV string.
14
+ function parseCsv (csv) {
15
+ const trimmedCsv = csv.trim()
16
+ const regex = CSV_PARSER
17
+ const lines = [[]]
18
+ let match
19
+ while ((match = regex.exec(trimmedCsv))) {
20
+ const [separator, quoted, unquoted] = match.slice(1)
21
+ if (separator.length && separator !== ',') {
22
+ lines.push([])
23
+ }
24
+
25
+ lines.at(-1).push(quoted ? quoted.replaceAll('""', '"') : unquoted)
26
+ }
27
+
28
+ const headers = lines[0].map(header => header.trim())
29
+ return lines.slice(1).map((line, lineIndex) => line.reduce((obj, value, index) => {
30
+ obj[headers[index]] = value?.trim?.() || value
31
+ return obj
32
+ }, { key: lineIndex }))
33
+ }
@@ -0,0 +1,337 @@
1
+ const { h, matter, preactHooks } = globalThis.ami
2
+ const { useEffect, useMemo, useRef, useState } = preactHooks
3
+ const { Button, Input, Link, Box } = matter
4
+
5
+ const I18N_DEFAULT = {
6
+ loading: 'Loading...',
7
+ loadingError: 'Failed to load data.',
8
+ resetFilters: 'Reset filters',
9
+ loadMore: 'Load more',
10
+ ascending: 'ascending',
11
+ descending: 'descending',
12
+ noResults: 'No results found.'
13
+ }
14
+ const MAX_FILTERS_SINGLE_ROW = 3
15
+ const LOAD_MORE_LIMIT = 24
16
+
17
+ // Special filter keys.
18
+ const FILTER_KEY_TEXT = 'text'
19
+ const FILTER_KEY_SELECT = 'select'
20
+ const FILTER_KEY_ORDER = 'order'
21
+
22
+ const TEXT_CENTER = 'text-center'
23
+
24
+ // -----------------------------------------------------------------------------
25
+ // Subcomponents
26
+ // -----------------------------------------------------------------------------
27
+
28
+ function FilterBar ({ i18n, filters, resetLoadMore }) {
29
+ const layout = useMemo(() => filters && getFilterLayout(filters), [filters?.length])
30
+
31
+ if (!filters?.length) {
32
+ return null
33
+ }
34
+
35
+ return h(Box, { className: 'text-txa bg-bga rounded-2xl' },
36
+ h('div', { className: `grid gap-5 grid-cols-${layout.colNumber} md:grid-cols-2 sm:grid-cols-1` }, filters?.map?.(filter => {
37
+ // Determine if text filter should span all columns.
38
+ const colspan = ((filter.type === FILTER_KEY_TEXT) && layout.fullWidthText) && layout.colNumber
39
+ const onChange = e => {
40
+ const filterValueMap = filters
41
+ .map(f => ({ [f.name]: f.value }))
42
+ .reduce((a, b) => ({ ...a, ...b }), {})
43
+ setHash({ ...filterValueMap, [filter.name]: e.target.value })
44
+ filter.type !== FILTER_KEY_ORDER && resetLoadMore()
45
+ }
46
+
47
+ return h(FilterInput, { ...filter, colspan, onChange })
48
+ })),
49
+ // Reset filters.
50
+ i18n.resetFilters && h(Link, {
51
+ _variant: 'underline',
52
+ _model: { name: i18n.resetFilters, value: '#' },
53
+ onClick: e => {
54
+ e.preventDefault()
55
+ if (!globalThis.location.hash) {
56
+ return
57
+ }
58
+ resetHash()
59
+ resetLoadMore()
60
+ }
61
+ })
62
+ )
63
+ }
64
+
65
+ function FilterInput ({ colspan, label, type, name, values, value, defaultValue, onChange }) {
66
+ const labelClass = colspan ? `lg:col-span-${colspan}` : ''
67
+ return h('label', { className: `block ${labelClass}` },
68
+ h('span', { className: 'font-bold' }, label),
69
+ type === FILTER_KEY_TEXT
70
+ ? h(Input, {
71
+ _size: 'sm',
72
+ name,
73
+ type: 'search',
74
+ value,
75
+ onInput: onChange
76
+ })
77
+ : h(Input, {
78
+ _size: 'sm',
79
+ name,
80
+ type: 'select',
81
+ onChange,
82
+ children: values.map(({ name: optionName, value: optionValue }) =>
83
+ h('option', {
84
+ value: optionValue,
85
+ selected: (value === optionValue) || (!value && optionValue === defaultValue)
86
+ }, optionName)
87
+ )
88
+ })
89
+ )
90
+ }
91
+
92
+ // -----------------------------------------------------------------------------
93
+ // Helper functions
94
+ // -----------------------------------------------------------------------------
95
+
96
+ // Determine filter layout according to its count/type.
97
+ function getFilterLayout (filters) {
98
+ const filterNumber = filters.length
99
+ // Indicates the first field is text and is the only one of this type.
100
+ const isFirstFilterText = filters.every((filter, i) => {
101
+ const isText = filter?.type === FILTER_KEY_TEXT
102
+ return i === 0 ? isText : !isText
103
+ })
104
+
105
+ if (filterNumber <= MAX_FILTERS_SINGLE_ROW) {
106
+ return {
107
+ // For a single filter, it spans all columns only if it is of type text.
108
+ colNumber: filterNumber + Number((filterNumber === 1) && !isFirstFilterText),
109
+ fullWidthText: false
110
+ }
111
+ }
112
+
113
+ const layouts = [
114
+ { colNumber: 3, fullWidthText: false },
115
+ { colNumber: 2, fullWidthText: false },
116
+ isFirstFilterText && { colNumber: 3, fullWidthText: true },
117
+ isFirstFilterText && { colNumber: 2, fullWidthText: true }
118
+ ].filter(Boolean)
119
+ let last
120
+
121
+ for (const currentLayout of layouts) {
122
+ const currentFilterNumber = filterNumber - Number(currentLayout.fullWidthText)
123
+ const orphanNumber = currentFilterNumber % currentLayout.colNumber
124
+ if (!last || (orphanNumber < last.orphanNumber)) {
125
+ last = { orphanNumber, layout: currentLayout }
126
+ if (orphanNumber === 0) {
127
+ break
128
+ }
129
+ }
130
+ }
131
+
132
+ return last.layout
133
+ }
134
+
135
+ // Applies properties according to filter type.
136
+ function decorateFilterItem (filterItem, allItems, i18n) {
137
+ const filterHandlers = {
138
+ [FILTER_KEY_TEXT]: {
139
+ onFilter: (filterItems, filterValue) =>
140
+ filterValue
141
+ ? filterItems.filter(item => filterItem.keys.some(key =>
142
+ `${item[key]}`.toLowerCase().includes(filterValue.toLowerCase())
143
+ ))
144
+ : filterItems
145
+ },
146
+ [FILTER_KEY_SELECT]: {
147
+ name: filterItem.keys.at(-1)
148
+ ?.toLowerCase().replaceAll(/[^A-Za-z0-9]/g, '') || FILTER_KEY_SELECT,
149
+ getValues: () => [
150
+ allItems[0] && (allItems[0][filterItem.keys[0]] === undefined) && { name: filterItem.keys[0], value: '' },
151
+ ...filterItem.keys.flatMap(key =>
152
+ [...new Set(allItems.flatMap(item => (item[key] || '')).filter(Boolean))]
153
+ ).sort((v1, v2) => v1.localeCompare(v2)).map(value => ({ name: value, value }))
154
+ ].filter(Boolean),
155
+
156
+ onFilter: (items, filterValue) =>
157
+ filterValue
158
+ ? items.filter(item => filterItem.keys.some(key =>
159
+ // Check if the value matches (single) or one of them (array).
160
+ (item[key] === filterValue) || (Array.isArray(item[key]) && item[key].includes(filterValue)))
161
+ )
162
+ : items
163
+ },
164
+ [FILTER_KEY_ORDER]: {
165
+ getValues: () => filterItem.keys.map(key => {
166
+ const isDesc = key.startsWith('-')
167
+ const rawKey = isDesc ? key.substring(1) : key
168
+
169
+ // Determine if order direction (asc/desc) is needed
170
+ const hasOpposite = filterItem.keys.includes(`${isDesc ? '' : '-'}${rawKey}`)
171
+ let directionSuffix = ''
172
+ if (hasOpposite) {
173
+ directionSuffix = ` (${isDesc ? i18n.descending : i18n.ascending})`
174
+ }
175
+
176
+ return {
177
+ name: `${rawKey}${directionSuffix}`,
178
+ value: key
179
+ }
180
+ }),
181
+
182
+ onFilter: (items, filterValue, sign) => items.sort((a, b) =>
183
+ (typeof a[filterValue] === 'number'
184
+ ? (a[filterValue] - b[filterValue])
185
+ : `${a[filterValue]}`.localeCompare?.(`${b[filterValue]}`)) * sign
186
+ )
187
+ }
188
+ }
189
+
190
+ const name = filterHandlers[filterItem.type]?.name || filterItem.type
191
+ const values = filterHandlers[filterItem.type]?.getValues?.() || []
192
+ const onFilter = filterHandlers[filterItem.type]?.onFilter
193
+
194
+ return {
195
+ ...filterItem,
196
+ label: filterItem.label || (allItems[0] && filterItem.keys.find(key => allItems[0][key] !== undefined)),
197
+ name,
198
+ values,
199
+ value: '',
200
+ defaultValue: values[0]?.value,
201
+ onFilter
202
+ }
203
+ }
204
+
205
+ // Filter items according to current filter values and load more cursor position.
206
+ function applyFiltersLoadMore ({ filters, items, i18n, loadMoreCursor }) {
207
+ // Apply filters.
208
+ let filteredItems = [...items]
209
+ for (const { type, onFilter, value, defaultValue } of filters) {
210
+ let filterValue = value || defaultValue || ''
211
+ if (type === FILTER_KEY_ORDER) {
212
+ const sign = filterValue.startsWith?.('-') ? -1 : 1
213
+ filterValue = filterValue.replace(/^-/, '')
214
+ filteredItems = onFilter(filteredItems, filterValue, sign)
215
+ } else if (onFilter) {
216
+ filteredItems = onFilter(filteredItems, filterValue)
217
+ }
218
+ }
219
+
220
+ // Apply load more.
221
+ let hasLoadMore = false
222
+ if (i18n.loadMore) {
223
+ const filteredItemsLength = filteredItems.length
224
+ filteredItems = filteredItems.slice(0, loadMoreCursor + LOAD_MORE_LIMIT)
225
+ hasLoadMore = (filteredItems.length < filteredItemsLength)
226
+ }
227
+
228
+ return [filteredItems, hasLoadMore]
229
+ }
230
+
231
+ function setHash (params) {
232
+ const search = new URLSearchParams()
233
+ for (const [key, value] of Object.entries(params)) { value && search.set(key, value) }
234
+ if (search.size > 0) {
235
+ globalThis.history.replaceState({}, '', `#${search.toString()}`)
236
+ globalThis.dispatchEvent(new globalThis.HashChangeEvent('hashchange'))
237
+ } else {
238
+ resetHash()
239
+ }
240
+ }
241
+
242
+ function resetHash () {
243
+ // Avoid scroll to top.
244
+ globalThis.history.replaceState({}, '', globalThis.location.href.split('#')[0])
245
+ globalThis.dispatchEvent(new globalThis.HashChangeEvent('hashchange'))
246
+ }
247
+
248
+ function getFiltersFromHash (filters) {
249
+ const search = new URLSearchParams(globalThis.location.hash.substring(1))
250
+ return filters?.map?.(filterItem => ({ ...filterItem, value: search.get(filterItem.name) || '' }))
251
+ }
252
+
253
+ // -----------------------------------------------------------------------------
254
+ // Main component
255
+ // -----------------------------------------------------------------------------
256
+
257
+ export default function FilteredData ({ getData, filters: initialFilters, Items, i18n, ...props }) {
258
+ const [data, setData] = useState({
259
+ errorMessage: null,
260
+ i18n: { ...I18N_DEFAULT, ...i18n },
261
+ items: [],
262
+ filters: null,
263
+ loadMoreCursor: 0
264
+ })
265
+ const itemsRef = useRef(null)
266
+
267
+ // Resolve data.
268
+ useEffect(() => {
269
+ (async () => {
270
+ let items
271
+ try {
272
+ items = await getData?.(props) || []
273
+ } catch (error) {
274
+ setData(currentData => ({ ...currentData, errorMessage: data.i18n.loadingError }))
275
+ console.error(error)
276
+ return
277
+ }
278
+ const filters = getFiltersFromHash( // Set initial filter values.
279
+ initialFilters?.map?.(filter => decorateFilterItem(filter, items, data.i18n)) // Apply additional properties.
280
+ ) || []
281
+
282
+ setData({ ...data, items, filters })
283
+
284
+ // Listen to hash changes. Multiple functions to satisfy SonarQube.
285
+ const setDataHandler = currentData => ({
286
+ ...currentData,
287
+ filters: getFiltersFromHash(currentData.filters)
288
+ })
289
+ const hashchangeHandler = () => setData(setDataHandler)
290
+ globalThis.addEventListener('hashchange', hashchangeHandler)
291
+ })()
292
+ }, [])
293
+
294
+ // Focus on the first newly loaded element after load more click.
295
+ useEffect(() => {
296
+ if (data.loadMoreCursor > 0) {
297
+ // Determine item container.
298
+ const container = [...itemsRef?.current?.querySelectorAll('[tabindex="0"]') || []]?.find(element => element.parentNode.children.length >= data.loadMoreCursor)?.parentNode
299
+ container?.children?.[data.loadMoreCursor]?.focus?.()
300
+ }
301
+ }, [data.loadMoreCursor])
302
+
303
+ // Error.
304
+ if (data.errorMessage) {
305
+ return h('div', { className: TEXT_CENTER }, data.errorMessage)
306
+ }
307
+
308
+ // Not ready yet or misconfigured.
309
+ if (!data.filters) {
310
+ return Boolean(data.i18n.loading) && h(Box, { className: TEXT_CENTER }, data.i18n.loading)
311
+ }
312
+
313
+ // Apply filters & load more.
314
+ const [filteredItems, hasLoadMore] = applyFiltersLoadMore(data)
315
+ // Reset load more cursor.
316
+ const resetLoadMore = () => data.i18n.loadMore && setData(currentData => ({ ...currentData, loadMoreCursor: 0 }))
317
+
318
+ return h('div', { className: 'space-y-5' },
319
+ // Filters.
320
+ h(FilterBar, { ...data, resetLoadMore }),
321
+ // Item list.
322
+ Items && ((filteredItems.length > 0) || !data.i18n.noResults) && h('div', { ref: itemsRef },
323
+ h(Items, { items: filteredItems })
324
+ ),
325
+ data.i18n.noResults && !filteredItems.length && h('div', { className: TEXT_CENTER }, data.i18n.noResults),
326
+ // Load more.
327
+ hasLoadMore && h('div', { className: 'flex justify-center' },
328
+ h(Button, {
329
+ className: 'w-auto',
330
+ onClick: () => { setData({ ...data, loadMoreCursor: data.loadMoreCursor + LOAD_MORE_LIMIT }) },
331
+ _model: {
332
+ name: data.i18n.loadMore
333
+ }
334
+ })
335
+ )
336
+ )
337
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <polygon fill="#595959" points="19.293 3.293 20.707 4.707 13.414 12 20.707 19.293 19.293 20.707 12 13.414 4.707 20.707 3.293 19.293 10.586 12 3.293 4.707 4.707 3.293 12 10.586"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <polygon fill="#595959" points="7.374 4.324 15.219 12.952 7.407 20.773 8.822 22.187 17.981 13.017 8.854 2.979"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <polygon fill="#595959" points="15.543 4.294 7.699 12.922 15.51 20.743 14.095 22.156 4.937 12.987 14.063 2.949"/>
3
+ </svg>