@salla.sa/twilight-components 3.0.24 → 3.0.25

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 (370) hide show
  1. package/dist/cjs/{Helper-sq6ztayx.js → Helper-BSFw7uVh.js} +1 -1
  2. package/dist/cjs/{filepond-BMxlEH8M.js → filepond-DzJcpvBM.js} +1 -1
  3. package/dist/cjs/{filepond-plugin-file-poster-DVSz4StJ.js → filepond-plugin-file-poster-BILkbyw_.js} +1 -1
  4. package/dist/cjs/{filepond-plugin-file-validate-size-CXJiLSzt.js → filepond-plugin-file-validate-size-DIJUAZji.js} +1 -1
  5. package/dist/cjs/{filepond-plugin-file-validate-type-DWK85msl.js → filepond-plugin-file-validate-type-4lpOf3hf.js} +1 -1
  6. package/dist/cjs/{filepond-plugin-image-edit-BHttJI5X.js → filepond-plugin-image-edit-DtE3Wj_4.js} +1 -1
  7. package/dist/cjs/{filepond-plugin-image-exif-orientation-DTKIO2zE.js → filepond-plugin-image-exif-orientation-Dc_jwifJ.js} +1 -1
  8. package/dist/cjs/{filepond-plugin-image-preview-BaRxwhFa.js → filepond-plugin-image-preview-D_8qt0dd.js} +1 -1
  9. package/dist/cjs/{index-BJ3-fFSF.js → index-D0d25H-r.js} +2 -2
  10. package/dist/cjs/{intlTelInput-DNoOb73M.js → intlTelInput-DqmuleM6.js} +1 -1
  11. package/dist/cjs/loader.cjs.js +2 -2
  12. package/dist/cjs/salla-accordion-body_2.cjs.entry.js +1 -1
  13. package/dist/cjs/salla-accordion_6.cjs.entry.js +2 -2
  14. package/dist/cjs/salla-add-product-button_5.cjs.entry.js +2 -2
  15. package/dist/cjs/salla-advertisement.cjs.entry.js +1 -1
  16. package/dist/cjs/salla-alert_2.cjs.entry.js +1 -1
  17. package/dist/cjs/salla-app-install-alert.cjs.entry.js +1 -1
  18. package/dist/cjs/salla-applied-filters.cjs.entry.js +2 -2
  19. package/dist/cjs/salla-apps-icons.cjs.entry.js +1 -1
  20. package/dist/cjs/salla-attachment-view_9.cjs.entry.js +11 -11
  21. package/dist/cjs/salla-badge.cjs.entry.js +1 -1
  22. package/dist/cjs/salla-basket-gap.cjs.entry.js +1 -1
  23. package/dist/cjs/salla-bullet-delivery_3.cjs.entry.js +1 -1
  24. package/dist/cjs/salla-cart-coupons.cjs.entry.js +1 -1
  25. package/dist/cjs/salla-cart-item-offers_2.cjs.entry.js +5 -1
  26. package/dist/cjs/salla-cart-summary-card.cjs.entry.js +1 -1
  27. package/dist/cjs/salla-cashback-banner.cjs.entry.js +1 -1
  28. package/dist/cjs/salla-comment-factors.cjs.entry.js +1 -1
  29. package/dist/cjs/salla-comment-form_9.cjs.entry.js +2 -2
  30. package/dist/cjs/salla-conditional-offer.cjs.entry.js +628 -19
  31. package/dist/cjs/salla-contacts.cjs.entry.js +1 -1
  32. package/dist/cjs/salla-cookies-bar.cjs.entry.js +1 -1
  33. package/dist/cjs/salla-count-down.cjs.entry.js +1 -1
  34. package/dist/cjs/salla-custom-fields.cjs.entry.js +1 -1
  35. package/dist/cjs/salla-delivery-promise.cjs.entry.js +1 -1
  36. package/dist/cjs/salla-edit-order-button.cjs.entry.js +1 -1
  37. package/dist/cjs/salla-filters-widget.cjs.entry.js +2 -2
  38. package/dist/cjs/salla-filters.cjs.entry.js +2 -2
  39. package/dist/cjs/salla-free-shipping-banner.cjs.entry.js +1 -1
  40. package/dist/cjs/salla-free-shipping-tag.cjs.entry.js +1 -1
  41. package/dist/cjs/salla-fulfillment-methods.cjs.entry.js +1 -1
  42. package/dist/cjs/salla-gift-card-redeem_2.cjs.entry.js +1 -1
  43. package/dist/cjs/salla-gifting.cjs.entry.js +1 -1
  44. package/dist/cjs/salla-hook.cjs.entry.js +1 -1
  45. package/dist/cjs/salla-infinite-scroll.cjs.entry.js +2 -2
  46. package/dist/cjs/salla-installment.cjs.entry.js +1 -1
  47. package/dist/cjs/salla-list-tile.cjs.entry.js +1 -1
  48. package/dist/cjs/salla-localization-modal.cjs.entry.js +1 -1
  49. package/dist/cjs/salla-login-modal.cjs.entry.js +1 -1
  50. package/dist/cjs/salla-loyalty-banner_10.cjs.entry.js +1 -1
  51. package/dist/cjs/salla-loyalty-exchange-product-card.cjs.entry.js +1 -1
  52. package/dist/cjs/salla-loyalty-points-banner.cjs.entry.js +1 -1
  53. package/dist/cjs/salla-loyalty-points-history.cjs.entry.js +1 -1
  54. package/dist/cjs/salla-loyalty-prize-item.cjs.entry.js +1 -1
  55. package/dist/cjs/salla-loyalty.cjs.entry.js +1 -1
  56. package/dist/cjs/salla-maintenance-alert.cjs.entry.js +1 -1
  57. package/dist/cjs/salla-map_3.cjs.entry.js +1 -1
  58. package/dist/cjs/salla-menu.cjs.entry.js +1 -1
  59. package/dist/cjs/salla-metadata.cjs.entry.js +1 -1
  60. package/dist/cjs/salla-mobile-update-modal.cjs.entry.js +1 -1
  61. package/dist/cjs/salla-multiple-bundle-product-cart_2.cjs.entry.js +1 -1
  62. package/dist/cjs/salla-multiple-bundle-product-options-modal_2.cjs.entry.js +1 -1
  63. package/dist/cjs/salla-multiple-bundle-product.cjs.entry.js +1 -1
  64. package/dist/cjs/salla-next-order-coupon.cjs.entry.js +8 -1
  65. package/dist/cjs/salla-notification-item_2.cjs.entry.js +2 -2
  66. package/dist/cjs/salla-offer-modal.cjs.entry.js +1 -1
  67. package/dist/cjs/salla-offer.cjs.entry.js +1 -1
  68. package/dist/cjs/salla-order-branch.cjs.entry.js +1 -1
  69. package/dist/cjs/salla-order-cancel.cjs.entry.js +1 -1
  70. package/dist/cjs/salla-order-details-multiple-bundle-product.cjs.entry.js +1 -1
  71. package/dist/cjs/salla-order-details-options.cjs.entry.js +1 -1
  72. package/dist/cjs/salla-order-details.cjs.entry.js +1 -1
  73. package/dist/cjs/salla-order-edit-item.cjs.entry.js +1 -1
  74. package/dist/cjs/salla-order-edit-product-card.cjs.entry.js +1 -1
  75. package/dist/cjs/salla-order-edit.cjs.entry.js +1 -1
  76. package/dist/cjs/salla-order-items.cjs.entry.js +1 -1
  77. package/dist/cjs/salla-order-options-modal.cjs.entry.js +1 -1
  78. package/dist/cjs/salla-order-payment.cjs.entry.js +1 -1
  79. package/dist/cjs/salla-order-rewards.cjs.entry.js +1 -1
  80. package/dist/cjs/salla-order-shipments.cjs.entry.js +2 -11
  81. package/dist/cjs/salla-order-summary.cjs.entry.js +1 -1
  82. package/dist/cjs/salla-order-totals-card.cjs.entry.js +1 -1
  83. package/dist/cjs/salla-orders.cjs.entry.js +2 -2
  84. package/dist/cjs/salla-payments.cjs.entry.js +1 -1
  85. package/dist/cjs/salla-placeholder.cjs.entry.js +1 -1
  86. package/dist/cjs/salla-price-range.cjs.entry.js +1 -1
  87. package/dist/cjs/salla-product-card-embed.cjs.entry.js +1 -1
  88. package/dist/cjs/salla-product-card.cjs.entry.js +1 -1
  89. package/dist/cjs/salla-product-size-guide.cjs.entry.js +1 -1
  90. package/dist/cjs/salla-products-list.cjs.entry.js +2 -2
  91. package/dist/cjs/salla-products-slider.cjs.entry.js +2 -2
  92. package/dist/cjs/salla-quick-order.cjs.entry.js +1 -1
  93. package/dist/cjs/salla-rating-modal-v2.cjs.entry.js +2 -2
  94. package/dist/cjs/salla-rating-modal.cjs.entry.js +2 -2
  95. package/dist/cjs/salla-review-factors-tags.cjs.entry.js +1 -1
  96. package/dist/cjs/salla-review-order-item.cjs.entry.js +1 -1
  97. package/dist/cjs/salla-scopes.cjs.entry.js +1 -1
  98. package/dist/cjs/salla-search-product-card.cjs.entry.js +1 -1
  99. package/dist/cjs/salla-search.cjs.entry.js +2 -2
  100. package/dist/cjs/salla-skeleton.cjs.entry.js +1 -1
  101. package/dist/cjs/salla-social-share.cjs.entry.js +1 -1
  102. package/dist/cjs/salla-social.cjs.entry.js +1 -1
  103. package/dist/cjs/salla-tab-content_3.cjs.entry.js +1 -1
  104. package/dist/cjs/salla-thank-you-header.cjs.entry.js +1 -1
  105. package/dist/cjs/salla-thank-you-page.cjs.entry.js +1 -1
  106. package/dist/cjs/salla-tiered-offer.cjs.entry.js +1 -1
  107. package/dist/cjs/salla-trust-badges.cjs.entry.js +1 -1
  108. package/dist/cjs/salla-user-menu.cjs.entry.js +1 -1
  109. package/dist/cjs/salla-user-profile.cjs.entry.js +1 -1
  110. package/dist/cjs/salla-user-settings.cjs.entry.js +1 -1
  111. package/dist/cjs/salla-verify.cjs.entry.js +1 -1
  112. package/dist/cjs/salla-wallet.cjs.entry.js +1 -1
  113. package/dist/cjs/salla-wishlist-actions.cjs.entry.js +1 -1
  114. package/dist/cjs/salla-wishlist-summary.cjs.entry.js +2 -2
  115. package/dist/cjs/twilight.cjs.js +2 -2
  116. package/dist/cjs/{vanilla-picker-CceY3g8c.js → vanilla-picker-DP9Hy8IR.js} +1 -1
  117. package/dist/collection/components/salla-cart-item-offers/salla-cart-item-offers.js +4 -0
  118. package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.css +139 -0
  119. package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.js +568 -14
  120. package/dist/collection/components/salla-next-order-coupon/salla-next-order-coupon.js +7 -0
  121. package/dist/collection/components/salla-order-shipments/salla-order-shipments.js +1 -10
  122. package/dist/collection/shared/cart-free-gifts.js +42 -1
  123. package/dist/collection/shared/offer-stages.js +22 -3
  124. package/dist/components/index.js +2 -2
  125. package/dist/components/salla-cart-item-offers.js +4 -0
  126. package/dist/components/salla-conditional-offer.js +631 -18
  127. package/dist/components/salla-next-order-coupon.js +7 -0
  128. package/dist/components/salla-order-shipments.js +1 -10
  129. package/dist/docs.json +2 -2
  130. package/dist/esm/{Helper-BwzPfwZJ.js → Helper-DmnUAusv.js} +1 -1
  131. package/dist/esm/{filepond-JNV-1zqR.js → filepond-DZbzdjkK.js} +1 -1
  132. package/dist/esm/{filepond-plugin-file-poster-nFqNHCag.js → filepond-plugin-file-poster-D9cjvoj-.js} +1 -1
  133. package/dist/esm/{filepond-plugin-file-validate-size-P3KmOTH8.js → filepond-plugin-file-validate-size-D-kjllC-.js} +1 -1
  134. package/dist/esm/{filepond-plugin-file-validate-type-BV0Ctr7U.js → filepond-plugin-file-validate-type-D9hsNaJl.js} +1 -1
  135. package/dist/esm/{filepond-plugin-image-edit-D62rKlfH.js → filepond-plugin-image-edit-CDHAxJY8.js} +1 -1
  136. package/dist/esm/{filepond-plugin-image-exif-orientation-DerapxIk.js → filepond-plugin-image-exif-orientation-BGKiclwK.js} +1 -1
  137. package/dist/esm/{filepond-plugin-image-preview-BlRd2Ble.js → filepond-plugin-image-preview-BUjvnt8G.js} +1 -1
  138. package/dist/esm/{index-CtoXzf1y.js → index-ze9e0dxM.js} +2 -2
  139. package/dist/esm/{intlTelInput-BZz-u5_w.js → intlTelInput-BkYRB6km.js} +1 -1
  140. package/dist/esm/loader.js +3 -3
  141. package/dist/esm/salla-accordion-body_2.entry.js +1 -1
  142. package/dist/esm/salla-accordion_6.entry.js +2 -2
  143. package/dist/esm/salla-add-product-button_5.entry.js +2 -2
  144. package/dist/esm/salla-advertisement.entry.js +1 -1
  145. package/dist/esm/salla-alert_2.entry.js +1 -1
  146. package/dist/esm/salla-app-install-alert.entry.js +1 -1
  147. package/dist/esm/salla-applied-filters.entry.js +2 -2
  148. package/dist/esm/salla-apps-icons.entry.js +1 -1
  149. package/dist/esm/salla-attachment-view_9.entry.js +11 -11
  150. package/dist/esm/salla-badge.entry.js +1 -1
  151. package/dist/esm/salla-basket-gap.entry.js +1 -1
  152. package/dist/esm/salla-bullet-delivery_3.entry.js +1 -1
  153. package/dist/esm/salla-cart-coupons.entry.js +1 -1
  154. package/dist/esm/salla-cart-item-offers_2.entry.js +5 -1
  155. package/dist/esm/salla-cart-summary-card.entry.js +1 -1
  156. package/dist/esm/salla-cashback-banner.entry.js +1 -1
  157. package/dist/esm/salla-comment-factors.entry.js +1 -1
  158. package/dist/esm/salla-comment-form_9.entry.js +2 -2
  159. package/dist/esm/salla-conditional-offer.entry.js +628 -19
  160. package/dist/esm/salla-contacts.entry.js +1 -1
  161. package/dist/esm/salla-cookies-bar.entry.js +1 -1
  162. package/dist/esm/salla-count-down.entry.js +1 -1
  163. package/dist/esm/salla-custom-fields.entry.js +1 -1
  164. package/dist/esm/salla-delivery-promise.entry.js +1 -1
  165. package/dist/esm/salla-edit-order-button.entry.js +1 -1
  166. package/dist/esm/salla-filters-widget.entry.js +2 -2
  167. package/dist/esm/salla-filters.entry.js +2 -2
  168. package/dist/esm/salla-free-shipping-banner.entry.js +1 -1
  169. package/dist/esm/salla-free-shipping-tag.entry.js +1 -1
  170. package/dist/esm/salla-fulfillment-methods.entry.js +1 -1
  171. package/dist/esm/salla-gift-card-redeem_2.entry.js +1 -1
  172. package/dist/esm/salla-gifting.entry.js +1 -1
  173. package/dist/esm/salla-hook.entry.js +1 -1
  174. package/dist/esm/salla-infinite-scroll.entry.js +2 -2
  175. package/dist/esm/salla-installment.entry.js +1 -1
  176. package/dist/esm/salla-list-tile.entry.js +1 -1
  177. package/dist/esm/salla-localization-modal.entry.js +1 -1
  178. package/dist/esm/salla-login-modal.entry.js +1 -1
  179. package/dist/esm/salla-loyalty-banner_10.entry.js +1 -1
  180. package/dist/esm/salla-loyalty-exchange-product-card.entry.js +1 -1
  181. package/dist/esm/salla-loyalty-points-banner.entry.js +1 -1
  182. package/dist/esm/salla-loyalty-points-history.entry.js +1 -1
  183. package/dist/esm/salla-loyalty-prize-item.entry.js +1 -1
  184. package/dist/esm/salla-loyalty.entry.js +1 -1
  185. package/dist/esm/salla-maintenance-alert.entry.js +1 -1
  186. package/dist/esm/salla-map_3.entry.js +1 -1
  187. package/dist/esm/salla-menu.entry.js +1 -1
  188. package/dist/esm/salla-metadata.entry.js +1 -1
  189. package/dist/esm/salla-mobile-update-modal.entry.js +1 -1
  190. package/dist/esm/salla-multiple-bundle-product-cart_2.entry.js +1 -1
  191. package/dist/esm/salla-multiple-bundle-product-options-modal_2.entry.js +1 -1
  192. package/dist/esm/salla-multiple-bundle-product.entry.js +1 -1
  193. package/dist/esm/salla-next-order-coupon.entry.js +8 -1
  194. package/dist/esm/salla-notification-item_2.entry.js +2 -2
  195. package/dist/esm/salla-offer-modal.entry.js +1 -1
  196. package/dist/esm/salla-offer.entry.js +1 -1
  197. package/dist/esm/salla-order-branch.entry.js +1 -1
  198. package/dist/esm/salla-order-cancel.entry.js +1 -1
  199. package/dist/esm/salla-order-details-multiple-bundle-product.entry.js +1 -1
  200. package/dist/esm/salla-order-details-options.entry.js +1 -1
  201. package/dist/esm/salla-order-details.entry.js +1 -1
  202. package/dist/esm/salla-order-edit-item.entry.js +1 -1
  203. package/dist/esm/salla-order-edit-product-card.entry.js +1 -1
  204. package/dist/esm/salla-order-edit.entry.js +1 -1
  205. package/dist/esm/salla-order-items.entry.js +1 -1
  206. package/dist/esm/salla-order-options-modal.entry.js +1 -1
  207. package/dist/esm/salla-order-payment.entry.js +1 -1
  208. package/dist/esm/salla-order-rewards.entry.js +1 -1
  209. package/dist/esm/salla-order-shipments.entry.js +2 -11
  210. package/dist/esm/salla-order-summary.entry.js +1 -1
  211. package/dist/esm/salla-order-totals-card.entry.js +1 -1
  212. package/dist/esm/salla-orders.entry.js +2 -2
  213. package/dist/esm/salla-payments.entry.js +1 -1
  214. package/dist/esm/salla-placeholder.entry.js +1 -1
  215. package/dist/esm/salla-price-range.entry.js +1 -1
  216. package/dist/esm/salla-product-card-embed.entry.js +1 -1
  217. package/dist/esm/salla-product-card.entry.js +1 -1
  218. package/dist/esm/salla-product-size-guide.entry.js +1 -1
  219. package/dist/esm/salla-products-list.entry.js +2 -2
  220. package/dist/esm/salla-products-slider.entry.js +2 -2
  221. package/dist/esm/salla-quick-order.entry.js +1 -1
  222. package/dist/esm/salla-rating-modal-v2.entry.js +2 -2
  223. package/dist/esm/salla-rating-modal.entry.js +2 -2
  224. package/dist/esm/salla-review-factors-tags.entry.js +1 -1
  225. package/dist/esm/salla-review-order-item.entry.js +1 -1
  226. package/dist/esm/salla-scopes.entry.js +1 -1
  227. package/dist/esm/salla-search-product-card.entry.js +1 -1
  228. package/dist/esm/salla-search.entry.js +2 -2
  229. package/dist/esm/salla-skeleton.entry.js +1 -1
  230. package/dist/esm/salla-social-share.entry.js +1 -1
  231. package/dist/esm/salla-social.entry.js +1 -1
  232. package/dist/esm/salla-tab-content_3.entry.js +1 -1
  233. package/dist/esm/salla-thank-you-header.entry.js +1 -1
  234. package/dist/esm/salla-thank-you-page.entry.js +1 -1
  235. package/dist/esm/salla-tiered-offer.entry.js +1 -1
  236. package/dist/esm/salla-trust-badges.entry.js +1 -1
  237. package/dist/esm/salla-user-menu.entry.js +1 -1
  238. package/dist/esm/salla-user-profile.entry.js +1 -1
  239. package/dist/esm/salla-user-settings.entry.js +1 -1
  240. package/dist/esm/salla-verify.entry.js +1 -1
  241. package/dist/esm/salla-wallet.entry.js +1 -1
  242. package/dist/esm/salla-wishlist-actions.entry.js +1 -1
  243. package/dist/esm/salla-wishlist-summary.entry.js +2 -2
  244. package/dist/esm/twilight.js +3 -3
  245. package/dist/esm/{vanilla-picker-BfmpXY4G.js → vanilla-picker-CxhuwWDi.js} +1 -1
  246. package/dist/twilight/{p-dc2be7b8.entry.js → p-008740fe.entry.js} +1 -1
  247. package/dist/twilight/{p-aac9d2fe.entry.js → p-024ecda2.entry.js} +1 -1
  248. package/dist/twilight/{p-60ae422e.entry.js → p-02b7dfcc.entry.js} +1 -1
  249. package/dist/twilight/{p-4008e164.entry.js → p-03b2df97.entry.js} +1 -1
  250. package/dist/twilight/{p-36167cbe.entry.js → p-07e581af.entry.js} +1 -1
  251. package/dist/twilight/{p-2afc4658.entry.js → p-08be494e.entry.js} +1 -1
  252. package/dist/twilight/{p-4f4933f8.entry.js → p-0c022fbd.entry.js} +1 -1
  253. package/dist/twilight/{p-ebe45961.entry.js → p-0c0b2b36.entry.js} +1 -1
  254. package/dist/twilight/{p-40e651ac.entry.js → p-0fd15433.entry.js} +1 -1
  255. package/dist/twilight/{p-a4fee391.entry.js → p-11eb8675.entry.js} +1 -1
  256. package/dist/twilight/{p-f738f492.entry.js → p-12a52f1b.entry.js} +1 -1
  257. package/dist/twilight/{p-3e285056.entry.js → p-16e2a110.entry.js} +1 -1
  258. package/dist/twilight/{p-3da2e3f3.entry.js → p-1ca39f1c.entry.js} +1 -1
  259. package/dist/twilight/{p-fbce8a18.entry.js → p-20949dfb.entry.js} +1 -1
  260. package/dist/twilight/{p-4080fdc9.entry.js → p-2aa9147c.entry.js} +1 -1
  261. package/dist/twilight/{p-0b583810.entry.js → p-2dcd24fe.entry.js} +1 -1
  262. package/dist/twilight/{p-8413d52b.entry.js → p-331003bc.entry.js} +1 -1
  263. package/dist/twilight/{p-1f39ca4f.entry.js → p-37b64e36.entry.js} +1 -1
  264. package/dist/twilight/{p-1af75625.entry.js → p-3a2ff4d6.entry.js} +1 -1
  265. package/dist/twilight/{p-213368ef.entry.js → p-3ec3e9fb.entry.js} +1 -1
  266. package/dist/twilight/{p-36a7344c.entry.js → p-446e3f38.entry.js} +1 -1
  267. package/dist/twilight/{p-ed37e702.entry.js → p-455a4702.entry.js} +1 -1
  268. package/dist/twilight/{p-e89daada.entry.js → p-45a7d444.entry.js} +1 -1
  269. package/dist/twilight/{p-f3455825.entry.js → p-474d882c.entry.js} +1 -1
  270. package/dist/twilight/{p-b3472096.entry.js → p-47c79c49.entry.js} +1 -1
  271. package/dist/twilight/{p-4254e9c2.entry.js → p-4c191ab4.entry.js} +1 -1
  272. package/dist/twilight/{p-8cbf3bc2.entry.js → p-4cf81a44.entry.js} +1 -1
  273. package/dist/twilight/{p-97065b4f.entry.js → p-4ec15515.entry.js} +1 -1
  274. package/dist/twilight/{p-9c1b0978.entry.js → p-519cf4c2.entry.js} +1 -1
  275. package/dist/twilight/{p-374d9f0c.entry.js → p-57fafaba.entry.js} +1 -1
  276. package/dist/twilight/{p-a9ae6ab9.entry.js → p-59694c45.entry.js} +1 -1
  277. package/dist/twilight/{p-5dea6c09.entry.js → p-5b118b5b.entry.js} +1 -1
  278. package/dist/twilight/{p-7169ba9a.entry.js → p-5ceda3d5.entry.js} +1 -1
  279. package/dist/twilight/{p-02ddd64c.entry.js → p-5d8937c5.entry.js} +1 -1
  280. package/dist/twilight/{p-a8102290.entry.js → p-5e0f85ae.entry.js} +1 -1
  281. package/dist/twilight/{p-b3c9ab0e.entry.js → p-5f9d87ec.entry.js} +1 -1
  282. package/dist/twilight/{p-034b80ad.entry.js → p-61b98119.entry.js} +1 -1
  283. package/dist/twilight/{p-f51f31df.entry.js → p-67cf7454.entry.js} +1 -1
  284. package/dist/twilight/{p-cd55369c.entry.js → p-6d6b99fe.entry.js} +1 -1
  285. package/dist/twilight/{p-1d105a57.entry.js → p-71ce8e4f.entry.js} +1 -1
  286. package/dist/twilight/{p-00a1ec4e.entry.js → p-73649c81.entry.js} +1 -1
  287. package/dist/twilight/{p-a260f35e.entry.js → p-75ad7f14.entry.js} +1 -1
  288. package/dist/twilight/{p-117e714b.entry.js → p-75f7f1d8.entry.js} +1 -1
  289. package/dist/twilight/{p-2e1dd9cf.entry.js → p-770142fe.entry.js} +1 -1
  290. package/dist/twilight/{p-7c684782.entry.js → p-7731e280.entry.js} +1 -1
  291. package/dist/twilight/p-776f9492.entry.js +4 -0
  292. package/dist/twilight/{p-365ff361.entry.js → p-7790a9a1.entry.js} +1 -1
  293. package/dist/twilight/p-78c04993.entry.js +4 -0
  294. package/dist/twilight/{p-3d3eda0f.entry.js → p-792494c8.entry.js} +1 -1
  295. package/dist/twilight/{p-c4264e87.entry.js → p-7a985fe3.entry.js} +1 -1
  296. package/dist/twilight/{p-6495383e.entry.js → p-7fffa509.entry.js} +1 -1
  297. package/dist/twilight/{p-d76b7564.entry.js → p-8077d8b6.entry.js} +1 -1
  298. package/dist/twilight/{p-9e50f29c.entry.js → p-830abcc1.entry.js} +1 -1
  299. package/dist/twilight/{p-3356638d.entry.js → p-89598ad3.entry.js} +1 -1
  300. package/dist/twilight/{p-0cb60522.entry.js → p-8b0b6bca.entry.js} +1 -1
  301. package/dist/twilight/{p-8b9c44c8.entry.js → p-8d096f95.entry.js} +1 -1
  302. package/dist/twilight/{p-a236dd3b.entry.js → p-8d3c5d1f.entry.js} +1 -1
  303. package/dist/twilight/{p-ae1efde9.entry.js → p-93571975.entry.js} +1 -1
  304. package/dist/twilight/{p-38b70281.entry.js → p-97c5b11d.entry.js} +1 -1
  305. package/dist/twilight/{p-c18fe9e5.entry.js → p-97fa91db.entry.js} +1 -1
  306. package/dist/twilight/{p-94d5b080.entry.js → p-9a2567e2.entry.js} +1 -1
  307. package/dist/twilight/p-9acfe389.entry.js +4 -0
  308. package/dist/twilight/{p-79b743eb.entry.js → p-9dd7cf71.entry.js} +1 -1
  309. package/dist/twilight/{p-2882d626.entry.js → p-9fe5c0f5.entry.js} +1 -1
  310. package/dist/twilight/{p-F1Erq7GR.js → p-B8nGVDvQ.js} +1 -1
  311. package/dist/twilight/{p-CC-KFXD9.js → p-BjUh9YCs.js} +1 -1
  312. package/dist/twilight/{p-BwUX8dew.js → p-BlPdaFn2.js} +1 -1
  313. package/dist/twilight/{p-6KZIgnh6.js → p-BpJsOjKP.js} +1 -1
  314. package/dist/twilight/{p-78r0jdyY.js → p-C9ldyQUq.js} +1 -1
  315. package/dist/twilight/{p-x4_Sxqd_.js → p-CICtuB_u.js} +1 -1
  316. package/dist/twilight/{p-DT73iyFr.js → p-CNQaYCUJ.js} +1 -1
  317. package/dist/twilight/{p-DYAn-F48.js → p-DOUAavek.js} +2 -2
  318. package/dist/twilight/{p-b2cbdaa0.entry.js → p-a1a650ab.entry.js} +1 -1
  319. package/dist/twilight/{p-71b5546b.entry.js → p-a7566af2.entry.js} +1 -1
  320. package/dist/twilight/{p-e8198efc.entry.js → p-a7849674.entry.js} +1 -1
  321. package/dist/twilight/{p-2140be84.entry.js → p-a865995d.entry.js} +1 -1
  322. package/dist/twilight/{p-8fe950e8.entry.js → p-b297fe2e.entry.js} +1 -1
  323. package/dist/twilight/{p-e8ebb2fe.entry.js → p-b6098938.entry.js} +1 -1
  324. package/dist/twilight/{p-0d1b8490.entry.js → p-b71673ee.entry.js} +1 -1
  325. package/dist/twilight/{p-e2cb5c49.entry.js → p-bb137dbb.entry.js} +1 -1
  326. package/dist/twilight/{p-6ffbf505.entry.js → p-bd06c200.entry.js} +1 -1
  327. package/dist/twilight/{p-7303ebd1.entry.js → p-be8c42f1.entry.js} +1 -1
  328. package/dist/twilight/{p-3845e427.entry.js → p-bf5b5f52.entry.js} +1 -1
  329. package/dist/twilight/{p-72b0724c.entry.js → p-bf65d2fa.entry.js} +1 -1
  330. package/dist/twilight/{p-d6a3b2d8.entry.js → p-c0301e00.entry.js} +1 -1
  331. package/dist/twilight/{p-2c04bba3.entry.js → p-c122e9c1.entry.js} +1 -1
  332. package/dist/twilight/{p-f67d03fb.entry.js → p-c6f09614.entry.js} +1 -1
  333. package/dist/twilight/{p-8ea3a265.entry.js → p-cb961f05.entry.js} +1 -1
  334. package/dist/twilight/{p-e5dc1d27.entry.js → p-cbd056ba.entry.js} +1 -1
  335. package/dist/twilight/{p-4c5a7ffc.entry.js → p-cdeaf60b.entry.js} +1 -1
  336. package/dist/twilight/{p-D5DrmlyQ.js → p-cykIAOlk.js} +1 -1
  337. package/dist/twilight/{p-2566f688.entry.js → p-d089ac6d.entry.js} +1 -1
  338. package/dist/twilight/{p-ebed682b.entry.js → p-d2396143.entry.js} +1 -1
  339. package/dist/twilight/{p-974c100d.entry.js → p-d300a117.entry.js} +1 -1
  340. package/dist/twilight/{p-1b4c3b85.entry.js → p-d61298f8.entry.js} +1 -1
  341. package/dist/twilight/{p-f664fa69.entry.js → p-d89328d7.entry.js} +1 -1
  342. package/dist/twilight/p-da0349f9.entry.js +4 -0
  343. package/dist/twilight/{p-118aa92e.entry.js → p-da60995f.entry.js} +1 -1
  344. package/dist/twilight/{p-cbff6c68.entry.js → p-dbb4db25.entry.js} +1 -1
  345. package/dist/twilight/{p-4adaee32.entry.js → p-dceb3725.entry.js} +1 -1
  346. package/dist/twilight/{p-8fd6ed52.entry.js → p-de10d0c3.entry.js} +1 -1
  347. package/dist/twilight/{p-77deaa00.entry.js → p-dfde387f.entry.js} +1 -1
  348. package/dist/twilight/{p-7a8fde62.entry.js → p-e23b84bf.entry.js} +1 -1
  349. package/dist/twilight/{p-0ce0cdb0.entry.js → p-e3dd9502.entry.js} +1 -1
  350. package/dist/twilight/{p-b1bc70af.entry.js → p-e849f5ef.entry.js} +1 -1
  351. package/dist/twilight/{p-f310b923.entry.js → p-ed96c703.entry.js} +1 -1
  352. package/dist/twilight/{p-2401f0d7.entry.js → p-f054cf8e.entry.js} +1 -1
  353. package/dist/twilight/{p-aca2717e.entry.js → p-f23ada45.entry.js} +1 -1
  354. package/dist/twilight/{p-95e7c2e1.entry.js → p-f7c06bee.entry.js} +1 -1
  355. package/dist/twilight/{p-c1ab1b40.entry.js → p-f9dd6b3e.entry.js} +1 -1
  356. package/dist/twilight/{p-055bf228.entry.js → p-fb955257.entry.js} +1 -1
  357. package/dist/twilight/{p-061216d4.entry.js → p-ffc7f526.entry.js} +1 -1
  358. package/dist/twilight/{p-BeC8AhMn.js → p-wYYV5ykR.js} +1 -1
  359. package/dist/twilight/{p-CtoXzf1y.js → p-ze9e0dxM.js} +1 -1
  360. package/dist/twilight/twilight.esm.js +1 -1
  361. package/dist/types/components/salla-conditional-offer/interfaces.d.ts +36 -0
  362. package/dist/types/components/salla-conditional-offer/salla-conditional-offer.d.ts +182 -3
  363. package/dist/types/components/salla-order-shipments/salla-order-shipments.d.ts +0 -1
  364. package/dist/types/shared/cart-free-gifts.d.ts +26 -0
  365. package/dist/types/shared/offer-stages.d.ts +6 -1
  366. package/package.json +5 -5
  367. package/dist/twilight/p-0fb390fb.entry.js +0 -4
  368. package/dist/twilight/p-17be7967.entry.js +0 -4
  369. package/dist/twilight/p-5bc2190a.entry.js +0 -4
  370. package/dist/twilight/p-919650b7.entry.js +0 -4
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Crafted with ❤ by Salla
3
3
  */
4
- import { r as registerInstance, h, H as Host, a as getElement } from './index-CtoXzf1y.js';
4
+ import { r as registerInstance, h, H as Host, a as getElement } from './index-ze9e0dxM.js';
5
5
 
6
6
  var DiscountType;
7
7
  (function (DiscountType) {
@@ -44,6 +44,9 @@ var OfferType;
44
44
  * `twilight/packages/twilight/src/api/cart.js:831-846`.
45
45
  */
46
46
  const EMPTY_GIFT_ITEMS = Object.freeze([]);
47
+ const EMPTY_PRODUCT_LINES = Object.freeze([]);
48
+ /** Every line from the last parsed response — published in the SAME pass as `itemsCache`. */
49
+ let linesCache = null;
47
50
  /**
48
51
  * The gift LINES from the last parsed response. `null` is the single "nothing has loaded
49
52
  * yet" flag shared with `countCache`, which keeps the two from ever disagreeing about
@@ -145,6 +148,7 @@ function syncCartIdentity() {
145
148
  if (cacheIdentity !== undefined && cacheIdentity !== current) {
146
149
  generation++;
147
150
  itemsCache = null;
151
+ linesCache = null;
148
152
  countCache = null;
149
153
  isAuthoritative = false;
150
154
  pendingRequest = null;
@@ -269,6 +273,24 @@ function buildGiftItems(items) {
269
273
  }
270
274
  return gifts;
271
275
  }
276
+ /** Every line of the cart, in cart order — see `CartProductLine`. */
277
+ function buildProductLines(items) {
278
+ const lines = [];
279
+ for (const item of items) {
280
+ if (item?.id === undefined || item?.id === null)
281
+ continue;
282
+ const quantity = toNumber(item.quantity);
283
+ lines.push({
284
+ id: String(item.id),
285
+ productId: toNullableNumber(item.product_id),
286
+ quantity,
287
+ // Only a line the server flagged as granted by a cart offer has free units.
288
+ freeQuantity: item.added_by_offer ? Math.max(0, resolveFreeQuantity(item, quantity)) : 0,
289
+ offerId: item.added_by_offer ? toNullableNumber(item.offer_id) : null,
290
+ });
291
+ }
292
+ return lines;
293
+ }
272
294
  function fetchAndCache() {
273
295
  if (pendingRequest)
274
296
  return pendingRequest;
@@ -283,6 +305,7 @@ function fetchAndCache() {
283
305
  // Both projections come out of the SAME response, in one pass.
284
306
  return {
285
307
  gifts: buildGiftItems(items),
308
+ lines: buildProductLines(items),
286
309
  count: extractCartCount(res, found),
287
310
  // The response was reached and parsed — these values are the cart, so the cache
288
311
  // they land in may be short-circuited on. See `isAuthoritative`.
@@ -302,6 +325,7 @@ function fetchAndCache() {
302
325
  // connection can never claim the cart is empty.
303
326
  .catch((error) => ({
304
327
  gifts: itemsCache ?? [],
328
+ lines: linesCache ?? [],
305
329
  count: isCartNotCreated(error) ? 0 : countCache,
306
330
  /**
307
331
  * NOT authoritative, `CartNotCreated` included.
@@ -318,13 +342,14 @@ function fetchAndCache() {
318
342
  */
319
343
  ok: false,
320
344
  }))
321
- .then(({ gifts, count, ok }) => {
345
+ .then(({ gifts, lines, count, ok }) => {
322
346
  // Only publish if nothing invalidated us while we were in flight.
323
347
  // Otherwise this is pre-mutation data racing a newer, authoritative
324
348
  // request — publishing it would make the lookups short-circuit on stale
325
349
  // data until the NEXT mutation happens to come along.
326
350
  if (startedAtGeneration === generation) {
327
351
  itemsCache = gifts;
352
+ linesCache = lines;
328
353
  countCache = count;
329
354
  isAuthoritative = ok;
330
355
  }
@@ -353,6 +378,20 @@ function getCartFreeGiftItems() {
353
378
  }
354
379
  return fetchAndCache().then(() => itemsCache ?? EMPTY_GIFT_ITEMS);
355
380
  }
381
+ /**
382
+ * Every line of the cart (paid and free), in cart order — for the CP-2171 free product picker.
383
+ *
384
+ * Shares the cache, the in-flight promise and the generation guard with
385
+ * `getCartFreeGiftItems()` and `getCartItemCount()`: no request of its own, and never a
386
+ * different cart state from theirs.
387
+ */
388
+ function getCartProductLines() {
389
+ syncCartIdentity();
390
+ if (isAuthoritative && itemsCache) {
391
+ return Promise.resolve(linesCache ?? EMPTY_PRODUCT_LINES);
392
+ }
393
+ return fetchAndCache().then(() => linesCache ?? EMPTY_PRODUCT_LINES);
394
+ }
356
395
  /**
357
396
  * Whether the cached cart picture came from a response that was actually reached and
358
397
  * parsed, rather than from the fallback a rejected lookup publishes.
@@ -425,6 +464,7 @@ function refreshCartFreeGifts() {
425
464
  return pendingRefresh;
426
465
  generation++;
427
466
  itemsCache = null;
467
+ linesCache = null;
428
468
  countCache = null;
429
469
  isAuthoritative = false;
430
470
  // This invalidation already covers whatever id the cart has now.
@@ -663,8 +703,13 @@ function sortByThreshold(items, getThreshold, getIdentity) {
663
703
  * @param rawTierValues each stage's OWN `current_value`, aligned the same way. A tier
664
704
  * without a readable one is measured by `currentValue`, so omitting it — an older
665
705
  * backend — changes nothing. See `OfferStageProgress.values`.
706
+ * @param rawCanAddFree each stage's `can_add_free` (CP-2171), aligned the same way. Read
707
+ * only in case (3), where no tier carries `is_applied`: a free product tier that
708
+ * carries one is reached when the ENGINE says so, not when its `current_value` —
709
+ * which counts the gift lines at full price — passes `min_spend`. Omit it and case
710
+ * (3) is the threshold behaviour, unchanged.
666
711
  */
667
- function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = []) {
712
+ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = [], rawCanAddFree = []) {
668
713
  // Unparseable thresholds are dropped, not zeroed — see `isUsableThreshold`.
669
714
  //
670
715
  // The flag travels WITH its threshold through both the drop and the sort. Sorting two
@@ -677,6 +722,7 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
677
722
  threshold: parseNumeric(threshold),
678
723
  applied: readAppliedFlag(rawApplied?.[index]),
679
724
  tierValue: parseNumeric(rawTierValues?.[index]),
725
+ canAddFree: typeof rawCanAddFree?.[index] === 'boolean' ? rawCanAddFree[index] : undefined,
680
726
  }))
681
727
  .filter((entry) => entry.threshold !== null)
682
728
  .sort((a, b) => a.threshold - b.threshold || a.index - b.index);
@@ -764,12 +810,25 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
764
810
  appliedIndex = index;
765
811
  }
766
812
  }
767
- // Case (3) closes the prefix at `lastReachedIndex` rather than comparing tier by tier.
813
+ // Case (3) closes the prefix at the last reached tier rather than comparing tier by tier.
768
814
  // With one value for every tier the two are the same statement; with per-tier values
769
815
  // it is what keeps `met` a prefix, on the same single-winner reasoning as case (1).
816
+ //
817
+ // CP-2171 — a tier that carries `can_add_free` is reached on the engine's answer, not on
818
+ // its value: a guest with 40 paid and three listed gifts in the cart has a value past
819
+ // `min_spend`, yet every gift is charged. Only `met` reads it; the bar (`lastReachedIndex`,
820
+ // `fills`, `isComplete`) stays the threshold projection, and the guidance line, finding
821
+ // nothing left to spend, says the conditions are not met.
822
+ let lastMetIndex = NO_STAGE_REACHED;
823
+ for (let index = 0; index < thresholds.length; index++) {
824
+ const canAddFree = entries[index].canAddFree;
825
+ if (canAddFree === undefined ? values[index] >= thresholds[index] : canAddFree) {
826
+ lastMetIndex = index;
827
+ }
828
+ }
770
829
  const met = hasEngineVerdict
771
830
  ? thresholds.map((_, index) => index <= appliedIndex)
772
- : thresholds.map((_, index) => index <= lastReachedIndex);
831
+ : thresholds.map((_, index) => index <= lastMetIndex);
773
832
  const metCount = met.filter(Boolean).length;
774
833
  const nextUnmetIndex = met.findIndex(isMet => !isMet); // -1 === NO_STAGE_REACHED
775
834
  const allMet = nextUnmetIndex === NO_STAGE_REACHED;
@@ -917,7 +976,7 @@ function getRemainingToStage(index, progress, measure = OfferStageMeasure.CURREN
917
976
  return rounded > 0 ? rounded : 1 / factor;
918
977
  }
919
978
 
920
- const sallaConditionalOfferCss = "/*!twlt*/:host{display:block}.s-conditional-offer-root{background-color:rgba(0, 137, 123, 0.06);background-color:color-mix(in srgb, var(--color-primary, #00897b) 7%, transparent)}.s-conditional-offer-badges::-webkit-scrollbar{height:3px}.s-conditional-offer-badges::-webkit-scrollbar-thumb{border-radius:9999px;background-color:rgba(0, 0, 0, 0.15)}.s-conditional-offer-badges::-webkit-scrollbar-track{background-color:transparent}.s-conditional-offer-tooltip{--s-conditional-offer-tooltip-shift:0px;--s-conditional-offer-tooltip-caret:0px;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-shift)))}.s-conditional-offer-tooltip::before{content:\"\";position:absolute;bottom:100%;left:50%;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-caret)));border:8px solid transparent;border-bottom-color:#fff}.s-conditional-offer-tier-icon-met,.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark, #00695c);background-color:rgba(0, 137, 123, 0.12);background-color:color-mix(in srgb, var(--color-primary, #00897b) 14%, transparent)}.s-conditional-offer-tier:not(:last-child)::before{content:\"\";position:absolute;top:2.5rem;bottom:-1rem;inset-inline-start:1.125rem;width:1px;background-color:#d1d5db}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.w-6{width:1.5rem}.resize{resize:both}.gap-1{gap:0.25rem}.rounded{border-radius:var(--s-radius, 0.25rem)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.s-conditional-offer-root{margin-bottom:1.25rem;display:block;width:100%;min-width:0px;max-width:100%;border-radius:0.375rem;padding:0.75rem}@media (min-width: 768px){.s-conditional-offer-root{padding:1rem}}.s-conditional-offer-banner{display:flex;width:100%;min-width:0px;align-items:center;justify-content:space-between;gap:0.75rem}@media (min-width: 768px){.s-conditional-offer-banner{gap:1rem}}.s-conditional-offer-lead{display:flex;min-width:0px;flex:1 1 0%;align-items:center;gap:0.5rem}@media (min-width: 768px){.s-conditional-offer-lead{gap:0.75rem}}.s-conditional-offer-badges{display:flex;max-width:45%;flex-shrink:0;flex-wrap:nowrap;align-items:center;gap:0.25rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;scrollbar-width:thin}.s-conditional-offer-badge{display:inline-flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:11px;line-height:1;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-badge-met{background-color:var(--color-primary)}.s-conditional-offer-badge-unmet{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.s-conditional-offer-badge-link{width:0.5rem;flex-shrink:0;border-top-width:1px}.s-conditional-offer-badge-link-met{border-color:var(--color-primary)}.s-conditional-offer-badge-link-unmet{border-style:dashed;--tw-border-opacity:1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.s-conditional-offer-badge-icon{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-text{display:flex;min-width:0px;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-heading{display:flex;min-width:0px;align-items:center;gap:0.375rem}.s-conditional-offer-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-info{position:relative;display:inline-flex;flex-shrink:0;align-items:center}.s-conditional-offer-info-trigger{display:inline-flex;height:1rem;width:1rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-info-trigger:hover{color:var(--color-primary-dark)}.s-conditional-offer-info-icon{height:0.875rem;width:0.875rem;flex-shrink:0}.s-conditional-offer-message{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}@media (min-width: 768px){.s-conditional-offer-message{font-size:1rem;line-height:1.5rem}}.s-conditional-offer-toggle{align-items:center;justify-content:center;gap:0.375rem;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle:hover{color:var(--color-primary-dark)}.s-conditional-offer-toggle-inline{display:none;flex-shrink:0}@media (min-width: 768px){.s-conditional-offer-toggle-inline{display:inline-flex}}.s-conditional-offer-toggle-bar{margin-top:0.5rem;display:inline-flex;width:100%;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding-left:0.75rem;padding-right:0.75rem;padding-top:0.625rem;padding-bottom:0.625rem}@media (min-width: 768px){.s-conditional-offer-toggle-bar{display:none}}.s-conditional-offer-toggle-icon{font-size:1rem;line-height:1.5rem;line-height:1;transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle-icon-open{--tw-rotate:180deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.s-conditional-offer-panel{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-gifts-header{margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-gifts-icon{display:inline-flex;height:2rem;width:2rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:var(--color-primary);font-size:1rem;line-height:1.5rem;line-height:1;color:var(--color-primary)}.s-conditional-offer-gifts-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-gifts-list{margin:0px;display:flex;list-style-type:none;flex-direction:column;gap:0.5rem;padding:0px}.s-conditional-offer-gift{display:flex;align-items:center;gap:0.75rem;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));padding:0.5rem}.s-conditional-offer-gift-thumb{display:inline-flex;height:3.5rem;width:3.5rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.25rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-gift-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-gift-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:0.875rem;line-height:1.25rem;font-weight:700;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-gift-free{font-weight:700;--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-price{--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));text-decoration-line:line-through}.s-conditional-offer-tooltip{position:absolute;top:calc(100% + 0.75rem);left:50%;z-index:10;width:min(22rem, calc(100vw - 2rem));border-radius:0.5rem;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:1rem;text-align:start;--tw-shadow:5px 10px 30px #2B2D340D;--tw-shadow-colored:5px 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.s-conditional-offer-tooltip-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tooltip-description{margin-top:0.25rem;font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip-list{margin:0px;margin-top:0.75rem;display:flex;list-style-type:none;flex-direction:column;gap:1rem;padding:0px}.s-conditional-offer-tier{position:relative;display:flex;align-items:flex-start;gap:0.75rem}.s-conditional-offer-tier-icon{position:relative;display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:1rem;line-height:1.5rem;line-height:1}.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark)}.s-conditional-offer-tier-status{position:absolute;bottom:-0.125rem;display:inline-flex;height:1rem;width:1rem;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));inset-inline-start:-2px}.s-conditional-offer-tier-status-met{color:var(--color-primary-dark)}.s-conditional-offer-tier-status-unmet{--tw-text-opacity:1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-status-check{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-tier-status-lock{height:0.625rem;width:0.625rem;flex-shrink:0}.s-conditional-offer-tier-body{display:flex;min-width:0px;flex-direction:column;gap:0.125rem}.s-conditional-offer-tier-title{font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tier-text{font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-text-met{text-decoration-line:line-through}.s-conditional-offer-skeleton-text{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.375rem}";
979
+ const sallaConditionalOfferCss = "/*!twlt*/:host{display:block}.s-conditional-offer-root{background-color:rgba(0, 137, 123, 0.06);background-color:color-mix(in srgb, var(--color-primary, #00897b) 7%, transparent)}.s-conditional-offer-badges::-webkit-scrollbar{height:3px}.s-conditional-offer-badges::-webkit-scrollbar-thumb{border-radius:9999px;background-color:rgba(0, 0, 0, 0.15)}.s-conditional-offer-badges::-webkit-scrollbar-track{background-color:transparent}.s-conditional-offer-tooltip{--s-conditional-offer-tooltip-shift:0px;--s-conditional-offer-tooltip-caret:0px;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-shift)))}.s-conditional-offer-tooltip::before{content:\"\";position:absolute;bottom:100%;left:50%;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-caret)));border:8px solid transparent;border-bottom-color:#fff}.s-conditional-offer-tier-icon-met,.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark, #00695c);background-color:rgba(0, 137, 123, 0.12);background-color:color-mix(in srgb, var(--color-primary, #00897b) 14%, transparent)}.s-conditional-offer-tier:not(:last-child)::before{content:\"\";position:absolute;top:2.5rem;bottom:-1rem;inset-inline-start:1.125rem;width:1px;background-color:#d1d5db}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.w-6{width:1.5rem}.resize{resize:both}.gap-1{gap:0.25rem}.rounded{border-radius:var(--s-radius, 0.25rem)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.s-conditional-offer-root{margin-bottom:1.25rem;display:block;width:100%;min-width:0px;max-width:100%;border-radius:0.375rem;padding:0.75rem}@media (min-width: 768px){.s-conditional-offer-root{padding:1rem}}.s-conditional-offer-banner{display:flex;width:100%;min-width:0px;align-items:center;justify-content:space-between;gap:0.75rem}@media (min-width: 768px){.s-conditional-offer-banner{gap:1rem}}.s-conditional-offer-lead{display:flex;min-width:0px;flex:1 1 0%;align-items:center;gap:0.5rem}@media (min-width: 768px){.s-conditional-offer-lead{gap:0.75rem}}.s-conditional-offer-badges{display:flex;max-width:45%;flex-shrink:0;flex-wrap:nowrap;align-items:center;gap:0.25rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;scrollbar-width:thin}.s-conditional-offer-badge{display:inline-flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:11px;line-height:1;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-badge-met{background-color:var(--color-primary)}.s-conditional-offer-badge-unmet{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.s-conditional-offer-badge-link{width:0.5rem;flex-shrink:0;border-top-width:1px}.s-conditional-offer-badge-link-met{border-color:var(--color-primary)}.s-conditional-offer-badge-link-unmet{border-style:dashed;--tw-border-opacity:1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.s-conditional-offer-badge-icon{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-text{display:flex;min-width:0px;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-heading{display:flex;min-width:0px;align-items:center;gap:0.375rem}.s-conditional-offer-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-info{position:relative;display:inline-flex;flex-shrink:0;align-items:center}.s-conditional-offer-info-trigger{display:inline-flex;height:1rem;width:1rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-info-trigger:hover{color:var(--color-primary-dark)}.s-conditional-offer-info-icon{height:0.875rem;width:0.875rem;flex-shrink:0}.s-conditional-offer-message{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}@media (min-width: 768px){.s-conditional-offer-message{font-size:1rem;line-height:1.5rem}}.s-conditional-offer-toggle{align-items:center;justify-content:center;gap:0.375rem;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle:hover{color:var(--color-primary-dark)}.s-conditional-offer-toggle-inline{display:none;flex-shrink:0}@media (min-width: 768px){.s-conditional-offer-toggle-inline{display:inline-flex}}.s-conditional-offer-toggle-bar{margin-top:0.5rem;display:inline-flex;width:100%;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding-left:0.75rem;padding-right:0.75rem;padding-top:0.625rem;padding-bottom:0.625rem}@media (min-width: 768px){.s-conditional-offer-toggle-bar{display:none}}.s-conditional-offer-toggle-icon{font-size:1rem;line-height:1.5rem;line-height:1;transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle-icon-open{--tw-rotate:180deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.s-conditional-offer-panel{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-gifts-header{margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-gifts-icon{display:inline-flex;height:2rem;width:2rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:var(--color-primary);font-size:1rem;line-height:1.5rem;line-height:1;color:var(--color-primary)}.s-conditional-offer-gifts-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-gifts-list{margin:0px;display:flex;list-style-type:none;flex-direction:column;gap:0.5rem;padding:0px}.s-conditional-offer-gift{display:flex;align-items:center;gap:0.75rem;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));padding:0.5rem}.s-conditional-offer-gift-thumb{display:inline-flex;height:3.5rem;width:3.5rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.25rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-gift-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-gift-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:0.875rem;line-height:1.25rem;font-weight:700;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-gift-free{font-weight:700;--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-price{--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));text-decoration-line:line-through}.s-conditional-offer-gifts{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-picker{display:flex;width:100%;min-width:0px;flex-direction:column;gap:0.75rem}.s-conditional-offer-picker-header{display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-picker-counter{display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;border-color:var(--color-primary);font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-picker-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-picker-list{margin:0px;display:flex;list-style-type:none;gap:0.75rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;padding:0px;padding-bottom:0.25rem;scrollbar-width:thin}.s-conditional-offer-picker-card{display:flex;width:9rem;flex-shrink:0;flex-direction:column;gap:0.375rem;text-align:start}.s-conditional-offer-picker-thumb{display:inline-flex;height:7rem;width:100%;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.5rem;line-height:2rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-picker-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-picker-price-regular{font-weight:700;--tw-text-opacity:1;color:rgb(23 23 23 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-actions{margin-top:auto;display:flex;width:100%;align-items:center;gap:0.375rem}.s-conditional-offer-picker-add{display:inline-flex;height:2.25rem;flex:1 1 0%;align-items:center;justify-content:center;border-radius:0.375rem;border-width:1px;border-color:var(--color-primary);font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark);transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-picker-add:hover{background-color:var(--color-primary);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip{position:absolute;top:calc(100% + 0.75rem);left:50%;z-index:10;width:min(22rem, calc(100vw - 2rem));border-radius:0.5rem;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:1rem;text-align:start;--tw-shadow:5px 10px 30px #2B2D340D;--tw-shadow-colored:5px 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.s-conditional-offer-tooltip-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tooltip-description{margin-top:0.25rem;font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip-list{margin:0px;margin-top:0.75rem;display:flex;list-style-type:none;flex-direction:column;gap:1rem;padding:0px}.s-conditional-offer-tier{position:relative;display:flex;align-items:flex-start;gap:0.75rem}.s-conditional-offer-tier-icon{position:relative;display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:1rem;line-height:1.5rem;line-height:1}.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark)}.s-conditional-offer-tier-status{position:absolute;bottom:-0.125rem;display:inline-flex;height:1rem;width:1rem;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));inset-inline-start:-2px}.s-conditional-offer-tier-status-met{color:var(--color-primary-dark)}.s-conditional-offer-tier-status-unmet{--tw-text-opacity:1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-status-check{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-tier-status-lock{height:0.625rem;width:0.625rem;flex-shrink:0}.s-conditional-offer-tier-body{display:flex;min-width:0px;flex-direction:column;gap:0.125rem}.s-conditional-offer-tier-title{font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tier-text{font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-text-met{text-decoration-line:line-through}.s-conditional-offer-skeleton-text{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.375rem}";
921
980
 
922
981
  /** Root of every translation key this component uses. */
923
982
  const LANG = "blocks.cart_offer";
@@ -982,6 +1041,19 @@ const RELOCATION_ANCESTOR_LIMIT = 3;
982
1041
  * DOM with no cart in it. Past this deadline the full-width banner is the final answer.
983
1042
  */
984
1043
  const RELOCATION_WATCH_MS = 5000;
1044
+ /**
1045
+ * CP-2171 — how long the picker stays locked after an add succeeded when no fresh cart
1046
+ * lines arrive (a missed `cart::updated`). Long enough to cover the coalescing window and the
1047
+ * cart lookup; short enough that a lost event never leaves Add ignoring taps for long. Past it
1048
+ * the lock goes and the lines are read again — see `unlockOnStaleLines`.
1049
+ */
1050
+ const FREE_PRODUCT_UNLOCK_FALLBACK_MS = 5000;
1051
+ /** CP-2171 — how long the cart page fetch for new lines may take before falling back to a reload. */
1052
+ const CART_PAGE_FETCH_TIMEOUT_MS = 8000;
1053
+ /** `sessionStorage` key of the cart page reload guard (see `reloadForMissingCartLines`). */
1054
+ const CART_PAGE_RELOAD_KEY = 'salla-conditional-offer:reloaded-for';
1055
+ /** A theme's cart line forms. Product option forms (`.cart-options`) carry an `item-` id too, but are not lines. */
1056
+ const CART_LINE_FORMS = 'form[id^="item-"]:not(.cart-options form)';
985
1057
  /**
986
1058
  * Clear space kept between the ⓘ popover and the viewport edge when it is nudged back
987
1059
  * inside. Purely cosmetic — one gutter, both edges, both directions.
@@ -1014,10 +1086,32 @@ const EN_FALLBACKS = {
1014
1086
  details_title: 'Offer details',
1015
1087
  free_gift: 'Free Gift',
1016
1088
  free_products_title: 'Free products selected from the offer',
1089
+ // CP-5033 — the gift tier is reached but none of its gifts can be added.
1090
+ free_products_unavailable: 'The free products are currently unavailable',
1091
+ // CP-2171 — the free product picker.
1092
+ free_products_choose: 'Choose :count free products',
1093
+ // Its own key, not :count = 2: Arabic uses the dual form, with no number.
1094
+ free_products_choose_two: 'Choose 2 free products',
1095
+ free_product_choose_one: 'Choose your free product',
1096
+ // The banner line at the gift tier: "…Choose it/them now" while no gift is picked yet, then
1097
+ // the plain line once one is. Same one / two / :count split as the picker title.
1098
+ free_product_got_one: 'You got a free product',
1099
+ free_product_got_pick_one: 'You got a free product! Choose it now',
1100
+ free_products_got: 'You got :count free products',
1101
+ free_products_got_pick: 'You got :count free products! Choose them now',
1102
+ free_products_got_pick_two: 'You got 2 free products! Choose them now',
1103
+ free_products_got_two: 'You got 2 free products',
1104
+ // A picker tier's reward in "Add :remaining and get :reward" and the tooltip rows, instead of
1105
+ // the server's count-less "Free Gift". Same one / two / :count split.
1106
+ free_product_reward_one: 'a free product',
1107
+ free_products_reward: ':count free products',
1108
+ free_products_reward_two: '2 free products',
1017
1109
  guidance_conditions_unmet: 'Your cart doesn’t meet the conditions for the next reward yet',
1018
1110
  guidance_next: 'Add :remaining and get :reward',
1019
1111
  guidance_next_generic: 'Add :remaining to unlock the next reward',
1020
1112
  manage_offer: 'Manage offer',
1113
+ // The picker's toggle. Its own key, so offers without a picker keep master's "Manage offer".
1114
+ offer_toggle: 'Offer',
1021
1115
  tier_requirement: 'Spend :threshold and get :reward',
1022
1116
  tier_requirement_count: 'Buy :threshold and get :reward',
1023
1117
  tier_requirement_count_generic: 'Buy :threshold to unlock this offer',
@@ -1027,6 +1121,15 @@ const EN_FALLBACKS = {
1027
1121
  /** The gift price label reuses the platform's existing "free of charge" string. */
1028
1122
  const FREE_LABEL_KEY = 'pages.checkout.free_charge';
1029
1123
  const FREE_LABEL_FALLBACK = 'Free';
1124
+ /**
1125
+ * CP-2171 — the picker's add label is the platform's shared "Add", not a cart-offer key of its
1126
+ * own: the dictionaries keep one string per meaning.
1127
+ */
1128
+ const ADD_LABEL_KEY = 'common.elements.add';
1129
+ const ADD_LABEL_FALLBACK = 'Add';
1130
+ // The "Free products" panel title reuses the loyalty program's existing "Free products".
1131
+ const FREE_PRODUCTS_TITLE_KEY = 'pages.loyalty_program.free_product_title';
1132
+ const FREE_PRODUCTS_TITLE_FALLBACK = 'Free products';
1030
1133
  /**
1031
1134
  * Escape a merchant-controlled string before it is substituted into a translation
1032
1135
  * template that we then render with `innerHTML`.
@@ -1133,6 +1236,37 @@ const SallaConditionalOffer = class {
1133
1236
  this.canRender = true;
1134
1237
  /** Free-gift cart LINES belonging to this offer — the "ادارة العرض" card. */
1135
1238
  this.gifts = [];
1239
+ /** CP-2171 — every cart line (paid and free), for the picker's per-product state. */
1240
+ this.cartLines = [];
1241
+ /**
1242
+ * CP-2171 — whether `cartLines` came from a cart lookup that answered. Until then the
1243
+ * lines are `[]`: the "Free products" panel and the gift-pick banner line wait for it.
1244
+ */
1245
+ this.cartLinesLoaded = false;
1246
+ /** CP-2171 — the picker's product cards, keyed by ENCODED product id. */
1247
+ this.freeProductCards = {};
1248
+ /**
1249
+ * CP-2171 — the product (encoded id) an add is in flight for. Held until cart lines read
1250
+ * AFTER the add arrive, not just until the request ends: the cart refresh starts only after
1251
+ * the coalescing window, and a tap in between is ignored, so a double tap adds one unit.
1252
+ */
1253
+ this.pendingFreeProduct = null;
1254
+ /** `giftsGeneration` when the pending change succeeded; a cart load started after it unlocks. */
1255
+ this.pendingFreeProductSettledAt = null;
1256
+ this.pendingFreeProductTimer = null;
1257
+ /**
1258
+ * `giftsGeneration` when an add from the picker succeeded: the first cart lines read after it
1259
+ * are checked against the page (see `showMissingCartLines`). Kept apart from the
1260
+ * button lock on purpose: the lock's fallback can fire before those lines arrive, and the
1261
+ * page still has to show the new line when they do.
1262
+ */
1263
+ this.cartPageCheckAfter = null;
1264
+ /** A cart page fetch for missing lines is in flight; lines read meanwhile wait in `pendingCartPageLines`. */
1265
+ this.insertingCartLines = false;
1266
+ this.pendingCartPageLines = null;
1267
+ /** Re-reads of the cart after a failed lookup while a picker change waits; see `retryPickerCartLookup`. */
1268
+ this.pickerLookupRetries = 0;
1269
+ this.pickerLookupTimer = null;
1136
1270
  /**
1137
1271
  * How many LINES the cart holds — `null` while that is genuinely UNKNOWN.
1138
1272
  *
@@ -1398,6 +1532,9 @@ const SallaConditionalOffer = class {
1398
1532
  clearTimeout(this.cartRefreshTimer);
1399
1533
  this.cartRefreshTimer = null;
1400
1534
  }
1535
+ // A dead host has no picker to unlock; drop the fallback timer with the rest.
1536
+ this.unlockFreeProduct();
1537
+ this.clearPickerLookupRetry();
1401
1538
  // Likewise: the timer goes, `cartLookupRetryPending` stays.
1402
1539
  if (this.cartLookupRetryTimer !== null) {
1403
1540
  clearTimeout(this.cartLookupRetryTimer);
@@ -1887,6 +2024,8 @@ const SallaConditionalOffer = class {
1887
2024
  return;
1888
2025
  this.cartRefreshPending = false;
1889
2026
  this.refresh();
2027
+ // A new cart change starts a new picker retry episode: this pass asks again itself.
2028
+ this.clearPickerLookupRetry();
1890
2029
  refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
1891
2030
  }
1892
2031
  /**
@@ -1953,6 +2092,14 @@ const SallaConditionalOffer = class {
1953
2092
  const text = salla.lang.get(FREE_LABEL_KEY);
1954
2093
  return text && text !== FREE_LABEL_KEY ? text : FREE_LABEL_FALLBACK;
1955
2094
  }
2095
+ getAddLabel() {
2096
+ return this.sharedLabel(ADD_LABEL_KEY, ADD_LABEL_FALLBACK);
2097
+ }
2098
+ /** A platform-wide label (not `blocks.cart_offer.*`): its own text, or `fallback` when missing. */
2099
+ sharedLabel(key, fallback) {
2100
+ const text = salla.lang.get(key);
2101
+ return text && text !== key ? text : fallback;
2102
+ }
1956
2103
  /**
1957
2104
  * Read a `blocks.cart_offer.*` translation.
1958
2105
  *
@@ -2082,6 +2229,7 @@ const SallaConditionalOffer = class {
2082
2229
  // Re-asked now that `this.offer` is known, because the gift filter is keyed to
2083
2230
  // the offer id — the early pass in `load()` could only answer the COUNT.
2084
2231
  this.loadCart();
2232
+ this.loadFreeProductCards();
2085
2233
  }
2086
2234
  catch (error) {
2087
2235
  if (this.isSuperseded(generation))
@@ -2146,8 +2294,8 @@ const SallaConditionalOffer = class {
2146
2294
  */
2147
2295
  loadCart() {
2148
2296
  const generation = ++this.giftsGeneration;
2149
- return Promise.all([getCartFreeGiftItems(), getCartItemCount()])
2150
- .then(([items, count]) => {
2297
+ return Promise.all([getCartFreeGiftItems(), getCartItemCount(), getCartProductLines()])
2298
+ .then(([items, count, lines]) => {
2151
2299
  if (generation !== this.giftsGeneration || this.isDisconnected)
2152
2300
  return;
2153
2301
  /**
@@ -2161,6 +2309,8 @@ const SallaConditionalOffer = class {
2161
2309
  */
2162
2310
  if (this.cartRefreshPending)
2163
2311
  return;
2312
+ // Was this pass the cart, or a failed lookup's fallback? See below.
2313
+ const answered = hasAuthoritativeCartData();
2164
2314
  /**
2165
2315
  * NEVER walks a KNOWN count back to `null`. `count` is `null` whenever the
2166
2316
  * shared lookup could not answer — which, on the removal path, is every
@@ -2197,6 +2347,28 @@ const SallaConditionalOffer = class {
2197
2347
  this.deferredByEmptyCart = false;
2198
2348
  this.refresh();
2199
2349
  }
2350
+ /**
2351
+ * A failed lookup hands back the cached lines, or `[]` right after a cart
2352
+ * update — not the cart. Publishing them would show an added gift as Add
2353
+ * again (one more tap is a PAID unit) and end the post-change checks below
2354
+ * as though fresh lines had arrived. Keep what we had, and while a picker
2355
+ * change is waiting on real lines — or a shown picker has never had any, so
2356
+ * Add is still disabled — ask again: the count alone schedules no retry once
2357
+ * the cart event has supplied one. A cart with no items (no banner) asks
2358
+ * nothing more.
2359
+ *
2360
+ * An offer without a picker has none of this at stake and goes on as it always
2361
+ * did: its gift card must still follow a removed gift, and nothing would retry.
2362
+ */
2363
+ const hasPicker = this.getPickerStage(this.getStages()) !== null;
2364
+ const awaitsPickerChange = this.pendingFreeProductSettledAt !== null || this.cartPageCheckAfter !== null;
2365
+ if (!answered && (hasPicker || awaitsPickerChange)) {
2366
+ if (awaitsPickerChange || (!this.cartLinesLoaded && this.hasCartItems())) {
2367
+ this.retryPickerCartLookup();
2368
+ }
2369
+ return;
2370
+ }
2371
+ this.clearPickerLookupRetry();
2200
2372
  /**
2201
2373
  * The gift card is keyed to the offer, so it can only be filled once the
2202
2374
  * offer is known. The early pass fired from `load()` runs BEFORE
@@ -2206,6 +2378,16 @@ const SallaConditionalOffer = class {
2206
2378
  * one.
2207
2379
  */
2208
2380
  this.gifts = this.offer ? this.selectOwnGifts(items) : [];
2381
+ this.cartLines = this.offer ? lines : [];
2382
+ this.cartLinesLoaded = !!this.offer;
2383
+ // These lines were read after the pending change landed: the picker may act again.
2384
+ if (this.pendingFreeProductSettledAt !== null && generation > this.pendingFreeProductSettledAt) {
2385
+ this.unlockFreeProduct();
2386
+ }
2387
+ if (this.cartPageCheckAfter !== null && generation > this.cartPageCheckAfter) {
2388
+ this.cartPageCheckAfter = null;
2389
+ this.showMissingCartLines(lines);
2390
+ }
2209
2391
  })
2210
2392
  .catch(error => {
2211
2393
  this.safeLog('cart lookup failed', error);
@@ -2237,6 +2419,27 @@ const SallaConditionalOffer = class {
2237
2419
  this.loadCart();
2238
2420
  }, CART_LOOKUP_RETRY_DELAYS_MS[this.cartLookupRetries]);
2239
2421
  }
2422
+ /**
2423
+ * Ask for the cart again after a lookup that could not answer while a picker change is
2424
+ * waiting on real lines. Its own bounded episode, on the count retry's delays: that one
2425
+ * ends as soon as the cart event has supplied a count, which is exactly when this is needed.
2426
+ */
2427
+ retryPickerCartLookup() {
2428
+ if (this.pickerLookupTimer !== null || this.pickerLookupRetries >= CART_LOOKUP_RETRY_DELAYS_MS.length)
2429
+ return;
2430
+ this.pickerLookupTimer = setTimeout(() => {
2431
+ this.pickerLookupTimer = null;
2432
+ this.pickerLookupRetries++;
2433
+ this.loadCart();
2434
+ }, CART_LOOKUP_RETRY_DELAYS_MS[this.pickerLookupRetries]);
2435
+ }
2436
+ clearPickerLookupRetry() {
2437
+ this.pickerLookupRetries = 0;
2438
+ if (this.pickerLookupTimer === null)
2439
+ return;
2440
+ clearTimeout(this.pickerLookupTimer);
2441
+ this.pickerLookupTimer = null;
2442
+ }
2240
2443
  /**
2241
2444
  * End the retry episode: a lookup answered, or the cart changed under it. The count
2242
2445
  * goes back to zero so a LATER failure gets its own full set of attempts.
@@ -2293,7 +2496,7 @@ const SallaConditionalOffer = class {
2293
2496
  * an older backend — is measured by the offer-level value exactly as before.
2294
2497
  */
2295
2498
  getProgress(stages) {
2296
- return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value));
2499
+ return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value), stages.map(({ can_add_free }) => can_add_free));
2297
2500
  }
2298
2501
  /**
2299
2502
  * A tier's render key. `id` is the stable identity across refetches; the array
@@ -2350,6 +2553,15 @@ const SallaConditionalOffer = class {
2350
2553
  * why every caller has a reward-less sentence to fall back to.
2351
2554
  */
2352
2555
  getRewardText(discount) {
2556
+ // CP-2171 — Figma: "Add 140 and get 2 free products". The server's label has no count.
2557
+ if (this.isPickerTier(discount)) {
2558
+ const limit = Math.max(1, Math.floor(Number(discount.max_free_products) || 1));
2559
+ if (limit === 1)
2560
+ return this.trans('free_product_reward_one');
2561
+ if (limit === 2)
2562
+ return this.trans('free_products_reward_two');
2563
+ return this.trans('free_products_reward', { count: `${limit}` });
2564
+ }
2353
2565
  return (discount?.reward_label ?? '').trim();
2354
2566
  }
2355
2567
  /**
@@ -2371,7 +2583,13 @@ const SallaConditionalOffer = class {
2371
2583
  getGuidanceMessage(stages, progress) {
2372
2584
  if (progress.nextUnmetIndex === NO_STAGE_REACHED)
2373
2585
  return '';
2374
- const remainingValue = getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
2586
+ // CP-2171 — the engine's own gap for a picker tier, when the server sends it: `current_value`
2587
+ // counts the gift units the tier would free, so it can read as past the threshold while the
2588
+ // tier is still short ("conditions unmet" with no amount).
2589
+ const engineAmountNeeded = Number(stages[progress.nextUnmetIndex]?.amount_needed);
2590
+ const remainingValue = this.isPickerTier(stages[progress.nextUnmetIndex]) && Number.isFinite(engineAmountNeeded) && engineAmountNeeded > 0
2591
+ ? engineAmountNeeded
2592
+ : getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
2375
2593
  if (remainingValue <= 0)
2376
2594
  return this.trans('guidance_conditions_unmet');
2377
2595
  const remaining = this.formatRequirement(remainingValue);
@@ -2649,26 +2867,403 @@ const SallaConditionalOffer = class {
2649
2867
  }, innerHTML: this.getTierSentence(discount, index, progress) }))));
2650
2868
  }))));
2651
2869
  }
2870
+ /**
2871
+ * CP-2171 — the tier the free product picker belongs to: the first `free_product` tier that
2872
+ * carries a non-empty `free_products` list. `null` on an offer without one, which keeps the
2873
+ * read-only gift card exactly as before. An empty list (every listed product unavailable) is
2874
+ * treated as no picker: there is nothing to choose, so a "Choose N" header would be a dead end.
2875
+ */
2876
+ getPickerStage(stages) {
2877
+ return stages.find(stage => this.isPickerTier(stage)) ?? null;
2878
+ }
2879
+ isPickerTier(stage) {
2880
+ return stage?.type === DiscountType.FREE_PRODUCT && Array.isArray(stage.free_products) && stage.free_products.length > 0;
2881
+ }
2882
+ /**
2883
+ * CP-5033 — the furthest tier the cart's value reaches is a gift tier with none of its gifts
2884
+ * addable (the server lists only what can be added: out of stock, hidden and deleted are left
2885
+ * out), and the cart holds none of this offer's gifts. The engine pays nothing for that tier,
2886
+ * so neither "congrats" nor "conditions unmet" is true.
2887
+ */
2888
+ areGiftsUnavailable(stages, progress) {
2889
+ const stage = stages[progress.lastReachedIndex];
2890
+ return stage?.type === DiscountType.FREE_PRODUCT
2891
+ && Array.isArray(stage.free_products) && stage.free_products.length === 0
2892
+ && this.gifts.length === 0;
2893
+ }
2894
+ /**
2895
+ * Load the picker's product cards (name, image, price) — one `salla.product.fetch` for
2896
+ * the ids not loaded yet. Never rejects: a failed lookup leaves the cards out, not the banner.
2897
+ */
2898
+ loadFreeProductCards() {
2899
+ const stage = this.getPickerStage(this.getStages());
2900
+ const ids = (stage?.free_products ?? [])
2901
+ .map(ref => String(ref.id))
2902
+ .filter(id => id && !this.freeProductCards[id]);
2903
+ if (!ids.length)
2904
+ return Promise.resolve();
2905
+ const listedIds = new Set((stage.free_products ?? []).map(ref => String(ref.id)));
2906
+ return Promise.resolve()
2907
+ .then(() => salla.product.fetch({ source: 'selected', source_value: ids }))
2908
+ .then((response) => {
2909
+ if (this.isDisconnected)
2910
+ return;
2911
+ const cards = { ...this.freeProductCards };
2912
+ (Array.isArray(response?.data) ? response.data : []).forEach((product) => {
2913
+ const id = String(product?.id ?? '');
2914
+ if (!id || !listedIds.has(id))
2915
+ return;
2916
+ cards[id] = {
2917
+ id,
2918
+ name: String(product.name ?? ''),
2919
+ imageUrl: String(product.image?.url ?? ''),
2920
+ imageAlt: String(product.image?.alt ?? product.name ?? ''),
2921
+ price: Number.isFinite(Number(product.price)) ? Number(product.price) : null,
2922
+ };
2923
+ });
2924
+ this.freeProductCards = cards;
2925
+ })
2926
+ .catch(error => this.safeLog('free product lookup failed', error));
2927
+ }
2928
+ /**
2929
+ * The picker's view of the cart for this offer.
2930
+ *
2931
+ * - `freeCount`: products a line of THIS offer made free — the engine's own verdict, read
2932
+ * off the cart lines (`added_by_offer` + `offer_id`), never re-derived here. Gifts of other
2933
+ * offers (another cart offer, Buy X Get Y) are not counted against this offer's limit.
2934
+ * - `reached`: the tier's threshold is met. Read from `current_value` against `min_spend`,
2935
+ * NOT from `is_applied`: a free product tier is only "applied" once a listed product is
2936
+ * in the cart, so gating "Add" on it would never let the customer add the first one.
2937
+ */
2938
+ getPickerState(stage) {
2939
+ const allFreeLines = this.cartLines.filter(line => line.freeQuantity > 0);
2940
+ const freeLines = this.selectOwnFreeLines(allFreeLines);
2941
+ const free = new Set(freeLines.map(line => line.productId).filter(id => id !== null));
2942
+ const limit = Math.max(1, Math.floor(Number(stage.max_free_products) || 1));
2943
+ // The engine's answer when the server sends it; `current_value >= min_spend` is only a
2944
+ // fallback — it is gross of earlier discounts, so it can say "choose" while the engine
2945
+ // would still charge for the product.
2946
+ const value = Number(stage.current_value ?? this.offer?.details?.current_value ?? 0);
2947
+ const reached = typeof stage.can_add_free === 'boolean'
2948
+ ? stage.can_add_free
2949
+ : Number.isFinite(value) && value >= Number(stage.min_spend);
2950
+ return { limit, reached, freeCount: free.size };
2951
+ }
2952
+ /**
2953
+ * The free lines this offer granted — the SAME rule as `selectOwnGifts`, so the picker and
2954
+ * the gifts card never disagree: lines matching the offer id; only when none match, the
2955
+ * lines whose `offer_id` is unreadable.
2956
+ */
2957
+ selectOwnFreeLines(lines) {
2958
+ const offerId = Number(this.offer?.id);
2959
+ if (!Number.isFinite(offerId))
2960
+ return lines;
2961
+ const owned = lines.filter(line => line.offerId !== null && Number(line.offerId) === offerId);
2962
+ return owned.length ? owned : lines.filter(line => line.offerId === null);
2963
+ }
2964
+ /**
2965
+ * Add a free product to the cart — always from the cart page, never a redirect.
2966
+ *
2967
+ * Quick-add, exactly like the single free product was auto-added before CP-2171: a variant
2968
+ * (SKU) product gets its default SKU — `is_default` first, or the first in stock when stock
2969
+ * is per SKU (`AddToCart::addItemToCart()`) — with that SKU's option values. Any listed
2970
+ * product in the cart counts, so the engine then frees it. A failed add is left to the SDK,
2971
+ * which already shows the shopper its error notice.
2972
+ */
2973
+ addFreeProduct(card) {
2974
+ if (this.pendingFreeProduct)
2975
+ return;
2976
+ this.pendingFreeProduct = card.id;
2977
+ this.runFreeProductChange(() => Promise.resolve(salla.cart.addItem(card.id)).then(() => { this.cartPageCheckAfter = this.giftsGeneration; }), 'free product add failed');
2978
+ }
2979
+ /** Its own method so specs can observe it; the mock window cannot reload. */
2980
+ reloadPage() {
2981
+ window.location.reload();
2982
+ }
2983
+ /**
2984
+ * Its own method so specs can stub it; the mock window has no `fetch`. Aborted after
2985
+ * `CART_PAGE_FETCH_TIMEOUT_MS`: a fetch that never settles would hold `insertingCartLines`
2986
+ * for the page view, and every later line would only be queued behind it.
2987
+ */
2988
+ fetchCartPageHtml() {
2989
+ const controller = new AbortController();
2990
+ const timer = setTimeout(() => controller.abort(), CART_PAGE_FETCH_TIMEOUT_MS);
2991
+ // `no-store`: a cached copy of the page would lack the new line and force the reload.
2992
+ return fetch(window.location.href, { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'text/html' }, signal: controller.signal })
2993
+ .then(response => (response.ok ? response.text() : Promise.reject(new Error(`cart page ${response.status}`))))
2994
+ .finally(() => clearTimeout(timer));
2995
+ }
2996
+ /**
2997
+ * Show the cart lines an add from the picker created that the page has no `form#item-<id>`
2998
+ * for. Themes render cart lines on the server and only refresh lines already on the page, so
2999
+ * the new line (and its price) would otherwise appear only after a manual refresh.
3000
+ *
3001
+ * The cart page is fetched again and the missing line forms are inserted, in the server's
3002
+ * order — no reload. The fetched markup is fresh, so its prices need no update. Its inline
3003
+ * handlers and web components work as on a loaded page; only what the theme's own script wires
3004
+ * to a line at page load (e.g. raed's loading overlay on a quantity change) is missing.
3005
+ * Any failure (fetch, a line the fetched page lacks) falls back to `reloadForMissingCartLines`.
3006
+ *
3007
+ * Left alone on a page with no cart line forms at all (not a cart page this can reason about).
3008
+ * One fetch at a time; lines read meanwhile are checked again after it.
3009
+ */
3010
+ showMissingCartLines(lines) {
3011
+ if (this.insertingCartLines) {
3012
+ this.pendingCartPageLines = lines;
3013
+ return;
3014
+ }
3015
+ const missing = this.missingCartLineIds(lines);
3016
+ if (!missing.length)
3017
+ return;
3018
+ this.insertingCartLines = true;
3019
+ // Started inside the chain, so a synchronous throw (no `fetch`) still reaches `catch` and `finally`.
3020
+ Promise.resolve()
3021
+ .then(() => this.fetchCartPageHtml())
3022
+ .then(html => {
3023
+ if (this.isDisconnected || this.cartChangedDuringPageFetch())
3024
+ return;
3025
+ const stillMissing = this.missingCartLineIds(this.cartLines);
3026
+ if (stillMissing.length && !this.insertCartLineForms(html, stillMissing)) {
3027
+ this.reloadForMissingCartLines(stillMissing);
3028
+ }
3029
+ })
3030
+ .catch(error => {
3031
+ this.safeLog('cart page fetch for the new lines failed, reloading', error);
3032
+ if (this.isDisconnected || this.cartChangedDuringPageFetch())
3033
+ return;
3034
+ this.reloadForMissingCartLines(this.missingCartLineIds(this.cartLines));
3035
+ })
3036
+ .finally(() => {
3037
+ this.insertingCartLines = false;
3038
+ const pending = this.pendingCartPageLines;
3039
+ this.pendingCartPageLines = null;
3040
+ if (pending && !this.isDisconnected)
3041
+ this.showMissingCartLines(pending);
3042
+ });
3043
+ }
3044
+ /**
3045
+ * A picker change started while the cart page was fetched (e.g. a second add),
3046
+ * so neither the fetched page nor the cart lines can be trusted: inserting now could bring
3047
+ * back a line the cart no longer has. The answer is dropped and the cart read after that
3048
+ * change checks the page again (the change itself arms `cartPageCheckAfter` when it lands;
3049
+ * armed here too, for a change that fails and so arms nothing).
3050
+ *
3051
+ * Lines are always read from `cartLines` once the page arrives, never from the call's own
3052
+ * `lines`: a change that landed during the fetch has already replaced them there.
3053
+ */
3054
+ cartChangedDuringPageFetch() {
3055
+ // Lines not read since a change (the lock timed out on them) are no basis either.
3056
+ if (this.pendingFreeProduct === null && this.cartLinesLoaded)
3057
+ return false;
3058
+ if (this.cartPageCheckAfter === null)
3059
+ this.cartPageCheckAfter = this.giftsGeneration;
3060
+ return true;
3061
+ }
3062
+ /** Ids of `lines` the page has no form for, sorted; empty on a page with no cart line forms. */
3063
+ missingCartLineIds(lines) {
3064
+ try {
3065
+ if (!document.querySelector(CART_LINE_FORMS))
3066
+ return [];
3067
+ return lines.map(line => line.id).filter(id => !document.getElementById(`item-${id}`)).sort();
3068
+ }
3069
+ catch (error) {
3070
+ this.safeLog('cart page check after a free product add failed', error);
3071
+ return [];
3072
+ }
3073
+ }
3074
+ /**
3075
+ * Insert the `missing` line forms from the fetched cart page `html`, each right before the next
3076
+ * line the page already has (in the fetched order), else after the last one. Returns false —
3077
+ * nothing inserted — when the fetched page lacks any of them.
3078
+ */
3079
+ insertCartLineForms(html, missing) {
3080
+ try {
3081
+ const fetched = Array.from(new DOMParser().parseFromString(html, 'text/html').querySelectorAll(CART_LINE_FORMS));
3082
+ // `getAttribute`, not `form.id`: a line form holds an `<input name="id">`, which shadows the property.
3083
+ const fetchedIds = fetched.map(form => form.getAttribute('id'));
3084
+ if (missing.some(id => !fetchedIds.includes(`item-${id}`)))
3085
+ return false;
3086
+ fetched.forEach((form, index) => {
3087
+ const id = fetchedIds[index];
3088
+ if (!missing.includes(id.slice('item-'.length)) || document.getElementById(id))
3089
+ return;
3090
+ // Moved, not cloned: inserting adopts it into this document, and its web components
3091
+ // upgrade once connected. Its inline handlers (`onchange`, delete `onclick`) come along.
3092
+ const next = fetchedIds.slice(index + 1).map(nextId => document.getElementById(nextId)).find(Boolean);
3093
+ if (next) {
3094
+ next.parentNode.insertBefore(form, next);
3095
+ }
3096
+ else {
3097
+ const onPage = document.querySelectorAll(CART_LINE_FORMS);
3098
+ const last = onPage[onPage.length - 1];
3099
+ last.parentNode.insertBefore(form, last.nextSibling);
3100
+ }
3101
+ });
3102
+ return missing.every(id => document.getElementById(`item-${id}`));
3103
+ }
3104
+ catch (error) {
3105
+ this.safeLog('cart line insert failed', error);
3106
+ return false;
3107
+ }
3108
+ }
3109
+ /**
3110
+ * Reload once, when the missing lines could not be inserted. Never twice for the same missing
3111
+ * lines, so a line a theme never renders cannot loop the page. The guard lives in
3112
+ * `sessionStorage`; where storage is blocked the page still reloads, but never twice in a row.
3113
+ */
3114
+ reloadForMissingCartLines(missing) {
3115
+ if (!missing.length)
3116
+ return;
3117
+ const signature = missing.join(',');
3118
+ const stored = this.readReloadGuard();
3119
+ if (stored === signature)
3120
+ return;
3121
+ if (stored === undefined && this.pageWasReloaded())
3122
+ return;
3123
+ this.writeReloadGuard(signature);
3124
+ this.reloadPage();
3125
+ }
3126
+ /** The reload guard, or `undefined` when storage cannot be read (blocked, disabled). */
3127
+ readReloadGuard() {
3128
+ try {
3129
+ return window.sessionStorage.getItem(CART_PAGE_RELOAD_KEY);
3130
+ }
3131
+ catch {
3132
+ return undefined;
3133
+ }
3134
+ }
3135
+ writeReloadGuard(signature) {
3136
+ try {
3137
+ window.sessionStorage.setItem(CART_PAGE_RELOAD_KEY, signature);
3138
+ }
3139
+ catch {
3140
+ // Storage full or blocked: pageWasReloaded() is the guard on the next load.
3141
+ }
3142
+ }
3143
+ /** Whether this page load is itself a reload — the guard when storage is unavailable. */
3144
+ pageWasReloaded() {
3145
+ try {
3146
+ const navigation = window.performance?.getEntriesByType?.('navigation')?.[0];
3147
+ return navigation?.type === 'reload';
3148
+ }
3149
+ catch {
3150
+ return false;
3151
+ }
3152
+ }
3153
+ /**
3154
+ * Run one add. A failure unlocks at once — nothing changed. A success stays locked
3155
+ * until a cart load that started after it publishes (see `loadCart`), or the fallback fires.
3156
+ */
3157
+ runFreeProductChange(change, failure) {
3158
+ Promise.resolve()
3159
+ .then(change)
3160
+ .then(() => {
3161
+ if (this.isDisconnected)
3162
+ return this.unlockFreeProduct();
3163
+ this.pendingFreeProductSettledAt = this.giftsGeneration;
3164
+ this.pendingFreeProductTimer = setTimeout(() => this.unlockOnStaleLines(), FREE_PRODUCT_UNLOCK_FALLBACK_MS);
3165
+ })
3166
+ .catch(error => {
3167
+ this.safeLog(failure, error);
3168
+ this.unlockFreeProduct();
3169
+ // A failed change fires no cart event, so nothing else checks a cart page answer
3170
+ // that was dropped while this change was in flight.
3171
+ if (this.cartPageCheckAfter !== null)
3172
+ this.loadCart();
3173
+ });
3174
+ }
3175
+ /**
3176
+ * The lock's fallback: no cart lines read after the change have arrived, so the lines on
3177
+ * screen are the ones from BEFORE it. Release the lock, mark the lines unread and ask again
3178
+ * on a fresh read — `loadCart` retries within its bound while the picker has no lines.
3179
+ */
3180
+ unlockOnStaleLines() {
3181
+ this.unlockFreeProduct();
3182
+ this.cartLinesLoaded = false;
3183
+ this.clearPickerLookupRetry();
3184
+ refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
3185
+ }
3186
+ unlockFreeProduct() {
3187
+ if (this.pendingFreeProductTimer !== null)
3188
+ clearTimeout(this.pendingFreeProductTimer);
3189
+ this.pendingFreeProductTimer = null;
3190
+ this.pendingFreeProductSettledAt = null;
3191
+ this.pendingFreeProduct = null;
3192
+ }
3193
+ /**
3194
+ * CP-2171 — Figma's banner line at the gift tier: "You got N free products", with
3195
+ * "Choose them now" while none is picked yet. N is the tier's limit, not the count picked.
3196
+ */
3197
+ pickerGotMessage(limit, awaitsPick) {
3198
+ if (limit === 1)
3199
+ return this.trans(awaitsPick ? 'free_product_got_pick_one' : 'free_product_got_one');
3200
+ if (limit === 2)
3201
+ return this.trans(awaitsPick ? 'free_products_got_pick_two' : 'free_products_got_two');
3202
+ return this.trans(awaitsPick ? 'free_products_got_pick' : 'free_products_got', { count: `${limit}` });
3203
+ }
3204
+ getPickerCards(stage) {
3205
+ return (stage.free_products ?? [])
3206
+ .map(ref => this.freeProductCards[String(ref.id)])
3207
+ .filter(Boolean);
3208
+ }
3209
+ /** CP-2171 — Figma "Choose N free products": the counter, then one card per listed product. */
3210
+ renderPicker(stage) {
3211
+ const state = this.getPickerState(stage);
3212
+ const cards = this.getPickerCards(stage);
3213
+ // Cards still loading, or the lookup failed: no header and counter over an empty list.
3214
+ if (!cards.length)
3215
+ return null;
3216
+ return (h("div", { class: "s-conditional-offer-picker", "data-picker-reached": state.reached ? 'true' : 'false' }, h("div", { class: "s-conditional-offer-picker-header" }, h("span", { class: "s-conditional-offer-picker-counter", "aria-label": `${state.freeCount}/${state.limit}` }, `${state.freeCount}/${state.limit}`), h("span", { class: "s-conditional-offer-picker-title" }, state.limit === 1
3217
+ ? this.trans('free_product_choose_one')
3218
+ : state.limit === 2
3219
+ ? this.trans('free_products_choose_two')
3220
+ : this.trans('free_products_choose', { count: `${state.limit}` }))), h("ul", { class: "s-conditional-offer-picker-list" }, cards.map(card => this.renderPickerCard(card)))));
3221
+ }
3222
+ /**
3223
+ * One picker card: the product, its normal price and Add — the same on every card, whether
3224
+ * the product is in the cart, free or not. Add is never disabled: every tap adds one unit at
3225
+ * the normal price (also once the product is in the cart, and after the free limit is used
3226
+ * up) and the engine decides what is free. What IS free shows in the "Free products" panel
3227
+ * below, never on the card. A tap while the previous add is still in flight is ignored
3228
+ * (`addFreeProduct`), so one double tap is one unit.
3229
+ *
3230
+ * No remove: the picker never deletes a cart line. A free line can also hold paid units, and
3231
+ * deleting it would take what the customer bought with the gift — products leave the cart
3232
+ * from their own cart line only.
3233
+ */
3234
+ renderPickerCard(card) {
3235
+ return (h("li", { class: "s-conditional-offer-picker-card", "data-product-id": card.id }, h("span", { class: "s-conditional-offer-picker-thumb" }, card.imageUrl ? (h("img", { class: "s-conditional-offer-picker-image", src: card.imageUrl, alt: card.imageAlt || card.name, loading: "lazy", decoding: "async" })) : (h("i", { class: "sicon-gift", "aria-hidden": "true" }))), h("span", { class: "s-conditional-offer-picker-name" }, card.name), card.price !== null ? (h("span", { class: "s-conditional-offer-picker-prices" }, h("span", { class: "s-conditional-offer-picker-price-regular", innerHTML: salla.money(card.price) }))) : null, h("span", { class: "s-conditional-offer-picker-actions" }, h("button", { type: "button", class: "s-conditional-offer-picker-add", onClick: () => this.addFreeProduct(card) }, this.getAddLabel()))));
3236
+ }
2652
3237
  /**
2653
3238
  * "ادارة العرض". Rendered TWICE with complementary responsive visibility rather than
2654
3239
  * moved with `order:`: desktop wants it inline with the banner row, mobile wants a
2655
3240
  * full-width bar BELOW the gifts card, and those are different places
2656
3241
  * in the flow, not a different order within one flex line.
2657
3242
  */
2658
- renderGiftsToggle(variant) {
3243
+ renderGiftsToggle(variant, hasPicker) {
2659
3244
  return (h("button", { type: "button", id: this.elementId(`toggle-${variant}`), class: {
2660
3245
  's-conditional-offer-toggle': true,
2661
3246
  's-conditional-offer-toggle-inline': variant === 'inline',
2662
3247
  's-conditional-offer-toggle-bar': variant === 'bar',
2663
- }, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, h("span", null, this.trans('manage_offer')), h("i", { class: {
3248
+ }, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, h("span", null, this.trans(hasPicker ? 'offer_toggle' : 'manage_offer')), h("i", { class: {
2664
3249
  'sicon-keyboard_arrow_down': true,
2665
3250
  's-conditional-offer-toggle-icon': true,
2666
3251
  's-conditional-offer-toggle-icon-open': this.isGiftsOpen,
2667
3252
  }, "aria-hidden": "true" })));
2668
3253
  }
2669
- /** Spec §5 — the gifts card: one read-only row per gift line. */
2670
- renderGiftsCard() {
2671
- return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.trans('free_products_title'))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift)))));
3254
+ /**
3255
+ * Spec §5 — the gifts card: one row per gift line. Read-only on an offer without a picker,
3256
+ * exactly as before; with the CP-2171 picker it becomes the Figma "Free products" panel,
3257
+ * shown only once a gift is in the cart. Its rows are read-only, as are the picker cards: a
3258
+ * gift leaves the cart from its own cart line.
3259
+ */
3260
+ renderGiftsCard(pickerStage = null) {
3261
+ // No picker: the card exactly as before CP-2171 — no wrapper, so no new box styling.
3262
+ if (!pickerStage) {
3263
+ return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.trans('free_products_title'))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift)))));
3264
+ }
3265
+ const state = this.getPickerState(pickerStage);
3266
+ return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, state.reached ? this.renderPicker(pickerStage) : null, this.gifts.length ? (h("div", { class: "s-conditional-offer-gifts" }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.sharedLabel(FREE_PRODUCTS_TITLE_KEY, FREE_PRODUCTS_TITLE_FALLBACK))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift))))) : null));
2672
3267
  }
2673
3268
  renderGiftRow(gift) {
2674
3269
  return (h("li", { class: "s-conditional-offer-gift", "data-item-id": gift.id }, h("span", { class: "s-conditional-offer-gift-thumb" }, gift.imageUrl ? (h("img", { class: "s-conditional-offer-gift-image", src: gift.imageUrl, alt: gift.imageAlt || gift.name, loading: "lazy", decoding: "async" })) : (h("i", { class: "sicon-gift", "aria-hidden": "true" }))), h("div", { class: "s-conditional-offer-gift-body" }, h("span", { class: "s-conditional-offer-gift-name" }, gift.name || this.trans('free_gift')), h("span", { class: "s-conditional-offer-gift-prices" }, h("span", { class: "s-conditional-offer-gift-free" }, this.getFreeLabel()), gift.originalPrice !== null ? (
@@ -2715,12 +3310,26 @@ const SallaConditionalOffer = class {
2715
3310
  const progress = this.getProgress(stages);
2716
3311
  const guidance = this.getGuidanceMessage(stages, progress);
2717
3312
  const detailsTitle = this.trans('details_title');
2718
- // Spec §2 — the accordion exists only when there is something to manage.
2719
- const hasGifts = this.gifts.length > 0;
2720
- return (h(Host, { class: "s-conditional-offer-root" }, h("div", { class: "s-conditional-offer-banner" }, h("div", { class: "s-conditional-offer-lead" }, this.renderBadges(stages, progress), h("div", { class: "s-conditional-offer-text" }, h("div", { class: "s-conditional-offer-heading", onMouseEnter: () => (this.isTooltipHovered = true), onMouseLeave: () => (this.isTooltipHovered = false) }, h("span", { class: "s-conditional-offer-name" }, this.offer.title), h("span", { class: "s-conditional-offer-info", ref: (el) => (this.infoAnchorEl = el) }, h("button", { type: "button", class: "s-conditional-offer-info-trigger", ref: (el) => (this.infoTriggerEl = el), "aria-label": detailsTitle, title: detailsTitle, "aria-expanded": this.isTooltipOpen ? 'true' : 'false', "aria-describedby": this.elementId('tooltip'), onClick: () => this.toggleTooltip() }, h(InformationIcon, { class: "s-conditional-offer-info-icon" })), this.isTooltipOpen ? this.renderTooltip(stages, progress) : null)), progress.allMet ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('congrats'))) : guidance ? (
3313
+ // Spec §2 — the accordion exists only when there is something to manage: gift lines,
3314
+ // or (CP-2171) a free product picker — shown only once the customer has reached its
3315
+ // stage, then before anything is picked too.
3316
+ const pickerStage = this.getPickerStage(stages);
3317
+ const pickerState = pickerStage !== null ? this.getPickerState(pickerStage) : null;
3318
+ const isPickerReached = pickerState?.reached ?? false;
3319
+ const hasGifts = this.gifts.length > 0 || (isPickerReached && this.getPickerCards(pickerStage).length > 0);
3320
+ // The gift tier is reached but none of this offer's gifts is in the cart: the engine
3321
+ // grants that tier only once one is, and pays nothing until then (no fallback to a lower
3322
+ // tier). So neither "congrats" nor "conditions unmet" is true — ask for the pick. Only
3323
+ // once the cart lines are read, or a cart that holds the gift would flash this line.
3324
+ const awaitsGiftPick = isPickerReached && this.cartLinesLoaded && pickerState.freeCount === 0;
3325
+ // "You got N free products" replaces congrats only when the gift tier is the offer's last:
3326
+ // below a higher tier, `guidance` (or congrats for that higher tier) is the truer line.
3327
+ const gotGifts = progress.allMet && isPickerReached && pickerState.freeCount > 0
3328
+ && stages[stages.length - 1] === pickerStage;
3329
+ return (h(Host, { class: "s-conditional-offer-root" }, h("div", { class: "s-conditional-offer-banner" }, h("div", { class: "s-conditional-offer-lead" }, this.renderBadges(stages, progress), h("div", { class: "s-conditional-offer-text" }, h("div", { class: "s-conditional-offer-heading", onMouseEnter: () => (this.isTooltipHovered = true), onMouseLeave: () => (this.isTooltipHovered = false) }, h("span", { class: "s-conditional-offer-name" }, this.offer.title), h("span", { class: "s-conditional-offer-info", ref: (el) => (this.infoAnchorEl = el) }, h("button", { type: "button", class: "s-conditional-offer-info-trigger", ref: (el) => (this.infoTriggerEl = el), "aria-label": detailsTitle, title: detailsTitle, "aria-expanded": this.isTooltipOpen ? 'true' : 'false', "aria-describedby": this.elementId('tooltip'), onClick: () => this.toggleTooltip() }, h(InformationIcon, { class: "s-conditional-offer-info-icon" })), this.isTooltipOpen ? this.renderTooltip(stages, progress) : null)), awaitsGiftPick || gotGifts ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.pickerGotMessage(pickerState.limit, awaitsGiftPick))) : this.areGiftsUnavailable(stages, progress) ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('free_products_unavailable'))) : progress.allMet ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('congrats'))) : guidance ? (
2721
3330
  // innerHTML: the `:remaining` half carries `salla.money()` markup;
2722
3331
  // the reward half is escaped in `getGuidanceMessage`.
2723
- h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline') : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard() : null, hasGifts ? this.renderGiftsToggle('bar') : null));
3332
+ h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline', !!pickerStage) : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard(pickerStage) : null, hasGifts ? this.renderGiftsToggle('bar', !!pickerStage) : null));
2724
3333
  }
2725
3334
  get hostEl() { return getElement(this); }
2726
3335
  };