@wix/himalaya-cli 0.1582.0 → 0.1584.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +8 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +26 -2
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +18 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +2 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/comments-v1/src/extensions.d.ts +2 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +3 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/extensions.d.ts +15 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/loyalty/src/extensions.d.ts +13 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +19 -4
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +5 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +5 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +3 -0
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +4 -2
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-groups.mjs +5 -0
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-loyalty.mjs +4 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +10 -3
  17. package/dist/cli.mjs +1098 -702
  18. package/dist/preview-assets.generated.bin +0 -0
  19. package/fixtures/wix-canned.json +25 -0
  20. package/guides/component-reference.json +4 -3
  21. package/guides/component-reference.md +24 -2
  22. package/guides/one-app-to-himi-app.md +52 -1
  23. package/guides/sdk-reference.json +37 -9
  24. package/guides/wix-api-usage.md +9 -0
  25. package/package.json +2 -2
  26. package/templates/beauty-salon/config.ts +27 -1
  27. package/templates/beauty-salon/dev/index.ts +2285 -757
  28. package/templates/beauty-salon/himalaya.content.json +3 -0
  29. package/templates/beauty-salon/tier5-src/_shared/booking-location.ts +147 -0
  30. package/templates/beauty-salon/tier5-src/_shared/booking-session.ts +222 -0
  31. package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  32. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +19 -3
  33. package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +2 -1
  34. package/templates/beauty-salon/tier5-src/_shared/owner-editor-value.ts +164 -0
  35. package/templates/beauty-salon/tier5-src/_shared/store-product-info.ts +50 -0
  36. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +21 -8
  37. package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +277 -14
  38. package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +351 -92
  39. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +25 -5
  40. package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
  41. package/templates/beauty-salon/tier5-src/himi-branded-lite-category/index.ts +3 -2
  42. package/templates/beauty-salon/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  43. package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  44. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +14 -3
  45. package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  46. package/templates/beauty-salon/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  47. package/templates/beauty-salon-owner/dev/index.ts +129 -66
  48. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +19 -3
  49. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  50. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  51. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  52. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  53. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  54. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  55. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
  56. package/templates/branded-lite/config.ts +29 -1
  57. package/templates/branded-lite/dev/index.ts +8371 -5565
  58. package/templates/branded-lite/himalaya.content.json +3 -0
  59. package/templates/branded-lite/l10n/en.json +2 -2
  60. package/templates/branded-lite/tier5-src/_shared/booking-location.ts +147 -0
  61. package/templates/branded-lite/tier5-src/_shared/booking-session.ts +222 -0
  62. package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  63. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +19 -3
  64. package/templates/branded-lite/tier5-src/_shared/event-registration.ts +86 -0
  65. package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +2 -1
  66. package/templates/branded-lite/tier5-src/_shared/owner-editor-value.ts +164 -0
  67. package/templates/branded-lite/tier5-src/_shared/store-product-info.ts +50 -0
  68. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +21 -8
  69. package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +277 -14
  70. package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +351 -92
  71. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +25 -5
  72. package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
  73. package/templates/branded-lite/tier5-src/himi-branded-lite-badges/index.ts +2 -1
  74. package/templates/branded-lite/tier5-src/himi-branded-lite-category/index.ts +3 -2
  75. package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +4 -3
  76. package/templates/branded-lite/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
  77. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +115 -49
  78. package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  79. package/templates/branded-lite/tier5-src/himi-branded-lite-podcasts/index.ts +2 -1
  80. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -1
  81. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -2
  82. package/templates/branded-lite/tier5-src/himi-branded-lite-program/facts.ts +52 -0
  83. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +21 -11
  84. package/templates/branded-lite/tier5-src/himi-branded-lite-video/index.ts +2 -1
  85. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +12 -86
  86. package/templates/branded-lite/tier5-src/himi-events-list/index.ts +47 -6
  87. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +13 -3
  88. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +3 -2
  89. package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +53 -37
  90. package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +100 -34
  91. package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  92. package/templates/branded-lite/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  93. package/templates/community-club/config.ts +36 -1
  94. package/templates/community-club/dev/index.ts +1930 -401
  95. package/templates/community-club/himalaya.content.json +6 -0
  96. package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  97. package/templates/community-club/tier5-src/_shared/collection-contract.ts +19 -3
  98. package/templates/community-club/tier5-src/_shared/event-registration.ts +86 -0
  99. package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +2 -1
  100. package/templates/community-club/tier5-src/himi-blog-post/index.ts +21 -8
  101. package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +4 -3
  102. package/templates/community-club/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
  103. package/templates/community-club/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  104. package/templates/community-club/tier5-src/himi-events-detail/index.ts +12 -86
  105. package/templates/community-club/tier5-src/himi-events-list/index.ts +47 -6
  106. package/templates/community-club/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  107. package/templates/community-club-owner/dev/index.ts +53 -1
  108. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +19 -3
  109. package/templates/community-club-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  110. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  111. package/templates/community-club-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  112. package/templates/community-club-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  113. package/templates/community-club-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  114. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  115. package/templates/community-club-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  116. package/templates/events-meetups/config.ts +36 -1
  117. package/templates/events-meetups/dev/index.ts +1930 -401
  118. package/templates/events-meetups/himalaya.content.json +6 -0
  119. package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  120. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +19 -3
  121. package/templates/events-meetups/tier5-src/_shared/event-registration.ts +86 -0
  122. package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +2 -1
  123. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +21 -8
  124. package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +4 -3
  125. package/templates/events-meetups/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
  126. package/templates/events-meetups/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  127. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +12 -86
  128. package/templates/events-meetups/tier5-src/himi-events-list/index.ts +47 -6
  129. package/templates/events-meetups/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  130. package/templates/events-meetups-owner/dev/index.ts +6 -0
  131. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +19 -3
  132. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  133. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  134. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  135. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  136. package/templates/gym-fitness/config.ts +27 -1
  137. package/templates/gym-fitness/dev/index.ts +2803 -844
  138. package/templates/gym-fitness/himalaya.content.json +3 -0
  139. package/templates/gym-fitness/tier5-src/_shared/booking-location.ts +147 -0
  140. package/templates/gym-fitness/tier5-src/_shared/booking-session.ts +222 -0
  141. package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  142. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +19 -3
  143. package/templates/gym-fitness/tier5-src/_shared/event-registration.ts +86 -0
  144. package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +2 -1
  145. package/templates/gym-fitness/tier5-src/_shared/owner-editor-value.ts +164 -0
  146. package/templates/gym-fitness/tier5-src/_shared/store-product-info.ts +50 -0
  147. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +21 -8
  148. package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +277 -14
  149. package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +351 -92
  150. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +25 -5
  151. package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
  152. package/templates/gym-fitness/tier5-src/himi-branded-lite-category/index.ts +3 -2
  153. package/templates/gym-fitness/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  154. package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  155. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +12 -86
  156. package/templates/gym-fitness/tier5-src/himi-events-list/index.ts +47 -6
  157. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +14 -3
  158. package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  159. package/templates/gym-fitness/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  160. package/templates/gym-fitness-owner/dev/index.ts +132 -66
  161. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +19 -3
  162. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  163. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  164. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  165. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  166. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  167. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  168. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  169. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  170. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  171. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
  172. package/templates/nonprofit-cause/config.ts +36 -1
  173. package/templates/nonprofit-cause/dev/index.ts +1925 -396
  174. package/templates/nonprofit-cause/himalaya.content.json +6 -0
  175. package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  176. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +19 -3
  177. package/templates/nonprofit-cause/tier5-src/_shared/event-registration.ts +86 -0
  178. package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +2 -1
  179. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +21 -8
  180. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +4 -3
  181. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group-about/index.ts +243 -0
  182. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  183. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  184. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +12 -86
  185. package/templates/nonprofit-cause/tier5-src/himi-events-list/index.ts +47 -6
  186. package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  187. package/templates/nonprofit-cause/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  188. package/templates/nonprofit-cause-owner/dev/index.ts +85 -65
  189. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +19 -3
  190. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-rich-guard.ts +27 -0
  191. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  192. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  193. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  194. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  195. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  196. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  197. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
  198. package/templates/owner-lite/dev/index.ts +132 -66
  199. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +19 -3
  200. package/templates/owner-lite/tier5-src/himi-owner-lite-event/index.ts +16 -8
  201. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  202. package/templates/owner-lite/tier5-src/himi-owner-lite-events/index.ts +4 -2
  203. package/templates/owner-lite/tier5-src/himi-owner-lite-post/index.ts +7 -0
  204. package/templates/owner-lite/tier5-src/himi-owner-lite-product/index.ts +39 -6
  205. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  206. package/templates/owner-lite/tier5-src/himi-owner-lite-products/index.ts +7 -4
  207. package/templates/owner-lite/tier5-src/himi-owner-lite-service/index.ts +38 -7
  208. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  209. package/templates/owner-lite/tier5-src/himi-owner-lite-services/index.ts +4 -2
  210. package/templates/podcast-creator/config.ts +27 -1
  211. package/templates/podcast-creator/dev/index.ts +1051 -158
  212. package/templates/podcast-creator/himalaya.content.json +3 -0
  213. package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  214. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +19 -3
  215. package/templates/podcast-creator/tier5-src/_shared/event-registration.ts +86 -0
  216. package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +2 -1
  217. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +21 -8
  218. package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +4 -3
  219. package/templates/podcast-creator/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  220. package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  221. package/templates/podcast-creator/tier5-src/himi-branded-lite-podcasts/index.ts +2 -1
  222. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +12 -86
  223. package/templates/podcast-creator/tier5-src/himi-events-list/index.ts +47 -6
  224. package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  225. package/templates/podcast-creator/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  226. package/templates/podcast-creator-owner/dev/index.ts +53 -1
  227. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +19 -3
  228. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  229. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  230. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  231. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  232. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  233. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  234. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  235. package/templates/portfolio-creative/config.ts +27 -1
  236. package/templates/portfolio-creative/dev/index.ts +1305 -468
  237. package/templates/portfolio-creative/himalaya.content.json +3 -0
  238. package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  239. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +19 -3
  240. package/templates/portfolio-creative/tier5-src/_shared/event-registration.ts +86 -0
  241. package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +2 -1
  242. package/templates/portfolio-creative/tier5-src/_shared/owner-editor-value.ts +164 -0
  243. package/templates/portfolio-creative/tier5-src/_shared/store-product-info.ts +50 -0
  244. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +21 -8
  245. package/templates/portfolio-creative/tier5-src/himi-branded-lite-category/index.ts +3 -2
  246. package/templates/portfolio-creative/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  247. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -1
  248. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -2
  249. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +12 -86
  250. package/templates/portfolio-creative/tier5-src/himi-events-list/index.ts +47 -6
  251. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +14 -3
  252. package/templates/portfolio-creative/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  253. package/templates/portfolio-creative-owner/dev/index.ts +129 -66
  254. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +19 -3
  255. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  256. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  257. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  258. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  259. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  260. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  261. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
  262. package/templates/restaurant/config.ts +27 -1
  263. package/templates/restaurant/dev/index.ts +1051 -158
  264. package/templates/restaurant/himalaya.content.json +3 -0
  265. package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  266. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +19 -3
  267. package/templates/restaurant/tier5-src/_shared/event-registration.ts +86 -0
  268. package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +2 -1
  269. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +21 -8
  270. package/templates/restaurant/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  271. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +12 -86
  272. package/templates/restaurant/tier5-src/himi-events-list/index.ts +47 -6
  273. package/templates/restaurant/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  274. package/templates/restaurant-owner/dev/index.ts +132 -66
  275. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +19 -3
  276. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  277. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  278. package/templates/restaurant-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  279. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  280. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  281. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  282. package/templates/restaurant-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  283. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  284. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  285. package/templates/restaurant-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
  286. package/templates/retail-store/config.ts +27 -1
  287. package/templates/retail-store/dev/index.ts +597 -191
  288. package/templates/retail-store/himalaya.content.json +3 -0
  289. package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  290. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +19 -3
  291. package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +2 -1
  292. package/templates/retail-store/tier5-src/_shared/owner-editor-value.ts +164 -0
  293. package/templates/retail-store/tier5-src/_shared/store-product-info.ts +50 -0
  294. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +21 -8
  295. package/templates/retail-store/tier5-src/himi-branded-lite-category/index.ts +3 -2
  296. package/templates/retail-store/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  297. package/templates/retail-store/tier5-src/himi-store-product/index.ts +13 -3
  298. package/templates/retail-store/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  299. package/templates/retail-store-owner/dev/index.ts +50 -1
  300. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +19 -3
  301. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  302. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product/index.ts +39 -6
  303. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +14 -8
  304. package/templates/retail-store-owner/tier5-src/himi-owner-lite-products/index.ts +7 -4
  305. package/templates/yoga-studio/config.ts +27 -1
  306. package/templates/yoga-studio/dev/index.ts +2996 -877
  307. package/templates/yoga-studio/himalaya.content.json +3 -0
  308. package/templates/yoga-studio/tier5-src/_shared/booking-location.ts +147 -0
  309. package/templates/yoga-studio/tier5-src/_shared/booking-session.ts +222 -0
  310. package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -1
  311. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +19 -3
  312. package/templates/yoga-studio/tier5-src/_shared/event-registration.ts +86 -0
  313. package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +2 -1
  314. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +21 -8
  315. package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +277 -14
  316. package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +351 -92
  317. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +25 -5
  318. package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +219 -220
  319. package/templates/yoga-studio/tier5-src/himi-branded-lite-notifsettings/index.ts +173 -0
  320. package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +2 -1
  321. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/facts.ts +52 -0
  322. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +21 -11
  323. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +12 -86
  324. package/templates/yoga-studio/tier5-src/himi-events-list/index.ts +47 -6
  325. package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +2 -1
  326. package/templates/yoga-studio/tier5-src/wix-spaces-whats-new/index.ts +2 -1
  327. package/templates/yoga-studio-owner/dev/index.ts +85 -65
  328. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +19 -3
  329. package/templates/yoga-studio-owner/tier5-src/_shared/owner-rich-guard.ts +27 -0
  330. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event/index.ts +16 -8
  331. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +16 -7
  332. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-events/index.ts +4 -2
  333. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post/index.ts +7 -0
  334. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service/index.ts +38 -7
  335. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +53 -7
  336. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-services/index.ts +4 -2
@@ -1,22 +1,32 @@
1
1
  // Tier-5 bundle: himi-booking — customer-facing services catalog.
2
2
  //
3
- // Visitors browse the site's Wix Bookings services in the business's own order (the
4
- // order of the site's Book Online page), narrowed by category chips and a name search.
5
- // Each row carries what the service record has: photo, name, tagline, a Class or
6
- // Course tag, and duration and price in the same words as the service screen. "Book"
7
- // shows only where online booking by time is possible. The worker exposes explicit
8
- // flags so the descriptor renders loading, empty, error and loaded states without raw
9
- // JavaScript conditions in the screen tree.
3
+ // Visitors browse every Wix Bookings service of the site, grouped by category under a small
4
+ // title, in the business's own order (the order of the site's Book Online page). The category
5
+ // chips are anchors, not filters: a tap scrolls the list to its category's title, and the chip
6
+ // of the category at the top of the list lights up while the member scrolls. A name search
7
+ // keeps the grouping. With two or more locations a pin limits the list to the services offered
8
+ // at one location; the choice is shared with the Agenda and Choose Time screens. Each row
9
+ // carries what the service record has: photo, name, tagline, a Class or Course tag, and
10
+ // duration and price in the same words as the service screen. "Book" shows only where online
11
+ // booking by time is possible. The worker exposes explicit flags so the descriptor renders
12
+ // loading, empty, error and loaded states without raw JavaScript conditions in the screen tree.
13
+ import { defineService } from "@himalaya/state";
10
14
  import { defineBackendCollection } from "../_shared/collection-service.js";
11
15
  import { COLLECTION_CHROME_MIN_ITEMS } from "../_shared/collection-contract.js";
12
16
  import { queryServiceCategories, queryServices, type WixService } from "@himalaya/wix-bookings";
13
17
  import { categoryName, courseDatesText, courseHasEnded, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
14
18
  import { formatServicePrice } from "../_shared/wix-service-price.js";
15
19
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
20
+ import { get as getAppInfo } from "@himalaya/app-info";
21
+ import { atLeast } from "../_shared/member-account-routes.js";
16
22
  import { initials } from "../_shared/himi-text.js";
17
23
  import { copyParam, copyTemplate } from "../_shared/himi-copy.js";
18
24
  import { offsetPage } from "../_shared/offset-page.js";
19
25
  import { wixMediaUrl } from "@himalaya/wix-media";
26
+ import {
27
+ distinctChoices, effectiveLocation, locationMenu, readSharedLocation, serviceLocationChoices, writeSharedLocation,
28
+ type LocationChoice, type LocationOptionRow, type SharedLocation,
29
+ } from "../_shared/booking-location.js";
20
30
 
21
31
  // Offline-first (data-offline): the services catalog is a public read-through —
22
32
  // paint the last-synced rows from the on-device store first (instant, works
@@ -52,6 +62,8 @@ interface Row {
52
62
  hasImage: boolean;
53
63
  noImage: boolean;
54
64
  category: string;
65
+ /** The category's identity: its Wix id, else "name:<name>"; "" for a service with no category. */
66
+ categoryKey: string;
55
67
  /** The category's place in the business's order; -1 when the record has none. */
56
68
  categoryOrder: number;
57
69
  /** The row's place in the business's order when it was read (the offline copy is sorted by it). */
@@ -74,8 +86,13 @@ interface Row {
74
86
  bookA11y: string;
75
87
  /** The row's spoken name: name, type tag, tagline and meta. */
76
88
  a11y: string;
89
+ /** Where the service is offered, as location choices (`serviceLocationChoices`). */
90
+ locations: LocationChoice[];
77
91
  }
78
92
 
93
+ /** A row of the bound list: a category's title, or a service under it. */
94
+ type ListRow = Row & { isSection: boolean; notSection: boolean; sectionTitle: string };
95
+
79
96
  interface CategoryChip { id: string; label: string }
80
97
 
81
98
  interface S {
@@ -87,8 +104,12 @@ interface S {
87
104
  loadingMore?: boolean;
88
105
  total?: number;
89
106
  fromCache?: boolean;
107
+ /** The services read (Branded Web prints `results` as it is): every location, no titles. */
90
108
  all: Row[];
91
109
  results: Row[];
110
+ /** What the Services list shows: each category's title row, then its services, at the chosen
111
+ * location. Title rows are `cat:<category id>`, service rows the service id. */
112
+ listRows: ListRow[];
92
113
  emptyText: string;
93
114
  countLabel: string;
94
115
  errored: boolean;
@@ -97,23 +118,39 @@ interface S {
97
118
  notLoading: boolean;
98
119
  hasResults: boolean;
99
120
  noEmpty: boolean;
121
+ /** One anchor chip per category the list shows, in the list's order. */
100
122
  categoryChips: CategoryChip[];
101
123
  hideCategories: boolean;
124
+ /** The chip of the category at the top of the list (or the one tapped last). */
125
+ activeCategory: string;
126
+ /** "<row id>|<n>": the list scrolls that row to the top when this changes; "" scrolls nothing. */
127
+ scrollTo: string;
102
128
  /** Loading with no rows to show: the skeleton stands in for the list. */
103
129
  hideRows: boolean;
104
- /** The no-match line: a search miss, or a chosen category with nothing in it. */
130
+ /** The no-match line of a search. */
105
131
  noMatchText: string;
106
- /** Which way out the no-match block offers: Clear Search (a query), else Show All. */
132
+ /** A search is typed: the no-match block offers Clear Search. */
107
133
  hasQuery: boolean;
108
134
  noQuery: boolean;
109
135
  /** The chips' spoken name, from the copy pack (a descriptor accessibilityLabel is not rewritten). */
110
136
  categoriesLabel: string;
111
- }
112
-
113
- function strOf(arg: unknown): string {
114
- if (typeof arg === "string") return arg;
115
- const a = arg as { value?: string; text?: string } | undefined;
116
- return a?.value ?? a?.text ?? "";
137
+ // The location picker (two or more locations only): `locationMenu()` in booking-location.ts.
138
+ locationOptions: LocationOptionRow[];
139
+ locationLabel: string;
140
+ locationA11y: string;
141
+ hasLocationMenu: boolean;
142
+ noLocationMenu: boolean;
143
+ locationActive: boolean;
144
+ noLocationActive: boolean;
145
+ locationSheetOpen: boolean;
146
+ /** A location is chosen and none of the services read is offered there. */
147
+ locationEmpty: boolean;
148
+ notLocationEmpty: boolean;
149
+ locationEmptyText: string;
150
+ /** The location this list was last limited to, kept with the state so a cold boot shows the
151
+ * same list before the shared choice is read again. */
152
+ chosenLocationId: string;
153
+ chosenLocationLabel: string;
117
154
  }
118
155
 
119
156
  /** The list's copy keys for the shared price, duration and course-date lines. */
@@ -183,6 +220,23 @@ function categoryOrder(value: unknown): number {
183
220
  return typeof order === "number" && Number.isFinite(order) ? order : -1;
184
221
  }
185
222
 
223
+ /** A category's identity: its id, else its name (a string category, a name-only reference). */
224
+ function categoryKeyOf(id: unknown, name: string): string {
225
+ if (!name) return "";
226
+ return typeof id === "string" && id ? id : `name:${name}`;
227
+ }
228
+
229
+ /** The service's category. A Wix Bookings v2 service has ONE category (`category`), so a service
230
+ * shows under its one category. Should a payload ever carry several (`categories`), the rule is
231
+ * the same on every surface: the first one only, never a copy of the row under each. */
232
+ function categoryOf(service: WixService): { key: string; name: string; order: number } {
233
+ const raw = service as { category?: unknown; categories?: unknown };
234
+ const first = raw.category ?? (Array.isArray(raw.categories) ? raw.categories[0] : undefined);
235
+ const name = categoryName(first);
236
+ const id = first && typeof first === "object" ? (first as { id?: unknown }).id : undefined;
237
+ return { key: categoryKeyOf(id, name), name, order: categoryOrder(first) };
238
+ }
239
+
186
240
  function typeTagOf(type: unknown): string {
187
241
  if (type === "CLASS") return copyParam("services.tagClass", "Class");
188
242
  if (type === "COURSE") return copyParam("services.tagCourse", "Course");
@@ -226,11 +280,13 @@ function norm(service: WixService): Row {
226
280
  const tagline = typeof service.tagLine === "string" ? service.tagLine.trim() : "";
227
281
  const canBook = service.onlineBooking?.enabled !== false && !isCourse;
228
282
  const request = service.onlineBooking?.requireManualApproval === true;
283
+ const category = categoryOf(service);
229
284
  return {
230
285
  id,
231
286
  name,
232
- category: categoryName(service.category),
233
- categoryOrder: categoryOrder(service.category),
287
+ category: category.name,
288
+ categoryKey: category.key,
289
+ categoryOrder: category.order,
234
290
  subtitle: bits.length ? bits.join(" · ") : "Choose a time",
235
291
  // WS-C — canonical param-less detail screenId; the descriptor row binds the
236
292
  // separate `id` field as a first-class nav param (no more baked suffixes).
@@ -250,6 +306,7 @@ function norm(service: WixService): Row {
250
306
  bookCta: bookCtaFor(request),
251
307
  bookA11y: bookLabel(name, request),
252
308
  a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
309
+ locations: serviceLocationChoices((service as { locations?: unknown }).locations),
253
310
  };
254
311
  }
255
312
 
@@ -263,6 +320,7 @@ function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
263
320
  const tagline = str(row.tagline);
264
321
  const meta = str(row.meta);
265
322
  const canBook = row.canBook === true;
323
+ const category = str(row.category);
266
324
  const courseSchedule = row.courseSchedule;
267
325
  const endedText = courseSchedule && courseHasEnded({ schedule: courseSchedule }, Date.now())
268
326
  ? copyParam("services.ended", "Ended", shownEnded || str(row.endedText) || "Ended") : "";
@@ -276,7 +334,8 @@ function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
276
334
  image,
277
335
  hasImage: !!image,
278
336
  noImage: !image,
279
- category: str(row.category),
337
+ category,
338
+ categoryKey: str(row.categoryKey) || categoryKeyOf("", category),
280
339
  categoryOrder: typeof row.categoryOrder === "number" ? row.categoryOrder : -1,
281
340
  tagline,
282
341
  typeTag,
@@ -287,50 +346,49 @@ function withRowDefaults(row: Partial<Row>, shownEnded?: string): Row {
287
346
  noBook: !canBook,
288
347
  bookCta: str(row.bookCta) || bookCtaFor(false),
289
348
  bookA11y: str(row.bookA11y) || bookLabel(name),
349
+ // Recomputed, not restored: a course that ended since the row was saved says so.
290
350
  a11y: spokenName({ name, typeTag, endedText, tagline, meta }),
351
+ locations: Array.isArray(row.locations) ? row.locations.filter((l) => !!l && typeof l.key === "string" && !!l.key) : [],
291
352
  };
292
353
  }
293
354
 
294
- // ---- Category chips ---------------------------------------------------------------
355
+ // ---- Categories -------------------------------------------------------------------
295
356
  // The site's categories, read once per worker in the background — never holding back the
296
- // services page — in the owner's order. When the read lands, the chips repaint. A failed read
297
- // is tried again at most once a minute. Until it succeeds, the categories of the latest rows
298
- // loaded with no filter and no query stand in, ordered by `category.sortOrder`.
299
- let siteCategories: string[] | undefined;
357
+ // services read — give the business's order of the groups. When the read lands, the list
358
+ // repaints. A failed read is tried again at most once a minute. Until it succeeds, the groups
359
+ // follow each row's `category.sortOrder` (the order the services read already sorts by).
360
+ interface SiteCategory { key: string; name: string }
361
+ let siteCategories: SiteCategory[] | undefined;
300
362
  let categoriesInFlight = false;
301
363
  let categoriesFailedAt = 0;
302
364
  const CATEGORY_RETRY_MS = 60_000;
303
- const rowCategories = new Map<string, number>();
304
365
  /** A restored Ended label bridges only the first live page after boot, because an app that sends no
305
366
  * copy pack gives no sign that none is coming: after it, current copy (pack or stock) decides. */
306
367
  let restoredEndedBridge = true;
307
368
 
308
- /** Repaint the chips only, on the state already shown. Re-deriving from the query controller
309
- * would publish its working state, which a revalidation deliberately holds back (a restored list
310
- * would flash to the skeleton, or shrink to page 1, until every page is back). */
369
+ /** The title row of the services with no category (last, and only beside other groups). */
370
+ const UNCATEGORIZED = "cat:none";
371
+ const sectionIdOf = (key: string): string => (key ? `cat:${encodeURIComponent(key)}` : UNCATEGORIZED);
372
+
373
+ /** Repaint on the state already shown. Re-deriving from the query controller would publish its
374
+ * working state, which a revalidation deliberately holds back (a restored list would flash to
375
+ * the skeleton, or shrink to page 1, until every page is back). */
311
376
  function republish(): void {
312
377
  (svc as unknown as { categoriesLanded?: () => unknown }).categoriesLanded?.();
313
378
  }
314
379
 
315
- function withChips(s: S): S {
316
- const collection = (s as unknown as { collection?: { filter?: unknown; query?: unknown; hideChrome?: unknown } }).collection;
317
- const filter = typeof collection?.filter === "string" ? collection.filter : "all";
318
- // Restored rows stand in for the categories when the read failed before any live page — only
319
- // rows of the whole list (no category, no query), or the stand-in would be one category.
320
- const whole = filter === "all" && !(typeof collection?.query === "string" && collection.query.trim());
321
- // Not while a load is out: the rows on screen may still be a previous search's.
322
- if (!siteCategories?.length && rowCategories.size === 0 && whole && s.isLoading !== true) rememberRowCategories((s.results ?? []).map((row) => withRowDefaults(row)));
323
- const chips = categoryChips(filter);
324
- return { ...s, categoryChips: chips, hideCategories: collection?.hideChrome === true || (chips.length < 3 && !filter.startsWith("value:")) };
325
- }
326
-
327
380
  function startCategoriesRead(): void {
328
381
  if (siteCategories || categoriesInFlight) return;
329
382
  if (categoriesFailedAt && Date.now() - categoriesFailedAt < CATEGORY_RETRY_MS) return;
330
383
  categoriesInFlight = true;
331
384
  queryServiceCategories().then((res) => {
332
- const names = (res.categories ?? []).map((category) => categoryName(category)).filter(Boolean);
333
- siteCategories = [...new Set(names)];
385
+ const out: SiteCategory[] = [];
386
+ for (const category of res.categories ?? []) {
387
+ const name = categoryName(category);
388
+ const key = categoryKeyOf(category.id, name);
389
+ if (key && !out.some((c) => c.key === key)) out.push({ key, name });
390
+ }
391
+ siteCategories = out;
334
392
  }, () => {
335
393
  categoriesFailedAt = Date.now();
336
394
  }).then(() => {
@@ -339,42 +397,173 @@ function startCategoriesRead(): void {
339
397
  });
340
398
  }
341
399
 
342
- function rememberRowCategories(rows: Row[]): void {
400
+ interface Section { id: string; title: string; rows: Row[] }
401
+
402
+ /** The rows grouped by their one category: the business's order of categories (the categories
403
+ * read, else each row's category sortOrder, else first seen), each group in the rows' own order,
404
+ * and the services with no category last. A category with no row has no group. */
405
+ function sectionsOf(rows: Row[]): Section[] {
406
+ const site = siteCategories ?? [];
407
+ const groups = new Map<string, Section & { rank: number; order: number; seen: number }>();
343
408
  for (const row of rows) {
344
- if (row.category && !rowCategories.has(row.category)) rowCategories.set(row.category, row.categoryOrder);
409
+ // A row that names a category the read also has joins it, whatever shape its reference had.
410
+ let index = row.categoryKey ? site.findIndex((c) => c.key === row.categoryKey) : -1;
411
+ if (index < 0 && row.category) index = site.findIndex((c) => c.name === row.category);
412
+ const key = index >= 0 ? site[index].key : row.categoryKey;
413
+ const id = sectionIdOf(key);
414
+ let group = groups.get(id);
415
+ if (!group) {
416
+ group = { id, title: index >= 0 ? site[index].name : row.category, rows: [], rank: index, order: row.categoryOrder, seen: groups.size };
417
+ groups.set(id, group);
418
+ }
419
+ group.rows.push(row);
345
420
  }
421
+ const place = (g: { id: string; rank: number; order: number; seen: number }): number[] =>
422
+ g.id === UNCATEGORIZED ? [2, 0, g.seen] : g.rank >= 0 ? [0, g.rank, g.seen] : [1, g.order < 0 ? Number.MAX_SAFE_INTEGER : g.order, g.seen];
423
+ return [...groups.values()]
424
+ .sort((a, b) => { const pa = place(a), pb = place(b); return pa[0] - pb[0] || pa[1] - pb[1] || pa[2] - pb[2]; })
425
+ .map(({ id, title, rows: groupRows }) => ({
426
+ id, title: id === UNCATEGORIZED ? copyParam("services.otherCategory", "Other services") : title, rows: groupRows,
427
+ }));
428
+ }
429
+
430
+ const BLANK_ROW: Omit<Row, "id"> = {
431
+ name: "", subtitle: "", detailScreenId: "", initial: "", image: "", hasImage: false, noImage: true,
432
+ category: "", categoryKey: "", categoryOrder: -1, tagline: "", typeTag: "", noTypeTag: true, endedText: "", meta: "",
433
+ canBook: false, noBook: true, bookCta: "", bookA11y: "", a11y: "", locations: [],
434
+ };
435
+
436
+ // ---- Anchors -------------------------------------------------------------------------
437
+ // A chip tap scrolls the list to its category's title row (`scrollTo`, a new "|n" each tap, so a
438
+ // second tap on the same chip scrolls again) and lights that chip. The list reports the row at
439
+ // its top when the MEMBER scrolls (`topItemChanged`); its category's chip lights up.
440
+ let activeCategory = "";
441
+ /** The member has tapped a chip or scrolled the list: the lit chip is theirs. Until then it is the
442
+ * first category, whatever order the rows arrived in — the catalog's first rows can come before
443
+ * the business's category order, and the first chip of THAT list stayed lit over the real top. */
444
+ let activeChosen = false;
445
+ let scrollTarget = "";
446
+ let jumps = 0;
447
+ let quietUntil = 0;
448
+ /** How long a jump the worker asked for takes to settle: a top-row report inside it is the jump's
449
+ * own (the renderers suppress those, this is the belt to their braces). */
450
+ const JUMP_SETTLE_MS = 900;
451
+
452
+ // ---- Location ------------------------------------------------------------------------
453
+ /** The member's choice (shared with Agenda and Choose Time through the on-device store). */
454
+ let chosenLocation: SharedLocation = { id: "", label: "" };
455
+ /** Every location the whole catalog is offered at: a search narrows the rows, never the menu. */
456
+ let catalogLocations: LocationChoice[] = [];
457
+ let locationSheet = false;
458
+
459
+ /** The first runtime whose LazyList jumps to a row (`scrollToItemPath`) and reports the row at its
460
+ * top (`onTopItemChange`). An older native binary ignores both, so its chips would light without
461
+ * moving the list: they are hidden there, and the grouped list with its titles stays. Web is served
462
+ * with the release's own renderer, so it always has them. */
463
+ export const ANCHORS_MIN_BINARY = "1.8.0";
464
+ let anchorsOk: boolean | undefined;
465
+ async function anchorsSupported(): Promise<boolean> {
466
+ const info = await getAppInfo().catch(() => undefined);
467
+ if (!info || (info.platform !== "ios" && info.platform !== "android" && info.platform !== "macos")) return true;
468
+ return atLeast(info.binaryVersion, ANCHORS_MIN_BINARY);
346
469
  }
347
470
 
348
- function categoryNames(): string[] {
349
- if (siteCategories?.length) return siteCategories;
350
- const listed = [...rowCategories.entries()].map(([name, order], index) => ({ name, order, index }));
351
- listed.sort((a, b) => (a.order < 0 ? Infinity : a.order) - (b.order < 0 ? Infinity : b.order) || a.index - b.index);
352
- return listed.map((entry) => entry.name);
471
+ async function readLocation(): Promise<void> {
472
+ chosenLocation = await readSharedLocation();
473
+ anchorsOk ??= await anchorsSupported();
353
474
  }
354
475
 
355
- function categoryChips(filter: string): CategoryChip[] {
356
- const names = categoryNames();
357
- const chosen = filter.startsWith("value:") ? filter.slice(6) : "";
358
- // The chosen category stays, so the customer can always go back to All.
359
- const all = chosen && !names.includes(chosen) ? [...names, chosen] : names;
360
- return [{ id: "all", label: copyParam("services.allCategories", "All") }, ...all.map((name) => ({ id: `value:${name}`, label: name }))];
476
+ function argOf(p: unknown, key: string): string {
477
+ const a = p as Record<string, unknown> | undefined;
478
+ const v = a?.[key] ?? (a?.args as Record<string, unknown> | undefined)?.[key] ?? (a?.params as Record<string, unknown> | undefined)?.[key];
479
+ return typeof v === "string" ? v.trim() : "";
361
480
  }
362
481
 
482
+ /** Everything the list shows, rebuilt from the rows read and the module's choices. */
483
+ function display(s: S): S {
484
+ const rows = Array.isArray(s.results) ? s.results : [];
485
+ const loading = s.isLoading === true;
486
+ const errored = s.errored === true;
487
+ const choices = catalogLocations;
488
+ const locationKey = effectiveLocation(choices, chosenLocation.id);
489
+ const shown = locationKey ? rows.filter((row) => (row.locations ?? []).some((l) => l.key === locationKey)) : rows;
490
+ const sections = sectionsOf(shown);
491
+ // Titles once there is something to tell apart: never over a list with no category at all.
492
+ const titled = sections.length > 1 || (sections.length === 1 && sections[0].id !== UNCATEGORIZED);
493
+ const listRows: ListRow[] = sections.flatMap((section) => [
494
+ ...(titled ? [{ ...BLANK_ROW, id: section.id, isSection: true, notSection: false, sectionTitle: section.title }] : []),
495
+ ...section.rows.map((row) => ({ ...row, isSection: false, notSection: true, sectionTitle: "" })),
496
+ ]);
497
+ const chips = titled && sections.length >= 2 ? sections.map((section) => ({ id: section.id, label: section.title })) : [];
498
+ // No chips (a reload in flight, a single group): the lit chip waits for the list to come back.
499
+ if (chips.length && (!activeChosen || !chips.some((chip) => chip.id === activeCategory))) activeCategory = chips[0]!.id;
500
+ const target = scrollTarget.split("|")[0];
501
+ if (scrollTarget && !listRows.some((row) => row.id === target)) scrollTarget = "";
502
+ // The same rule as the shared `collection.hideChrome`: no search or chips over a short,
503
+ // un-narrowed list, an empty source, or before the first rows arrive.
504
+ const narrowed = s.hasQuery === true;
505
+ const sourceEmpty = !loading && !errored && rows.length === 0 && !narrowed;
506
+ const hideChrome = (!narrowed && !s.nextCursor && rows.length < COLLECTION_CHROME_MIN_ITEMS) || sourceEmpty
507
+ || ((loading || errored) && rows.length === 0 && !narrowed);
508
+ const locationEmpty = !loading && !errored && !!locationKey && rows.length > 0 && shown.length === 0;
509
+ const menu = locationMenu(choices, chosenLocation.id);
510
+ return {
511
+ ...s,
512
+ listRows,
513
+ categoryChips: chips,
514
+ hideCategories: hideChrome || chips.length < 2 || anchorsOk === false,
515
+ activeCategory,
516
+ scrollTo: scrollTarget,
517
+ ...menu,
518
+ locationSheetOpen: locationSheet && menu.hasLocationMenu,
519
+ locationEmpty,
520
+ notLocationEmpty: !locationEmpty,
521
+ locationEmptyText: locationEmpty ? copyTemplate("services.locationEmptyText", "There are no services at {location}.", { location: menu.locationLabel }) : "",
522
+ chosenLocationId: chosenLocation.id,
523
+ chosenLocationLabel: chosenLocation.label,
524
+ };
525
+ }
526
+
527
+ /** The section a row of the list belongs to: a title row is its own; a service, its group's. */
528
+ function sectionOfRow(listRows: ListRow[], id: string): string {
529
+ let section = "";
530
+ for (const row of listRows) {
531
+ if (row.isSection) section = row.id;
532
+ if (row.id === id) return section;
533
+ }
534
+ return "";
535
+ }
536
+
537
+ const PAGE = 100;
538
+ /** Grouping and anchors need the whole catalog: every page is read up front, up to this many
539
+ * (1,000 services). A bigger catalog keeps a cursor, and the list pages the rest as before. */
540
+ const MAX_PAGES = 10;
541
+
542
+ // A cold boot takes these from `state`, never from the last run: a jump the last run asked for
543
+ // would scroll the restored list on its first frame, and a sheet would reopen.
544
+ const registerServices = ((name: string, def: Parameters<typeof defineService>[1]) =>
545
+ defineService(name, { ...def, notHydrated: ["scrollTo", "locationSheetOpen"], perVisit: ["scrollTo", "locationSheetOpen"] } as never)) as typeof defineService;
546
+
363
547
  const svc = defineBackendCollection<S, Row>("himiBookingServices", {
548
+ register: registerServices,
364
549
  restorePagesOnLoad: true,
365
550
  onLoad() {
366
551
  // Copy is stashed before this hook. Repaint restored labels immediately, even
367
552
  // when the background query holds the old page through a slow or failed read.
368
553
  (svc as unknown as { normalizeRestored?: () => unknown }).normalizeRestored?.();
369
554
  },
370
- state: { status: "loading", query: "", all: [], results: [], emptyText: "", countLabel: "", errored: false,
555
+ state: { status: "loading", query: "", all: [], results: [], listRows: [], emptyText: "", countLabel: "", errored: false,
371
556
  notErrored: true, isLoading: true, notLoading: false, hasResults: false, noEmpty: true,
372
- categoryChips: [], hideCategories: true, hideRows: false, noMatchText: "", hasQuery: false, noQuery: true, categoriesLabel: "" },
557
+ categoryChips: [], hideCategories: true, activeCategory: "", scrollTo: "", hideRows: false, noMatchText: "", hasQuery: false, noQuery: true,
558
+ categoriesLabel: "", locationOptions: [], locationLabel: "", locationA11y: "", hasLocationMenu: false, noLocationMenu: true,
559
+ locationActive: false, noLocationActive: true, locationSheetOpen: false, locationEmpty: false, notLocationEmpty: true,
560
+ locationEmptyText: "", chosenLocationId: "", chosenLocationLabel: "" },
373
561
  source: {
374
562
  id: row => row.id,
375
563
  errorMessage: () => copyParam("services.errorText", "Services could not be loaded."),
376
564
  async cached(criteria) {
377
565
  if (criteria.query || criteria.filter !== "all" || criteria.sort !== "default") return undefined;
566
+ await readLocation();
378
567
  return { items: await readCachedRows(), nextCursor: "", metadata: { fromCache: true } };
379
568
  },
380
569
  async fetch(criteria, cursor) {
@@ -386,25 +575,105 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
386
575
  const sort = criteria.sort === "default"
387
576
  ? [{ fieldName: "category.sortOrder", order: "ASC" as const }, { fieldName: "sortOrder", order: "ASC" as const }, { fieldName: "id", order: "ASC" as const }]
388
577
  : [{ fieldName: "name", order: criteria.sort === "za" ? "DESC" as const : "ASC" as const }, { fieldName: "id", order: "ASC" as const }];
389
- if (!cursor) startCategoriesRead();
390
- const res = await queryServices({ query: { filter, paging: { limit: 50, offset: Number(cursor || 0) }, sort } });
391
- const raw = res.services ?? [];
392
- const offset = Number(cursor || 0);
393
- const items = raw.filter(row => !!row.id).map((service, index) => ({ ...norm(service), position: offset + index }));
578
+ if (!cursor) {
579
+ startCategoriesRead();
580
+ // Read again on every load and appearance: Agenda or Choose Time may have changed it.
581
+ await readLocation();
582
+ // The menu offers the whole catalog's places, and a search or category read lists only its
583
+ // matches: a cold boot into a persisted search takes them from the device's copy of the list.
584
+ if (catalogLocations.length === 0 && (criteria.query || criteria.filter !== "all")) {
585
+ catalogLocations = distinctChoices((await readCachedRows()).flatMap((row) => row.locations));
586
+ }
587
+ }
588
+ const items: Row[] = [];
589
+ let offset = Number(cursor || 0);
590
+ let next = "";
591
+ let total: number | undefined;
592
+ for (let page = 0; page < MAX_PAGES; page++) {
593
+ let res;
594
+ try {
595
+ res = await queryServices({ query: { filter, paging: { limit: PAGE, offset }, sort } });
596
+ } catch (error) {
597
+ // A later page failing keeps the pages already read: the list pages the rest (and says
598
+ // so if it fails again). The first page failing is the read failing.
599
+ if (page === 0) throw error;
600
+ next = String(offset);
601
+ break;
602
+ }
603
+ const raw = res.services ?? [];
604
+ items.push(...raw.filter(row => !!row.id).map((service, index) => ({ ...norm(service), position: offset + index })));
605
+ const paged = offsetPage(String(offset), raw.length, PAGE, res.pagingMetadata);
606
+ next = paged.nextCursor;
607
+ total = paged.total ?? total;
608
+ if (!next) break;
609
+ offset = Number(next);
610
+ }
394
611
  // Only a read of the whole list in the business's order is saved: the offline copy is read
395
612
  // back as that list, and a narrowed or re-sorted read's positions are not positions in it.
396
613
  if (!criteria.query && !criteria.filter.startsWith("value:") && criteria.sort === "default") await writeCachedRows(items);
397
- return { items, ...offsetPage(cursor, raw.length, 50, res.pagingMetadata), metadata: { fromCache: false } };
614
+ return { items, nextCursor: next, ...(total === undefined ? {} : { total }), metadata: { fromCache: false } };
398
615
  },
399
616
  },
400
617
  actions: {
401
- categoriesLanded: (s) => withChips(s),
402
- // Normalize old snapshots and refresh Ended/copy before revalidation settles.
403
- // A missing `noBook` must never show Book on a restored course.
618
+ categoriesLanded: (s) => display(s),
619
+ // A snapshot saved by an older worker comes back without the new row fields; a missing
620
+ // `noBook` would show Book on a course until the live read. Give restored rows the defaults,
621
+ // and the restored list its groups and the location it was limited to.
404
622
  normalizeRestored: (s) => {
405
- const rows = Array.isArray(s.results) ? s.results : [];
406
- const fixed = rows.map((row) => withRowDefaults(row as Partial<Row>));
407
- return { ...s, results: fixed, all: fixed };
623
+ const rows = (Array.isArray(s.results) ? s.results : []).map((row) => withRowDefaults(row as Partial<Row>));
624
+ if (typeof s.chosenLocationId === "string" && s.chosenLocationId) {
625
+ chosenLocation = { id: s.chosenLocationId, label: typeof s.chosenLocationLabel === "string" ? s.chosenLocationLabel : "" };
626
+ }
627
+ if (!s.query) catalogLocations = distinctChoices(rows.flatMap((row) => row.locations));
628
+ return display({ ...s, results: rows, all: rows });
629
+ },
630
+ // A chip: scroll to its category and light it. Re-tapping the lit chip deselects it on every
631
+ // renderer (`selectedIds: []`): that is the same chip again, so it scrolls again.
632
+ scrollToCategory: (s, p) => {
633
+ const ids = (p as { selectedIds?: unknown } | undefined)?.selectedIds
634
+ ?? ((p as { args?: { selectedIds?: unknown } } | undefined)?.args?.selectedIds);
635
+ const picked = Array.isArray(ids) && typeof ids[0] === "string" ? ids[0] : activeCategory;
636
+ if (!(s.categoryChips ?? []).some((chip) => chip.id === picked)) return s;
637
+ activeCategory = picked;
638
+ activeChosen = true;
639
+ scrollTarget = `${picked}|${++jumps}`;
640
+ quietUntil = Date.now() + JUMP_SETTLE_MS;
641
+ return display(s);
642
+ },
643
+ // The row at the top of the list after the member scrolled: its category's chip lights up.
644
+ topItemChanged: (s, p) => {
645
+ if (Date.now() < quietUntil) return s;
646
+ const rows = Array.isArray(s.listRows) ? s.listRows : [];
647
+ // A search miss or an empty location has no category to follow.
648
+ if (!rows.some((row) => !row.isSection)) return s;
649
+ const section = sectionOfRow(rows, argOf(p, "id"));
650
+ if (!section || !(s.categoryChips ?? []).some((chip) => chip.id === section)) return s;
651
+ activeCategory = section;
652
+ activeChosen = true;
653
+ // The member has moved on from the last jump: a re-render never scrolls back to it.
654
+ scrollTarget = "";
655
+ return display(s);
656
+ },
657
+ openLocations: (s) => {
658
+ locationSheet = true;
659
+ return display(s);
660
+ },
661
+ closeLocations: (s) => {
662
+ if (!locationSheet) return s;
663
+ locationSheet = false;
664
+ return display(s);
665
+ },
666
+ // A row of the location sheet: "" is every location. Agenda and Choose Time read it too.
667
+ selectLocation: async (s, p) => {
668
+ const id = argOf(p, "locationId");
669
+ const choice = catalogLocations.find((c) => c.key === id);
670
+ chosenLocation = id && choice ? { id, label: choice.label } : { id: "", label: "" };
671
+ locationSheet = false;
672
+ scrollTarget = "";
673
+ // Not awaited (it never throws): the state returned is this dispatch's own, so no later commit
674
+ // — a revalidation landing during a native round trip — is overwritten by it.
675
+ void writeSharedLocation(chosenLocation);
676
+ return display(s);
408
677
  },
409
678
  },
410
679
  apply(s, page) {
@@ -415,27 +684,17 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
415
684
  const shown = new Map((restoredEndedBridge && Array.isArray(s.results) ? s.results : []).map((row) => [row.id, row.endedText]));
416
685
  const rows = page.items.map((item) => withRowDefaults(item, shown.get(item.id)));
417
686
  if (page.metadata?.fromCache === false && !loading && !errored) restoredEndedBridge = false;
418
- // The stand-in categories come from the accepted rows of the whole list (a superseded
419
- // response never reaches apply): a live page replaces them, the device's copy only fills a gap.
420
- if (!page.criteria.query && page.criteria.filter === "all" && rows.length > 0) {
421
- // Only a settled page: while a page reloads, the list may still hold the previous
422
- // (searched) rows under the new criteria.
423
- if (page.metadata?.fromCache !== true) { if (!loading) { rowCategories.clear(); rememberRowCategories(rows); } }
424
- else if (rowCategories.size === 0) rememberRowCategories(rows);
425
- }
426
- // The same rule as the shared `collection.hideChrome`: no search or chips over a short,
427
- // un-narrowed list, an empty source, or before the first rows arrive.
428
687
  const filtered = page.criteria.filter.startsWith("value:");
429
688
  const hasQuery = !!page.criteria.query.trim();
430
- const narrowed = hasQuery || filtered;
431
- const sourceEmpty = !loading && !errored && rows.length === 0 && !narrowed;
432
- const hideChrome = (!narrowed && !page.nextCursor && rows.length < COLLECTION_CHROME_MIN_ITEMS) || sourceEmpty
433
- || ((loading || errored) && rows.length === 0 && !narrowed);
434
- const chips = categoryChips(page.criteria.filter);
435
- return { ...s, status: loading ? "loading" : "ready", query: page.criteria.query, all: rows, results: rows,
436
- categoryChips: chips, hideCategories: hideChrome || (chips.length < 3 && !filtered), hideRows: loading && rows.length === 0,
437
- noMatchText: hasQuery ? copyParam("services.noMatchText", "No services match your search.")
438
- : copyParam("services.categoryEmptyText", "There are no services in this category."),
689
+ // The menu offers every location of the whole catalog: a settled live read replaces it, the
690
+ // device's copy only fills a gap (a superseded response never reaches apply).
691
+ if (!hasQuery && !filtered && rows.length > 0) {
692
+ const live = page.metadata?.fromCache !== true;
693
+ if ((live && !loading) || catalogLocations.length === 0) catalogLocations = distinctChoices(rows.flatMap((row) => row.locations));
694
+ }
695
+ return display({ ...s, status: loading ? "loading" : "ready", query: page.criteria.query, all: rows, results: rows,
696
+ hideRows: loading && rows.length === 0,
697
+ noMatchText: copyParam("services.noMatchText", "No services match your search."),
439
698
  hasQuery, noQuery: !hasQuery, categoriesLabel: copyParam("services.categoriesLabel", "Service categories"),
440
699
  nextCursor: page.nextCursor, complete: page.complete, total: page.total, loadingMore: page.loadingMore, pageError: page.pageError,
441
700
  fromCache: page.metadata?.fromCache === true, errored, notErrored: !errored, isLoading: loading, notLoading: !loading,
@@ -444,17 +703,17 @@ const svc = defineBackendCollection<S, Row>("himiBookingServices", {
444
703
  emptyText: errored ? copyParam("services.errorText", "Services could not be loaded.")
445
704
  : loading || rows.length > 0 ? ""
446
705
  : hasQuery ? copyParam("services.noMatchText", "No services match your search.")
447
- : filtered ? copyParam("services.categoryEmptyText", "There are no services in this category.")
448
- : copyParam("services.emptyText", "There are no services to book yet.") };
706
+ : copyParam("services.emptyText", "There are no services to book yet.") });
449
707
  },
450
708
  });
451
- // The old chrome could persist a "saved" filter (there is no Saved chip now) or an A-Z sort
452
- // (there is no Refine now): both go back to the business's own view before the first read.
709
+ // The old chrome could persist a "saved" filter (there is no Saved chip now), a category filter
710
+ // (the chips are anchors now, the list always shows every category) or an A-Z sort (there is no
711
+ // Refine now): all go back to the business's own whole view before the first read.
453
712
  type Retired = { getState?: () => { collection?: { filter?: unknown; sort?: unknown } };
454
713
  collectionFilter?: (arg: unknown) => unknown; collectionSort?: (arg: unknown) => unknown };
455
714
  const retired = svc as unknown as Retired;
456
715
  const persisted = retired.getState?.()?.collection;
457
- if (persisted?.filter === "saved") void retired.collectionFilter?.({ selectedIds: ["all"] });
716
+ if (typeof persisted?.filter === "string" && persisted.filter !== "all") void retired.collectionFilter?.({ selectedIds: ["all"] });
458
717
  if (typeof persisted?.sort === "string" && persisted.sort !== "default") void retired.collectionSort?.({ selectedId: "default" });
459
718
  svc.load();
460
719
  export { svc };