@wix/agent-skills 1.13.0 → 1.15.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 (250) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/skills/README.md +23 -0
  4. package/skills/wix-app/SKILL.md +10 -2
  5. package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +326 -0
  6. package/skills/wix-app/references/CODE_QUALITY.md +1 -0
  7. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +124 -115
  8. package/skills/wix-app/references/DASHBOARD_PAGE.md +3 -0
  9. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +23 -9
  10. package/skills/wix-app/references/SITE_PLUGIN.md +1 -1
  11. package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +115 -0
  12. package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +264 -0
  13. package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +37 -0
  14. package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +86 -0
  15. package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +119 -0
  16. package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +103 -0
  17. package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +76 -0
  18. package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +82 -0
  19. package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +89 -0
  20. package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +124 -0
  21. package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +91 -0
  22. package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +71 -0
  23. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +70 -0
  24. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +73 -0
  25. package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +98 -0
  26. package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +278 -0
  27. package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +94 -0
  28. package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +43 -0
  29. package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +66 -0
  30. package/skills/wix-app/references/auto-patterns-dashboard/views.md +76 -0
  31. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -14
  32. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +54 -29
  33. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +2 -2
  34. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +17 -105
  35. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +127 -0
  36. package/skills/wix-app/references/editor-react-component/PARTS.md +2 -1
  37. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +15 -15
  38. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +25 -53
  39. package/skills/wix-app/scripts/generate-auto-patterns.js +510 -0
  40. package/skills/wix-auth/SKILL.md +12 -0
  41. package/skills/wix-auth/references/device-flow.md +140 -0
  42. package/skills/wix-docs/SKILL.md +188 -0
  43. package/skills/wix-docs/references/API_SPEC_SEARCH.md +213 -0
  44. package/skills/wix-docs/references/EXTRACTING.md +120 -0
  45. package/skills/wix-headless/SKILL.md +84 -130
  46. package/skills/wix-headless/entry/bootstrap.mjs +93 -0
  47. package/skills/wix-headless/entry/skill.md +78 -0
  48. package/skills/wix-headless/references/AI_FEATURES.md +178 -0
  49. package/skills/wix-headless/references/CAPABILITIES.md +84 -0
  50. package/skills/wix-headless/references/DISCOVERY.md +34 -56
  51. package/skills/wix-headless/references/DOC_DISCOVERY.md +37 -0
  52. package/skills/wix-headless/references/FEEDBACK.md +118 -0
  53. package/skills/wix-headless/references/IMAGE_GENERATION.md +69 -0
  54. package/skills/wix-headless/references/SDK_HANDOFF.md +97 -0
  55. package/skills/wix-headless/references/SEED.md +44 -188
  56. package/skills/wix-headless/references/SETUP.md +32 -103
  57. package/skills/wix-headless/references/astro.md +98 -0
  58. package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +196 -0
  59. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +236 -0
  60. package/skills/wix-headless/references/inline-recipes/how-to-code-bookings.md +279 -0
  61. package/skills/wix-headless/references/inline-recipes/how-to-code-cms.md +163 -0
  62. package/skills/wix-headless/references/inline-recipes/how-to-code-events.md +203 -0
  63. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +222 -0
  64. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +124 -0
  65. package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +160 -0
  66. package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +118 -0
  67. package/skills/wix-headless/references/inline-recipes/how-to-code-portfolio.md +132 -0
  68. package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +124 -0
  69. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +188 -0
  70. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +173 -0
  71. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurants.md +161 -0
  72. package/skills/wix-headless/references/inline-recipes/setup-blog.md +175 -0
  73. package/skills/wix-headless/references/inline-recipes/setup-bookings.md +228 -0
  74. package/skills/wix-headless/references/inline-recipes/setup-cms.md +200 -0
  75. package/skills/wix-headless/references/inline-recipes/setup-events.md +196 -0
  76. package/skills/wix-headless/references/inline-recipes/setup-forms.md +174 -0
  77. package/skills/wix-headless/references/inline-recipes/setup-online-store.md +176 -0
  78. package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +153 -0
  79. package/skills/wix-headless/references/inline-recipes/setup-pricing-plans.md +229 -0
  80. package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +161 -0
  81. package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +117 -0
  82. package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +152 -0
  83. package/skills/wix-headless/references/managed/AUTHENTICATION.md +57 -0
  84. package/skills/wix-headless/references/managed/CONNECT.md +74 -0
  85. package/skills/wix-headless/references/managed/CREATE.md +130 -0
  86. package/skills/wix-headless/references/managed/DEPLOYMENT.md +57 -0
  87. package/skills/wix-headless/references/non-astro.md +59 -0
  88. package/skills/wix-headless/references/self-managed/AUTHENTICATION.md +22 -0
  89. package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +7 -0
  90. package/skills/wix-headless/references/stripe/AUTHENTICATION.md +55 -0
  91. package/skills/wix-headless/references/stripe/DEPLOYMENT.md +63 -0
  92. package/skills/wix-manage/SKILL.md +24 -1
  93. package/skills/wix-manage/references/analytics/query-site-analytics.md +241 -0
  94. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +1 -1
  95. package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +265 -0
  96. package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +436 -0
  97. package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +209 -0
  98. package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +249 -0
  99. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +51 -0
  100. package/skills/wix-vibe-headless/SKILL.md +136 -0
  101. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +128 -0
  102. package/skills/wix-vibe-headless/references/blog/wix-blog.js +217 -0
  103. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +153 -0
  104. package/skills/wix-vibe-headless/references/bookings/wix-bookings-checkout.js +147 -0
  105. package/skills/wix-vibe-headless/references/bookings/wix-bookings-services.js +215 -0
  106. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +154 -0
  107. package/skills/wix-vibe-headless/references/cms/wix-cms.js +186 -0
  108. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +142 -0
  109. package/skills/wix-vibe-headless/references/events/wix-events-browse.js +135 -0
  110. package/skills/wix-vibe-headless/references/events/wix-events-registration.js +187 -0
  111. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +211 -0
  112. package/skills/wix-vibe-headless/references/members/wix-members-auth.js +387 -0
  113. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +111 -0
  114. package/skills/wix-vibe-headless/references/portfolio/wix-portfolio.js +194 -0
  115. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +127 -0
  116. package/skills/wix-vibe-headless/references/pricing-plans/wix-pricing-plans.js +157 -0
  117. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +138 -0
  118. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-menu.js +224 -0
  119. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-ordering.js +156 -0
  120. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-reservations.js +107 -0
  121. package/skills/wix-vibe-headless/references/shared/wix-client.js +186 -0
  122. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +155 -0
  123. package/skills/wix-vibe-headless/references/storefront/wix-store-cart.js +147 -0
  124. package/skills/wix-vibe-headless/references/storefront/wix-store-catalog.js +142 -0
  125. package/skills/wix-headless/references/BUILD-astro.md +0 -244
  126. package/skills/wix-headless/references/BUILD-own-build.md +0 -167
  127. package/skills/wix-headless/references/BUILD.md +0 -105
  128. package/skills/wix-headless/references/DESIGN_SYSTEM.md +0 -165
  129. package/skills/wix-headless/references/DISCOVERY-connect.md +0 -55
  130. package/skills/wix-headless/references/DISCOVERY-create.md +0 -238
  131. package/skills/wix-headless/references/PLAN-connect.md +0 -19
  132. package/skills/wix-headless/references/PLAN-create.md +0 -17
  133. package/skills/wix-headless/references/PLAN.md +0 -92
  134. package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +0 -300
  135. package/skills/wix-headless/references/astro/blog/BLOG_SETUP.md +0 -218
  136. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +0 -45
  137. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +0 -77
  138. package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +0 -214
  139. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +0 -231
  140. package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +0 -453
  141. package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +0 -354
  142. package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +0 -299
  143. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +0 -351
  144. package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +0 -119
  145. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +0 -103
  146. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +0 -132
  147. package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +0 -463
  148. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +0 -119
  149. package/skills/wix-headless/references/astro/gift-cards/PAGES.md +0 -129
  150. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +0 -178
  151. package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +0 -85
  152. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +0 -129
  153. package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +0 -229
  154. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +0 -215
  155. package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +0 -165
  156. package/skills/wix-headless/references/astro/templates/Footer.astro +0 -27
  157. package/skills/wix-headless/references/astro/templates/Layout.astro +0 -132
  158. package/skills/wix-headless/references/astro/templates/Navigation.astro +0 -38
  159. package/skills/wix-headless/references/astro/templates/astro.config.mjs +0 -41
  160. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +0 -374
  161. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +0 -136
  162. package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +0 -25
  163. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +0 -71
  164. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +0 -54
  165. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +0 -49
  166. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +0 -245
  167. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +0 -424
  168. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +0 -176
  169. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +0 -181
  170. package/skills/wix-headless/references/astro/templates/ecom/CartBadge.tsx +0 -84
  171. package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +0 -543
  172. package/skills/wix-headless/references/astro/templates/ecom/cart.astro +0 -13
  173. package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +0 -302
  174. package/skills/wix-headless/references/astro/templates/ecom/discounts.ts +0 -71
  175. package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +0 -24
  176. package/skills/wix-headless/references/astro/templates/gift-cards/GiftCardPurchase.tsx +0 -172
  177. package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +0 -42
  178. package/skills/wix-headless/references/astro/templates/gift-cards/_nav-snippet.astro +0 -11
  179. package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +0 -220
  180. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +0 -51
  181. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.ts +0 -75
  182. package/skills/wix-headless/references/astro/templates/global.css +0 -288
  183. package/skills/wix-headless/references/astro/templates/index.astro +0 -56
  184. package/skills/wix-headless/references/astro/templates/stores/AddToCartButton.tsx +0 -77
  185. package/skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx +0 -176
  186. package/skills/wix-headless/references/astro/templates/stores/CategoryRail.astro +0 -69
  187. package/skills/wix-headless/references/astro/templates/stores/ProductCard.astro +0 -79
  188. package/skills/wix-headless/references/astro/templates/stores/ProductPurchase.tsx +0 -397
  189. package/skills/wix-headless/references/astro/templates/stores/SeoTags.astro +0 -21
  190. package/skills/wix-headless/references/astro/templates/stores/back-in-stock.ts +0 -67
  191. package/skills/wix-headless/references/astro/templates/stores/categories.ts +0 -155
  192. package/skills/wix-headless/references/astro/templates/stores/category/[slug].astro +0 -130
  193. package/skills/wix-headless/references/astro/templates/stores/components-stores.css +0 -400
  194. package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +0 -128
  195. package/skills/wix-headless/references/astro/templates/stores/products/index.astro +0 -101
  196. package/skills/wix-headless/references/blog/BLOG_CONTENT.md +0 -161
  197. package/skills/wix-headless/references/blog/INSTRUCTIONS.md +0 -32
  198. package/skills/wix-headless/references/bookings/FLOW.md +0 -246
  199. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +0 -95
  200. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +0 -355
  201. package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +0 -176
  202. package/skills/wix-headless/references/cms/INSTRUCTIONS.md +0 -71
  203. package/skills/wix-headless/references/commands/known-apps.json +0 -6
  204. package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +0 -142
  205. package/skills/wix-headless/references/custom/INSTRUCTIONS.md +0 -88
  206. package/skills/wix-headless/references/custom/blog/WIRING.md +0 -52
  207. package/skills/wix-headless/references/custom/bookings/WIRING.md +0 -197
  208. package/skills/wix-headless/references/custom/cms/WIRING.md +0 -97
  209. package/skills/wix-headless/references/custom/ecom/WIRING.md +0 -67
  210. package/skills/wix-headless/references/custom/forms/WIRING.md +0 -101
  211. package/skills/wix-headless/references/custom/stores/WIRING.md +0 -80
  212. package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +0 -59
  213. package/skills/wix-headless/references/forms/INSTRUCTIONS.md +0 -43
  214. package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +0 -51
  215. package/skills/wix-headless/references/images/INSTRUCTIONS.md +0 -357
  216. package/skills/wix-headless/references/seed-recipes.md +0 -49
  217. package/skills/wix-headless/references/shared/AGENTS_MD.md +0 -47
  218. package/skills/wix-headless/references/shared/AUTHENTICATION.md +0 -108
  219. package/skills/wix-headless/references/shared/BUILD_NOISE.md +0 -8
  220. package/skills/wix-headless/references/shared/DESIGN_MD.md +0 -84
  221. package/skills/wix-headless/references/shared/DOCS_SEARCH.md +0 -41
  222. package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +0 -186
  223. package/skills/wix-headless/references/shared/IMPLEMENTER.md +0 -206
  224. package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +0 -97
  225. package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +0 -143
  226. package/skills/wix-headless/references/shared/STYLING.md +0 -141
  227. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +0 -126
  228. package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +0 -252
  229. package/skills/wix-headless/references/verticals/_schema.json +0 -74
  230. package/skills/wix-headless/references/verticals/_schema.md +0 -95
  231. package/skills/wix-headless/references/verticals/blog.md +0 -31
  232. package/skills/wix-headless/references/verticals/bookings.md +0 -41
  233. package/skills/wix-headless/references/verticals/cms.md +0 -30
  234. package/skills/wix-headless/references/verticals/ecom.md +0 -25
  235. package/skills/wix-headless/references/verticals/forms.md +0 -26
  236. package/skills/wix-headless/references/verticals/gift-cards.md +0 -24
  237. package/skills/wix-headless/references/verticals/stores.md +0 -33
  238. package/skills/wix-headless/scripts/check-manifest.mjs +0 -335
  239. package/skills/wix-headless/scripts/compose.mjs +0 -525
  240. package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -157
  241. package/skills/wix-headless/scripts/flatten.sh +0 -61
  242. package/skills/wix-headless/scripts/generate-images.mjs +0 -242
  243. package/skills/wix-headless/scripts/patch-decorative-slots.mjs +0 -148
  244. package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +0 -190
  245. package/skills/wix-headless/scripts/scaffold.sh +0 -139
  246. package/skills/wix-headless/scripts/seed-utilities.sh +0 -93
  247. package/skills/wix-headless/scripts/write-decorative-json.mjs +0 -62
  248. package/skills/wix-headless/shared-utilities/analytics.ts +0 -40
  249. package/skills/wix-headless/shared-utilities/ricos.ts +0 -179
  250. package/skills/wix-headless/shared-utilities/wix-image.ts +0 -72
@@ -1,27 +0,0 @@
1
- ---
2
- // ── Composer skeleton: src/components/Footer.astro ────────────────────────────
3
- // Substitutions (see compose.mjs):
4
- // {{brand.name}} — brand name for the wordmark + copyright.
5
- // {{shell.footerTagline}} — short brand-voice tagline from data.shell.
6
- // {{nav.links}} — same inlined Navigation links array (mirror/subset);
7
- // one <a href={href}>{label}</a> per entry, labels VERBATIM. No cart,
8
- // login, or account links.
9
- // Fixed bulk (keep literal): transition:persist, the year expression.
10
- const year = new Date().getFullYear();
11
- ---
12
- <footer class="site-footer" transition:persist="site-footer">
13
- <div class="max-w-6xl mx-auto px-6 py-16 flex flex-col md:flex-row md:items-start md:justify-between gap-8">
14
- <div class="flex flex-col gap-3 max-w-prose">
15
- <span class="font-display text-lg text-ink">{{brand.name}}</span>
16
- <p class="text-sm">{{shell.footerTagline}}</p>
17
- </div>
18
- <nav>
19
- <ul class="flex flex-col gap-3 text-sm">
20
- {{nav.links}}
21
- </ul>
22
- </nav>
23
- </div>
24
- <div class="max-w-6xl mx-auto px-6 pb-8 text-xs">
25
- <p>© {year} {{brand.name}}. All rights reserved.</p>
26
- </div>
27
- </footer>
@@ -1,132 +0,0 @@
1
- ---
2
- // ── Composer skeleton: src/layouts/Layout.astro ──────────────────────────────
3
- // Fully replaces the scaffold's bare stub. The <script> below and the View
4
- // Transitions wiring are LITERAL fixed bulk — do not re-author. Composer's only
5
- // substitutions (see compose.mjs):
6
- // {{components-css-imports}} — one `import '../styles/components-<pack>.css';`
7
- // per pack in "Packs with components" (omit entirely when none).
8
- // {{fonts.googleHref}} — the Google Fonts <link> href built from designTokens
9
- // .fonts (display + body families). Omit the <link> if fonts are system.
10
- // {{brand.name}} — default <title>.
11
- import '../styles/global.css';
12
- {{components-css-imports}}
13
- import { ClientRouter } from 'astro:transitions';
14
- import Navigation from '../components/Navigation.astro';
15
- import Footer from '../components/Footer.astro';
16
-
17
- interface Props {
18
- title?: string;
19
- description?: string;
20
- hasSeoTags?: boolean;
21
- }
22
- const { title, description, hasSeoTags = false } = Astro.props;
23
- ---
24
- <!doctype html>
25
- <html lang="en">
26
- <head>
27
- <meta charset="utf-8" />
28
- <meta name="viewport" content="width=device-width, initial-scale=1" />
29
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" />
30
- <link rel="preconnect" href="https://fonts.googleapis.com" />
31
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
32
- <link rel="stylesheet" href="{{fonts.googleHref}}" />
33
- {!hasSeoTags && (
34
- <>
35
- <title>{title ? `${title} · {{brand.name}}` : "{{brand.name}}"}</title>
36
- {description && <meta name="description" content={description} />}
37
- </>
38
- )}
39
- <slot name="head" />
40
- <ClientRouter />
41
- </head>
42
- <body>
43
- <div class="nav-progress" data-nav-progress aria-hidden="true"></div>
44
- <Navigation />
45
- <main><slot /></main>
46
- <Footer />
47
- <script>
48
- // Persisted rail aria-current sync + top loading bar + rail-anchored
49
- // scroll across listing-to-listing navigation. Stores pack relies on
50
- // all three; do not split into separate scripts.
51
- function syncCategoryRail() {
52
- const path = window.location.pathname;
53
- const activeSlug = path.startsWith("/category/")
54
- ? path.slice("/category/".length).replace(/\/$/, "")
55
- : path === "/products" || path === "/products/"
56
- ? ""
57
- : null;
58
- if (activeSlug === null) return;
59
- const pills = document.querySelectorAll<HTMLAnchorElement>(
60
- "[data-category-rail] .category-pill",
61
- );
62
- for (const pill of pills) {
63
- const slug = pill.dataset.categorySlug ?? "";
64
- if (slug === activeSlug) pill.setAttribute("aria-current", "page");
65
- else pill.removeAttribute("aria-current");
66
- }
67
- }
68
- // Eager click feedback — flip aria-current before the network call.
69
- document.addEventListener("click", (event) => {
70
- const target = event.target as HTMLElement | null;
71
- const pill = target?.closest<HTMLAnchorElement>(
72
- "[data-category-rail] .category-pill",
73
- );
74
- if (!pill) return;
75
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
76
- const pills = document.querySelectorAll<HTMLAnchorElement>(
77
- "[data-category-rail] .category-pill",
78
- );
79
- for (const p of pills) p.removeAttribute("aria-current");
80
- pill.setAttribute("aria-current", "page");
81
- }, true);
82
- const progress = document.querySelector<HTMLElement>("[data-nav-progress]");
83
- function startProgress() {
84
- if (!progress) return;
85
- progress.dataset.state = "loading";
86
- document.body.dataset.navigating = "true";
87
- }
88
- function endProgress() {
89
- if (!progress) return;
90
- progress.dataset.state = "done";
91
- delete document.body.dataset.navigating;
92
- window.setTimeout(() => {
93
- if (progress.dataset.state === "done") delete progress.dataset.state;
94
- }, 320);
95
- }
96
- // Anchor the persisted rail across listing-to-listing navigation. The
97
- // page-header above the rail differs between /products and /category/[slug]
98
- // (breadcrumbs, lede, optional image), so naive scroll preservation reads
99
- // as a jump. Capture rail.getBoundingClientRect().top before the swap and
100
- // re-equalize after.
101
- const isListing = (p: string) =>
102
- p === "/products" || p === "/products/" || p.startsWith("/category/");
103
- let railAnchorTop: number | null = null;
104
- document.addEventListener("astro:before-preparation", (event: any) => {
105
- const from = window.location.pathname;
106
- const to = event?.to?.pathname ?? from;
107
- const rail = document.querySelector<HTMLElement>("[data-category-rail]");
108
- railAnchorTop =
109
- rail && isListing(from) && isListing(to)
110
- ? rail.getBoundingClientRect().top
111
- : null;
112
- startProgress();
113
- });
114
- document.addEventListener("astro:after-swap", () => {
115
- syncCategoryRail();
116
- if (railAnchorTop !== null) {
117
- const rail = document.querySelector<HTMLElement>("[data-category-rail]");
118
- if (rail) {
119
- const delta = rail.getBoundingClientRect().top - railAnchorTop;
120
- if (Math.abs(delta) > 0.5) {
121
- window.scrollBy({ top: delta, left: 0, behavior: "instant" as ScrollBehavior });
122
- }
123
- }
124
- }
125
- railAnchorTop = null;
126
- endProgress();
127
- });
128
- document.addEventListener("astro:page-load", endProgress);
129
- syncCategoryRail();
130
- </script>
131
- </body>
132
- </html>
@@ -1,38 +0,0 @@
1
- ---
2
- // ── Composer skeleton: src/components/Navigation.astro ────────────────────────
3
- // Substitutions (see compose.mjs):
4
- // {{shell.navBrandMark}} — brand mark / wordmark text from data.shell.
5
- // {{nav.links}} — one <a href={href}>{label}</a> per entry in the inlined
6
- // Navigation links array, labels VERBATIM (no editorial rebrands). Render
7
- // each inside a <li class="site-nav-item">.
8
- // Fixed bulk (keep literal): transition:persist, the <!-- nav:links --> marker,
9
- // the empty <div class="nav-actions"> mount, the mobile hamburger toggle.
10
- ---
11
- <header class="site-nav" transition:persist="site-nav">
12
- <nav class="max-w-6xl mx-auto px-6 flex items-center justify-between gap-6 py-4">
13
- <a href="/" class="font-display text-lg tracking-tight">{{shell.navBrandMark}}</a>
14
-
15
- <input type="checkbox" id="nav-toggle" class="peer hidden" aria-hidden="true" />
16
- <label
17
- for="nav-toggle"
18
- class="md:hidden inline-flex flex-col gap-[5px] cursor-pointer"
19
- aria-label="Toggle navigation"
20
- >
21
- <span class="block h-[2px] w-6 bg-ink"></span>
22
- <span class="block h-[2px] w-6 bg-ink"></span>
23
- <span class="block h-[2px] w-6 bg-ink"></span>
24
- </label>
25
-
26
- <div
27
- class="hidden peer-checked:flex md:flex absolute md:static top-full left-0 right-0 md:right-auto
28
- flex-col md:flex-row gap-4 md:gap-6 items-start md:items-center
29
- bg-paper md:bg-transparent border-b md:border-0 border-rule px-6 md:px-0 py-4 md:py-0"
30
- >
31
- <ul class="flex flex-col md:flex-row gap-4 md:gap-6 items-start md:items-center">
32
- {{nav.links}}
33
- <!-- nav:links -->
34
- </ul>
35
- <div class="nav-actions"></div>
36
- </div>
37
- </nav>
38
- </header>
@@ -1,41 +0,0 @@
1
- // ── Composer skeleton: astro.config.mjs ───────────────────────────────────────
2
- // UNLIKE the other skeletons, this one is a MERGE TARGET, not a clobber. The
3
- // scaffold's generated astro.config.mjs varies (integrations args, image.domains,
4
- // vite.server settings differ run to run). Composer must NOT overwrite it with
5
- // this file — it applies exactly two mutations to the scaffold's own config and
6
- // preserves everything else (see compose.mjs):
7
- //
8
- // (1) register the Tailwind v4 Vite plugin → vite.plugins: [tailwindcss()]
9
- // (2) fix the bare `process.env` reference → globalThis guard (TS-safe)
10
- //
11
- // The shape below is the canonical post-mutation result for a typical scaffold —
12
- // use it as the reference for what those two edits should produce.
13
- // @ts-check
14
- import { defineConfig } from "astro/config";
15
- import wix from "@wix/astro";
16
- import wixPages from "@wix/astro-pages";
17
- import react from "@astrojs/react";
18
- import cloudProviderFetchAdapter from "@wix/cloud-provider-fetch-adapter";
19
- import tailwindcss from "@tailwindcss/vite"; // (1) added by Composer
20
-
21
- // (2) `process.env` guarded so strict `tsc --noEmit` does not fail with
22
- // "Cannot find name 'process'" without an @types/node dependency.
23
- const isBuild =
24
- (/** @type {any} */ (globalThis)).process?.env?.NODE_ENV === "production";
25
-
26
- // https://astro.build/config
27
- export default defineConfig({
28
- integrations: [wix(), wixPages(), react()],
29
- security: { checkOrigin: false },
30
- ...(isBuild && { adapter: cloudProviderFetchAdapter({}) }),
31
-
32
- image: {
33
- domains: ["static.wixstatic.com"],
34
- },
35
-
36
- vite: {
37
- plugins: [tailwindcss()], // (1) merge into any existing vite config
38
- },
39
-
40
- output: "server",
41
- });
@@ -1,374 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { availabilityTimeSlots, eventTimeSlots } from "@wix/bookings";
3
- import type { SelectedSlot } from "./bookingDriver";
4
- import { STAFF_MEMBER_RESOURCE_TYPE_ID } from "./bookingDriver";
5
-
6
- // AvailabilityCalendar.tsx — client:only="react" island. A week calendar: a
7
- // 7-day strip with week navigation, then the picked day's time slots. This is
8
- // the usable shape — a flat list of every slot with time-only labels leaves the
9
- // visitor unable to tell which day a slot is on.
10
- //
11
- // SDK calls run ambiently (the @wix/astro visitor client), like the ecom
12
- // CartView island — no createClient/OAuthStrategy here. On an own/own-build SPA
13
- // acquire a visitor client (OAuthStrategy) and call the same functions on it.
14
- //
15
- // Branches on serviceType:
16
- // APPOINTMENT → availabilityTimeSlots.listAvailabilityTimeSlots() (serviceId is
17
- // a single GUID string; slots carry scheduleId).
18
- // CLASS → eventTimeSlots.listEventTimeSlots() (serviceIds is an array;
19
- // slots carry eventInfo.eventId and NO scheduleId).
20
- //
21
- // LOCATION SCOPING (multi-location): listAvailabilityTimeSlots returns one slot
22
- // PER LOCATION per time — "if locations is not specified, returns time slots for
23
- // all locations where the service is available". A service offered at >1 location
24
- // therefore yields DUPLICATE same-time rows unless the call is scoped to a single
25
- // location. So availability is ALWAYS scoped to one business location: the picker
26
- // below defaults to the carried/first location and the call always passes a
27
- // single-element `locations` filter. (One location per availability call — the
28
- // same single-location guarantee the location-first SoT gets from its selector.)
29
- //
30
- // STAFF: a single slot carries multiple staff in availableResources, so staff is
31
- // a per-slot dropdown — it never multiplies rows. Only location multiplies rows.
32
- // Re-export SelectedSlot from bookingDriver so the form/driver share one shape.
33
- export type { SelectedSlot } from "./bookingDriver";
34
-
35
- // A staff member as carried on the service (service.staffMemberDetails.staffMembers[]),
36
- // fetched via conditionalFields:["STAFF_MEMBER_DETAILS"]. NOTE: the id field is
37
- // `staffMemberId` which — despite the name — IS the resource id (it matches the
38
- // service's staffMemberIds and the Staff Members API resourceId). That id is what
39
- // filters availability and books the resource.
40
- export interface StaffMemberOption {
41
- staffMemberId?: string;
42
- name?: string;
43
- }
44
-
45
- // A business location the service is offered at (derived from service.locations[]).
46
- export interface LocationOption {
47
- _id: string;
48
- name?: string;
49
- }
50
-
51
- interface Props {
52
- serviceId: string;
53
- serviceName: string;
54
- serviceType: "APPOINTMENT" | "CLASS";
55
- staffMembers?: StaffMemberOption[];
56
- locations?: LocationOption[]; // business locations the service offers
57
- locationId?: string; // the location chosen on the catalog (the default scope), if any
58
- onSlotSelected: (slot: SelectedSlot) => void;
59
- }
60
-
61
- interface FetchOpts {
62
- resourceId?: string; // a staff resource id to filter by (omit for "any staff")
63
- locationId?: string; // a business location id to scope to (omit only when the service has none)
64
- }
65
-
66
- const DAY_MS = 86_400_000;
67
- const HORIZON_WEEKS = 13; // how far "check next availability" probes forward
68
- const ANY_STAFF = ""; // the "any staff member" sentinel for the select value
69
-
70
- const pad = (n: number) => String(n).padStart(2, "0");
71
- // Local date string YYYY-MM-DDThh:mm:ss (no Z) — availability expects local time.
72
- const localDateString = (d: Date) =>
73
- `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
74
- const tz = () => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
75
- const dateKey = (iso: string) => iso.slice(0, 10); // group slots by calendar day
76
- const timeLabel = (iso: string) =>
77
- new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
78
-
79
- const startOfDay = (d: Date) => { const n = new Date(d); n.setHours(0, 0, 0, 0); return n; };
80
- // Monday-start week containing `d`.
81
- const mondayOf = (d: Date) => {
82
- const n = startOfDay(d);
83
- const day = (n.getDay() + 6) % 7; // 0 = Monday
84
- n.setDate(n.getDate() - day);
85
- return n;
86
- };
87
-
88
- // Fetch one week of availability and group the slots by calendar day.
89
- const fetchWeek = async (
90
- serviceId: string,
91
- serviceType: "APPOINTMENT" | "CLASS",
92
- weekStart: Date,
93
- opts: FetchOpts = {},
94
- ): Promise<Record<string, SelectedSlot[]>> => {
95
- const from = startOfDay(weekStart);
96
- const to = new Date(from.getTime() + 7 * DAY_MS);
97
- const timeZone = tz();
98
- let raw: any[] = [];
99
-
100
- if (serviceType === "CLASS") {
101
- // CLASS filters via eventFilter: staff by resources.id ($hasSome), location by
102
- // location.id (a bare array, NOT a $hasSome operator).
103
- const eventFilter: Record<string, any> = {};
104
- if (opts.resourceId) eventFilter["resources.id"] = { $hasSome: [opts.resourceId] };
105
- if (opts.locationId) eventFilter["location.id"] = [opts.locationId];
106
- const result = await eventTimeSlots.listEventTimeSlots({
107
- serviceIds: [serviceId], // CLASS API takes an array
108
- fromLocalDate: localDateString(from),
109
- toLocalDate: localDateString(to),
110
- timeZone,
111
- includeNonBookable: false,
112
- cursorPaging: { limit: 100 },
113
- ...(Object.keys(eventFilter).length ? { eventFilter } : {}),
114
- });
115
- raw = (result.timeSlots ?? []).map((s: any) => ({
116
- serviceType: "CLASS" as const,
117
- serviceId,
118
- localStartDate: s.localStartDate,
119
- localEndDate: s.localEndDate,
120
- timezone: result.timeZone ?? timeZone,
121
- eventId: s.eventInfo?.eventId, // session id — no scheduleId on event slots
122
- }));
123
- } else {
124
- // APPOINTMENT filters staff via resourceTypes (resourceIds), location via locations[].
125
- // Scoping to a single location collapses the per-location duplicate rows.
126
- const result = await availabilityTimeSlots.listAvailabilityTimeSlots({
127
- serviceId, // single GUID string — NOT an array
128
- fromLocalDate: localDateString(from),
129
- toLocalDate: localDateString(to),
130
- timeZone,
131
- bookable: true,
132
- cursorPaging: { limit: 100 },
133
- ...(opts.resourceId
134
- ? {
135
- resourceTypes: [
136
- { resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [opts.resourceId] },
137
- ],
138
- includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
139
- }
140
- : {}),
141
- ...(opts.locationId
142
- ? { locations: [{ _id: opts.locationId, locationType: "BUSINESS" as const }] }
143
- : {}),
144
- });
145
- raw = (result.timeSlots ?? []).map((s: any) => ({
146
- serviceType: "APPOINTMENT" as const,
147
- serviceId,
148
- localStartDate: s.localStartDate,
149
- localEndDate: s.localEndDate,
150
- timezone: result.timeZone ?? timeZone,
151
- scheduleId: s.scheduleId,
152
- locationId: s.location?._id, // the id field is _id, not .id
153
- locationType: s.location?.locationType,
154
- }));
155
- }
156
-
157
- const byDay: Record<string, SelectedSlot[]> = {};
158
- for (const slot of raw) {
159
- if (!slot.localStartDate) continue;
160
- (byDay[dateKey(slot.localStartDate)] ||= []).push(slot);
161
- }
162
- return byDay;
163
- };
164
-
165
- export default function AvailabilityCalendar({
166
- serviceId,
167
- serviceName,
168
- serviceType,
169
- staffMembers,
170
- locations,
171
- locationId,
172
- onSlotSelected,
173
- }: Props) {
174
- const thisMonday = useMemo(() => mondayOf(new Date()), []);
175
- const [weekStart, setWeekStart] = useState<Date>(thisMonday);
176
- const [byDay, setByDay] = useState<Record<string, SelectedSlot[]>>({});
177
- const [selectedDay, setSelectedDay] = useState<string | null>(null);
178
- const [selectedKey, setSelectedKey] = useState<string | null>(null);
179
- const [status, setStatus] = useState<"loading" | "ready" | "error" | "searching">("loading");
180
- // The chosen staff resource id ("" = any staff member → ANY_RESOURCE fallback).
181
- const [selectedResource, setSelectedResource] = useState<string>(ANY_STAFF);
182
-
183
- // Staff options keyed by resource id; picker shown only when >1 staff.
184
- const staffOptions = useMemo(
185
- () =>
186
- (staffMembers ?? [])
187
- .map((s) => ({ id: s.staffMemberId ?? "", name: s.name ?? "Staff member" }))
188
- .filter((s) => s.id),
189
- [staffMembers],
190
- );
191
- const showStaffPicker = staffOptions.length > 1;
192
-
193
- // Business-location options. Availability is always scoped to ONE of these (the
194
- // carried location if it's one of them, else the first), so multi-location
195
- // services don't return duplicate per-location rows. Picker shown only when >1.
196
- const locationOptions = useMemo(
197
- () => (locations ?? []).filter((l) => l._id),
198
- [locations],
199
- );
200
- const [selectedLocation, setSelectedLocation] = useState<string>(() => {
201
- if (locationId && locationOptions.some((l) => l._id === locationId)) return locationId;
202
- return locationOptions[0]?._id ?? "";
203
- });
204
- const showLocationPicker = locationOptions.length > 1;
205
-
206
- const days = useMemo(
207
- () => Array.from({ length: 7 }, (_, i) => new Date(weekStart.getTime() + i * DAY_MS)),
208
- [weekStart],
209
- );
210
- const atFirstWeek = weekStart.getTime() <= thisMonday.getTime();
211
-
212
- const fetchOpts = useCallback(
213
- (): FetchOpts => ({
214
- resourceId: selectedResource || undefined,
215
- locationId: selectedLocation || undefined,
216
- }),
217
- [selectedResource, selectedLocation],
218
- );
219
-
220
- const load = useCallback(async (start: Date) => {
221
- setStatus("loading"); setSelectedKey(null);
222
- try {
223
- const grouped = await fetchWeek(serviceId, serviceType, start, fetchOpts());
224
- setByDay(grouped);
225
- const firstDayWithSlots = Object.keys(grouped).sort()[0] ?? null;
226
- setSelectedDay(firstDayWithSlots);
227
- setStatus("ready");
228
- } catch (err) {
229
- console.error("[availability] list failed:", err);
230
- setStatus("error");
231
- }
232
- }, [serviceId, serviceType, fetchOpts]);
233
-
234
- // Reload whenever the week OR the staff/location filter changes.
235
- useEffect(() => { void load(weekStart); }, [weekStart, load]);
236
-
237
- const shiftWeek = (deltaWeeks: number) =>
238
- setWeekStart((prev) => startOfDay(new Date(prev.getTime() + deltaWeeks * 7 * DAY_MS)));
239
-
240
- // Probe forward week-by-week for the next week that has any slots.
241
- const checkNextAvailability = useCallback(async () => {
242
- setStatus("searching");
243
- let cursor = new Date(weekStart.getTime() + 7 * DAY_MS);
244
- for (let i = 0; i < HORIZON_WEEKS; i++) {
245
- try {
246
- const grouped = await fetchWeek(serviceId, serviceType, cursor, fetchOpts());
247
- if (Object.keys(grouped).length > 0) {
248
- setWeekStart(cursor); // triggers load() via the effect
249
- return;
250
- }
251
- } catch { /* keep probing */ }
252
- cursor = new Date(cursor.getTime() + 7 * DAY_MS);
253
- }
254
- setStatus("ready"); // nothing found within the horizon
255
- }, [serviceId, serviceType, weekStart, fetchOpts]);
256
-
257
- const handleSelect = (s: SelectedSlot) => {
258
- const id = s.serviceType === "CLASS" ? s.eventId : s.scheduleId;
259
- if (!s.localStartDate || !id) return;
260
- setSelectedKey(`${s.localStartDate}|${id}`);
261
- // When a specific staff was chosen, record it on the slot so the booking books
262
- // that resource; otherwise the driver emits the ANY_RESOURCE fallback.
263
- const chosen = selectedResource
264
- ? staffOptions.find((o) => o.id === selectedResource)
265
- : null;
266
- onSlotSelected(chosen ? { ...s, resource: { _id: chosen.id, name: chosen.name } } : s);
267
- };
268
-
269
- const weekLabel = `${days[0].toLocaleDateString([], { month: "short", day: "numeric" })} – ${days[6].toLocaleDateString([], { month: "short", day: "numeric" })}`;
270
- const daySlots = selectedDay ? byDay[selectedDay] ?? [] : [];
271
- const weekIsEmpty = status === "ready" && Object.keys(byDay).length === 0;
272
-
273
- return (
274
- <div className="availability-calendar">
275
- {(showLocationPicker || showStaffPicker) && (
276
- <div className="availability-filters">
277
- {showLocationPicker && (
278
- <div className="availability-staff">
279
- <label className="availability-staff-label" htmlFor="location-select">Location</label>
280
- <select
281
- id="location-select"
282
- className="availability-staff-select"
283
- value={selectedLocation}
284
- onChange={(e) => { setSelectedLocation(e.target.value); setSelectedKey(null); }}
285
- >
286
- {locationOptions.map((o) => (
287
- <option key={o._id} value={o._id}>{o.name ?? "Location"}</option>
288
- ))}
289
- </select>
290
- </div>
291
- )}
292
- {showStaffPicker && (
293
- <div className="availability-staff">
294
- <label className="availability-staff-label" htmlFor="staff-select">Staff member</label>
295
- <select
296
- id="staff-select"
297
- className="availability-staff-select"
298
- value={selectedResource}
299
- onChange={(e) => { setSelectedResource(e.target.value); setSelectedKey(null); }}
300
- >
301
- <option value={ANY_STAFF}>Any staff member</option>
302
- {staffOptions.map((o) => (
303
- <option key={o.id} value={o.id}>{o.name}</option>
304
- ))}
305
- </select>
306
- </div>
307
- )}
308
- </div>
309
- )}
310
-
311
- <div className="availability-week-nav">
312
- <button type="button" className="availability-nav-btn" onClick={() => shiftWeek(-1)} disabled={atFirstWeek} aria-label="Previous week">← Prev week</button>
313
- <span className="availability-week-label">{weekLabel}</span>
314
- <button type="button" className="availability-nav-btn" onClick={() => shiftWeek(1)} aria-label="Next week">Next week →</button>
315
- </div>
316
-
317
- <div className="availability-day-strip" role="group" aria-label="Pick a day">
318
- {days.map((d) => {
319
- const key = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
320
- const has = (byDay[key]?.length ?? 0) > 0;
321
- const isSelected = key === selectedDay;
322
- return (
323
- <button
324
- key={key}
325
- type="button"
326
- className={["availability-day", isSelected ? "availability-day--selected" : "", has ? "availability-day--has-slots" : "availability-day--empty"].filter(Boolean).join(" ")}
327
- disabled={!has}
328
- aria-pressed={isSelected}
329
- onClick={() => { setSelectedDay(key); setSelectedKey(null); }}
330
- >
331
- <span className="availability-day-name">{d.toLocaleDateString([], { weekday: "short" })}</span>
332
- <span className="availability-day-num">{d.getDate()}</span>
333
- </button>
334
- );
335
- })}
336
- </div>
337
-
338
- {status === "loading" && <p className="availability-loading">Checking availability…</p>}
339
- {status === "searching" && <p className="availability-loading">Searching for the next available time…</p>}
340
- {status === "error" && (
341
- <>
342
- <p className="availability-error">Could not load availability — please try again.</p>
343
- <button type="button" className="availability-nav-btn" onClick={() => void load(weekStart)}>Retry</button>
344
- </>
345
- )}
346
- {weekIsEmpty && (
347
- <div className="availability-empty">
348
- <p>No availability this week.</p>
349
- <button type="button" className="availability-nav-btn" onClick={() => void checkNextAvailability()}>Check next availability</button>
350
- </div>
351
- )}
352
- {status === "ready" && daySlots.length > 0 && (
353
- <div className="availability-slots" role="group" aria-label={`Available times for ${serviceName}`}>
354
- {daySlots.map((s) => {
355
- const id = s.serviceType === "CLASS" ? s.eventId : s.scheduleId;
356
- const key = `${s.localStartDate}|${id}`;
357
- const isSelected = key === selectedKey;
358
- return (
359
- <button
360
- key={key}
361
- type="button"
362
- className={["time-slot", isSelected ? "time-slot--selected" : "time-slot--available"].join(" ")}
363
- aria-pressed={isSelected}
364
- onClick={() => handleSelect(s)}
365
- >
366
- <span className="time-slot-time">{timeLabel(s.localStartDate)}</span>
367
- </button>
368
- );
369
- })}
370
- </div>
371
- )}
372
- </div>
373
- );
374
- }