@wix/agent-skills 1.17.0 → 1.18.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 (246) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +115 -12
  3. package/skills/wix-app/references/APP_MARKET_REVIEW.md +1 -1
  4. package/skills/wix-app/references/BACKEND_API.md +18 -0
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +6 -0
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +10 -2
  7. package/skills/wix-app/references/DASHBOARD_PAGE.md +32 -70
  8. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +14 -3
  9. package/skills/wix-app/references/SITE_PLUGIN.md +12 -0
  10. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +74 -68
  11. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +79 -0
  12. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +73 -0
  13. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +52 -0
  14. package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +184 -0
  15. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +12 -11
  16. package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +1 -0
  17. package/skills/wix-app/references/editor-react-component/BROWSER-SUPPORT.md +56 -0
  18. package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +16 -8
  19. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +19 -1
  20. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +3 -0
  21. package/skills/wix-base44-connector/SKILL.md +133 -44
  22. package/skills/wix-base44-connector/references/CAPABILITY_MAP.md +78 -0
  23. package/skills/wix-base44-connector/scripts/utils.js +216 -30
  24. package/skills/wix-headless/entry/skill.md +1 -1
  25. package/skills/wix-headless/references/CAPABILITIES.md +9 -1
  26. package/skills/wix-headless/references/DESIGN.md +1 -0
  27. package/skills/wix-headless/references/DISCOVERY.md +2 -1
  28. package/skills/wix-headless/references/IMAGE_GENERATION.md +1 -0
  29. package/skills/wix-headless/references/SDK_HANDOFF.md +4 -1
  30. package/skills/wix-headless/references/SEED.md +2 -1
  31. package/skills/wix-headless/references/SETUP.md +1 -0
  32. package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +2 -2
  33. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +26 -8
  34. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +26 -10
  35. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +8 -8
  36. package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +1 -1
  37. package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +1 -1
  38. package/skills/wix-headless/references/inline-recipes/how-to-code-rentals.md +254 -0
  39. package/skills/wix-headless/references/inline-recipes/setup-online-store.md +13 -0
  40. package/skills/wix-headless/references/inline-recipes/setup-rentals.md +254 -0
  41. package/skills/wix-headless/references/non-astro.md +1 -1
  42. package/skills/wix-headless-fast/SKILL.md +166 -0
  43. package/skills/wix-headless-fast/entry/skill.md +99 -0
  44. package/skills/wix-headless-fast/install/deploy.mjs +433 -0
  45. package/skills/wix-headless-fast/install/fast-path.mjs +141 -0
  46. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +137 -0
  47. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +137 -0
  48. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +113 -0
  49. package/skills/wix-headless-fast/references/blog/app/components/blog/RichContent.tsx +42 -0
  50. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +123 -0
  51. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +70 -0
  52. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +99 -0
  53. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +76 -0
  54. package/skills/wix-headless-fast/references/blog/app/wix/blog/types.ts +62 -0
  55. package/skills/wix-headless-fast/references/blog/app-astro/layouts/SiteLayout.astro +40 -0
  56. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +99 -0
  57. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog.astro +24 -0
  58. package/skills/wix-headless-fast/references/blog/lock/astro/package-lock.json +16156 -0
  59. package/skills/wix-headless-fast/references/blog/seed/SEED.md +72 -0
  60. package/skills/wix-headless-fast/references/blog/seed/seed-blog.mjs +287 -0
  61. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +129 -0
  62. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +143 -0
  63. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServicesView.tsx +119 -0
  64. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +138 -0
  65. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +54 -0
  66. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +206 -0
  67. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +96 -0
  68. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +70 -0
  69. package/skills/wix-headless-fast/references/bookings/app-astro/layouts/SiteLayout.astro +40 -0
  70. package/skills/wix-headless-fast/references/bookings/app-astro/pages/services/[slug].astro +76 -0
  71. package/skills/wix-headless-fast/references/bookings/app-astro/pages/services.astro +22 -0
  72. package/skills/wix-headless-fast/references/bookings/lock/astro/package-lock.json +11962 -0
  73. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +61 -0
  74. package/skills/wix-headless-fast/references/bookings/seed/seed-bookings.mjs +302 -0
  75. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +179 -0
  76. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +83 -0
  77. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +57 -0
  78. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +151 -0
  79. package/skills/wix-headless-fast/references/cms/app/wix/cms/types.ts +75 -0
  80. package/skills/wix-headless-fast/references/cms/app-astro/layouts/SiteLayout.astro +41 -0
  81. package/skills/wix-headless-fast/references/cms/lock/astro/package-lock.json +11262 -0
  82. package/skills/wix-headless-fast/references/cms/seed/SEED.md +103 -0
  83. package/skills/wix-headless-fast/references/cms/seed/seed-cms.mjs +324 -0
  84. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +149 -0
  85. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +66 -0
  86. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +176 -0
  87. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +118 -0
  88. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +117 -0
  89. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +59 -0
  90. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +136 -0
  91. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +128 -0
  92. package/skills/wix-headless-fast/references/events/app/wix/events/types.ts +61 -0
  93. package/skills/wix-headless-fast/references/events/app-astro/layouts/SiteLayout.astro +40 -0
  94. package/skills/wix-headless-fast/references/events/app-astro/pages/event-confirmation.astro +12 -0
  95. package/skills/wix-headless-fast/references/events/app-astro/pages/events/[slug].astro +80 -0
  96. package/skills/wix-headless-fast/references/events/app-astro/pages/events.astro +21 -0
  97. package/skills/wix-headless-fast/references/events/lock/astro/package-lock.json +11884 -0
  98. package/skills/wix-headless-fast/references/events/seed/SEED.md +86 -0
  99. package/skills/wix-headless-fast/references/events/seed/seed-events.mjs +276 -0
  100. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +150 -0
  101. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +317 -0
  102. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +269 -0
  103. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +231 -0
  104. package/skills/wix-headless-fast/references/forms/app/wix/forms/types.ts +135 -0
  105. package/skills/wix-headless-fast/references/forms/lock/astro/package-lock.json +10593 -0
  106. package/skills/wix-headless-fast/references/forms/seed/SEED.md +108 -0
  107. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +320 -0
  108. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +63 -0
  109. package/skills/wix-headless-fast/references/members/app/components/members/AccountView.tsx +60 -0
  110. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +155 -0
  111. package/skills/wix-headless-fast/references/members/app/components/members/MemberMenu.tsx +78 -0
  112. package/skills/wix-headless-fast/references/members/app/components/members/RequireAuth.tsx +47 -0
  113. package/skills/wix-headless-fast/references/members/app/hooks/members/useMember.ts +66 -0
  114. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +88 -0
  115. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +53 -0
  116. package/skills/wix-headless-fast/references/members/app/wix/members/member-store.ts +122 -0
  117. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +50 -0
  118. package/skills/wix-headless-fast/references/members/app/wix/members/types.ts +21 -0
  119. package/skills/wix-headless-fast/references/members/app-astro/layouts/SiteLayout.astro +42 -0
  120. package/skills/wix-headless-fast/references/members/app-astro/pages/account.astro +10 -0
  121. package/skills/wix-headless-fast/references/members/app-astro/pages/login.astro +11 -0
  122. package/skills/wix-headless-fast/references/members/lock/astro/package-lock.json +10329 -0
  123. package/skills/wix-headless-fast/references/members/seed/SEED.md +39 -0
  124. package/skills/wix-headless-fast/references/members/seed/seed-members.mjs +94 -0
  125. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +134 -0
  126. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +110 -0
  127. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +90 -0
  128. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +115 -0
  129. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +59 -0
  130. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +39 -0
  131. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +58 -0
  132. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +165 -0
  133. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/types.ts +55 -0
  134. package/skills/wix-headless-fast/references/portfolio/app-astro/layouts/SiteLayout.astro +40 -0
  135. package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio/[slug].astro +30 -0
  136. package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio.astro +21 -0
  137. package/skills/wix-headless-fast/references/portfolio/app-astro/pages/projects/[slug].astro +29 -0
  138. package/skills/wix-headless-fast/references/portfolio/lock/astro/package-lock.json +10197 -0
  139. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +78 -0
  140. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +258 -0
  141. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +133 -0
  142. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +58 -0
  143. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +126 -0
  144. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +37 -0
  145. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +39 -0
  146. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +75 -0
  147. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +45 -0
  148. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/types.ts +30 -0
  149. package/skills/wix-headless-fast/references/pricing-plans/app-astro/layouts/SiteLayout.astro +41 -0
  150. package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans/[slug].astro +40 -0
  151. package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans.astro +21 -0
  152. package/skills/wix-headless-fast/references/pricing-plans/lock/astro/package-lock.json +11226 -0
  153. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +71 -0
  154. package/skills/wix-headless-fast/references/pricing-plans/seed/seed-pricing-plans.mjs +279 -0
  155. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +165 -0
  156. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +142 -0
  157. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartButton.tsx +26 -0
  158. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +121 -0
  159. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/ReservationView.tsx +155 -0
  160. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +55 -0
  161. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useOrderCart.ts +43 -0
  162. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +177 -0
  163. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +165 -0
  164. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/order-store.ts +103 -0
  165. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +254 -0
  166. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +146 -0
  167. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/types.ts +168 -0
  168. package/skills/wix-headless-fast/references/restaurants/app-astro/layouts/SiteLayout.astro +47 -0
  169. package/skills/wix-headless-fast/references/restaurants/app-astro/pages/menu.astro +22 -0
  170. package/skills/wix-headless-fast/references/restaurants/app-astro/pages/reservations.astro +12 -0
  171. package/skills/wix-headless-fast/references/restaurants/lock/astro/package-lock.json +12560 -0
  172. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +81 -0
  173. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +479 -0
  174. package/skills/wix-headless-fast/references/shared/CONTENT.md +22 -0
  175. package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +73 -0
  176. package/skills/wix-headless-fast/references/shared/DESIGN.md +26 -0
  177. package/skills/wix-headless-fast/references/shared/app/styles/global.css +52 -0
  178. package/skills/wix-headless-fast/references/shared/app/wix/config.ts +15 -0
  179. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +27 -0
  180. package/skills/wix-headless-fast/references/shared/app/wix/money.ts +24 -0
  181. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +76 -0
  182. package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app/wix/media-upload/client.ts +46 -0
  183. package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app-astro/pages/api/wix/media-upload.ts +46 -0
  184. package/skills/wix-headless-fast/references/shared/seed/images.mjs +145 -0
  185. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +149 -0
  186. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartButton.tsx +26 -0
  187. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +121 -0
  188. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +100 -0
  189. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +130 -0
  190. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +48 -0
  191. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +66 -0
  192. package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +103 -0
  193. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useCart.ts +48 -0
  194. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +140 -0
  195. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +80 -0
  196. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +101 -0
  197. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +221 -0
  198. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +215 -0
  199. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +109 -0
  200. package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +46 -0
  201. package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +56 -0
  202. package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +23 -0
  203. package/skills/wix-headless-fast/references/storefront/lock/astro/package-lock.json +12247 -0
  204. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +85 -0
  205. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +415 -0
  206. package/skills/wix-manage/SKILL.md +18 -2
  207. package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +141 -0
  208. package/skills/wix-manage/references/google-business-profile/connect-google-business-profile.md +134 -0
  209. package/skills/wix-manage/references/google-business-profile/manage-google-business-profile-locations.md +182 -0
  210. package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +3 -0
  211. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +21 -6
  212. package/skills/wix-manage/references/stores/update-product-with-options.md +83 -0
  213. package/skills/wix-vibe-headless/SKILL.md +18 -4
  214. package/skills/wix-vibe-headless/install/deploy.cjs +12 -4
  215. package/skills/wix-vibe-headless/install/pin-agents-md.cjs +2 -1
  216. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
  217. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +3 -1
  218. package/skills/wix-vibe-headless/platforms/base44.md +23 -61
  219. package/skills/wix-vibe-headless/platforms/generic.md +6 -3
  220. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +43 -0
  221. package/skills/wix-vibe-headless/references/bookings/app/components/RentalDurationPicker.jsx +67 -0
  222. package/skills/wix-vibe-headless/references/bookings/app/hooks/useRentalDuration.js +128 -0
  223. package/skills/wix-vibe-headless/references/bookings/app/hooks/useServiceDetail.js +5 -3
  224. package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-rentals.js +250 -0
  225. package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-services.js +12 -5
  226. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +41 -0
  227. package/skills/wix-vibe-headless/references/bookings/seed/seed-rentals.js +267 -0
  228. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +6 -3
  229. package/skills/wix-vibe-headless/references/forms/INSTRUCTIONS.md +470 -0
  230. package/skills/wix-vibe-headless/references/forms/app/hooks/useWixForm.js +425 -0
  231. package/skills/wix-vibe-headless/references/forms/app/lib/wix-form-schema-utils.js +187 -0
  232. package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms-submissions.js +238 -0
  233. package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms.js +108 -0
  234. package/skills/wix-vibe-headless/references/forms/seed/SEED.md +1762 -0
  235. package/skills/wix-vibe-headless/references/rentals/INSTRUCTIONS.md +13 -0
  236. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +137 -84
  237. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +15 -8
  238. package/skills/wix-vibe-headless/references/storefront/app/lib/storeImage.js +37 -0
  239. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +18 -6
  240. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +30 -7
  241. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +46 -10
  242. package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +0 -110
  243. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGallery.jsx +0 -51
  244. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +0 -49
  245. package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +0 -92
  246. package/skills/wix-vibe-headless/references/storefront/app/pages/ProductDetail.jsx +0 -139
@@ -149,15 +149,16 @@ function buildOptions(options = []) {
149
149
  }
150
150
 
151
151
  // full Cartesian product of variants, each priced/stocked from the product; visible:true baked in
152
- function expandVariants(options = [], { price, compareAtPrice, quantity }) {
152
+ function expandVariants(options = [], { price, compareAtPrice, quantity }, digitalFileId) {
153
153
  const base = {
154
154
  price: {
155
155
  actualPrice: { amount: String(price) },
156
156
  ...(compareAtPrice ? { compareAtPrice: { amount: String(compareAtPrice) } } : {}),
157
157
  },
158
158
  visible: true,
159
- physicalProperties: {},
160
- inventoryItem: { quantity: quantity ?? 0, preorderInfo: { enabled: false } },
159
+ ...(digitalFileId
160
+ ? { digitalProperties: { digitalFile: { id: digitalFileId } }, inventoryItem: { inStock: true } }
161
+ : { physicalProperties: {}, inventoryItem: { quantity: quantity ?? 0, preorderInfo: { enabled: false } } }),
161
162
  };
162
163
  if (!options.length) return [base];
163
164
  let combos = [[]];
@@ -170,6 +171,33 @@ function expandVariants(options = [], { price, compareAtPrice, quantity }) {
170
171
  return combos.map((choices) => ({ ...base, choices }));
171
172
  }
172
173
 
174
+ // A digital variant is SELLABLE only with BOTH a file and stock: without the file the cart rejects
175
+ // it as ITEM_NOT_FOUND_IN_CATALOG, without stock as "exceeds available inventory" — and either way
176
+ // the product reads back visible and in the catalog, so nothing surfaces until a buyer tries to buy.
177
+ // `digitalFileUrl` is the only way into DIGITAL here, which makes the file-less product unbuildable.
178
+ // The bytes are PUT, not imported by url: an uploaded file is READY at once, while an imported one
179
+ // stays PENDING and the cart rejects the product until it settles.
180
+ const FILE_MIME = { pdf: "application/pdf", zip: "application/zip", epub: "application/epub+zip",
181
+ mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", png: "image/png", jpg: "image/jpeg" };
182
+
183
+ async function uploadDigitalFile(ctx, url, fileName) {
184
+ const mimeType = FILE_MIME[(fileName.split(".").pop() || "").toLowerCase()];
185
+ if (!mimeType) throw new Error(`digitalFileName needs one of these extensions (${Object.keys(FILE_MIME).join(", ")}): ${fileName}`);
186
+ const { uploadUrl } = await req(ctx, "/site-media/v1/files/generate-upload-url", { body: { mimeType, fileName } });
187
+ const src = await fetch(url);
188
+ if (!src.ok) throw new Error(`digitalFileUrl ${url} -> ${src.status}`);
189
+ const res = await fetch(uploadUrl, {
190
+ method: "PUT", headers: { "Content-Type": mimeType }, body: Buffer.from(await src.arrayBuffer()),
191
+ });
192
+ const json = await res.json().catch(() => ({}));
193
+ const id = (json.file || json)?.id;
194
+ if (!res.ok || !id) throw new Error(`digital file upload failed (${res.status}): ${JSON.stringify(json).slice(0, 200)}`);
195
+ return id;
196
+ }
197
+
198
+ const digitalFileName = (p) =>
199
+ p.digitalFileName || decodeURIComponent(new URL(p.digitalFileUrl).pathname.split("/").pop() || "");
200
+
173
201
  // ---- exported operations ----
174
202
 
175
203
  async function installStoresApp(ctx) {
@@ -193,7 +221,10 @@ async function listProducts(ctx) {
193
221
  /**
194
222
  * Bulk-create products.
195
223
  * @param products [{ name, description, price, compareAtPrice?, quantity,
196
- * options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }] }]
224
+ * options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }],
225
+ * digitalFileUrl?, digitalFileName? }]
226
+ * digitalFileUrl: makes the product a DIGITAL download — the file is uploaded and the variant
227
+ * created with both the file and stock (see uploadDigitalFile). `quantity` is ignored.
197
228
  * description: plain text, or simple HTML (`<p>`, `<br/>`, `<strong>`, `<em>`) — converted to
198
229
  * Wix rich text here, so the storefront renders paragraphs and bold rather than tag text.
199
230
  * options = ONLY things the buyer selects-and-buys (Size, Color) -> become variants.
@@ -202,17 +233,20 @@ async function listProducts(ctx) {
202
233
  * @returns [{ id, slug, revision }]
203
234
  */
204
235
  async function bulkCreateProducts(ctx, products) {
236
+ const fileIds = await Promise.all(products.map((p) =>
237
+ p.digitalFileUrl ? uploadDigitalFile(ctx, p.digitalFileUrl, digitalFileName(p)) : null));
205
238
  const body = {
206
239
  returnEntity: true,
207
240
  products: products.map((p, i) => ({
208
241
  name: p.name,
209
- productType: "PHYSICAL",
210
- physicalProperties: {},
242
+ // DIGITAL drops physicalProperties and can't be POS-visible (DIGITAL_PRODUCT_CANNOT_BE_VISIBLE_IN_POS).
243
+ ...(fileIds[i]
244
+ ? { productType: "DIGITAL" }
245
+ : { productType: "PHYSICAL", physicalProperties: {}, visibleInPos: true }),
211
246
  visible: true,
212
- visibleInPos: true,
213
247
  description: mkDesc(p.description, i),
214
248
  options: buildOptions(p.options),
215
- variantsInfo: { variants: expandVariants(p.options, p) },
249
+ variantsInfo: { variants: expandVariants(p.options, p, fileIds[i]) },
216
250
  })),
217
251
  };
218
252
  const r = await req(ctx, "/stores/v3/bulk/products-with-inventory/create", { body });
@@ -221,6 +255,7 @@ async function bulkCreateProducts(ctx, products) {
221
255
  id: x.item?.id, slug: x.item?.slug, revision: x.item?.revision,
222
256
  variantId: x.item?.variantsInfo?.variants?.[0]?.id,
223
257
  hasOptions: (products[i]?.options?.length ?? 0) > 0,
258
+ isDigital: !!fileIds[i],
224
259
  quantity: products[i]?.quantity ?? 0,
225
260
  }));
226
261
  await stockOptionlessProducts(ctx, created);
@@ -233,7 +268,7 @@ async function bulkCreateProducts(ctx, products) {
233
268
  // are already stocked by the create above, so they're skipped. Backfills the default variantId from a
234
269
  // query if the create response didn't return it.
235
270
  async function stockOptionlessProducts(ctx, created) {
236
- const need = created.filter((p) => !p.hasOptions && p.id);
271
+ const need = created.filter((p) => !p.hasOptions && !p.isDigital && p.id); // digital variants ship inStock from the create
237
272
  if (!need.length) return;
238
273
  const missing = need.filter((p) => !p.variantId).map((p) => p.id);
239
274
  if (missing.length) {
@@ -300,7 +335,8 @@ async function attachProductImages(ctx, items) {
300
335
  * the DEFAULT path — call it once instead of the individual functions.
301
336
  *
302
337
  * @param plan {{
303
- * products: [{ name, description, price, compareAtPrice?, quantity, options?, imageUrl?, altText? }],
338
+ * products: [{ name, description, price, compareAtPrice?, quantity, options?, imageUrl?, altText?,
339
+ * digitalFileUrl?, digitalFileName? }],
304
340
  * categories?: { [categoryName]: string[] }, // map of category name -> product NAMES in it
305
341
  * }}
306
342
  * @returns { products: [{id,slug,revision,name}], categories: [{id,name}], imagesAttached: number }
@@ -1,110 +0,0 @@
1
- // Reference implementation of a product grid tile built on useProductCard.
2
- // Styled with base44 design tokens (shadcn Tailwind classes: bg-card / text-foreground / border-border).
3
- // Build your own card on useProductCard instead — read this for inspiration, don't use it directly.
4
- import { useState } from "react";
5
- import { Link } from "react-router-dom";
6
- import { useCart } from "@/context/CartContext";
7
- import { useProductCard } from "@/hooks/useProductCard";
8
-
9
- const badge = "px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide rounded-full";
10
- const badgePromo = `${badge} bg-primary text-primary-foreground`;
11
- const badgeInfo = `${badge} bg-card text-foreground border border-border`;
12
- const badgeBlocked = `${badge} bg-destructive text-destructive-foreground`;
13
-
14
- const badgeClass = { "pre-order": badgeInfo, "sold-out": badgeBlocked, "limited-stock": badgeInfo };
15
-
16
- export default function ProductCard({ product }) {
17
- const { addToCart } = useCart();
18
- const [adding, setAdding] = useState(false);
19
-
20
- const {
21
- isSoldOut, isPreorder, leftBadges, promoBadge,
22
- priceDisplay, compareAtDisplay,
23
- colors, optionLabel,
24
- isQuickAddable, image, hoverImage,
25
- } = useProductCard(product);
26
-
27
- const quickAdd = async () => {
28
- setAdding(true);
29
- await addToCart(product.id);
30
- setAdding(false);
31
- };
32
-
33
- return (
34
- <div className="group relative flex flex-col bg-card border border-border rounded-lg overflow-hidden shadow-sm">
35
- <div className="relative aspect-square bg-background">
36
- <Link to={`/product/${product.slug}`} aria-label={product.name} className="block w-full h-full">
37
- {image ? (
38
- <>
39
- <img src={image} alt={product.name} loading="lazy"
40
- className={`w-full h-full object-cover ${hoverImage ? "transition-opacity duration-300 md:group-hover:opacity-0" : ""}`} />
41
- {hoverImage && (
42
- <img src={hoverImage} alt="" aria-hidden="true" loading="lazy"
43
- className="hidden md:block absolute inset-0 w-full h-full object-cover opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
44
- )}
45
- </>
46
- ) : (
47
- <div className="w-full h-full bg-muted flex items-center justify-center" aria-hidden="true">
48
- <svg viewBox="0 0 24 24" className="w-8 h-8 text-muted-foreground/40" fill="none" stroke="currentColor" strokeWidth="1.5">
49
- <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" />
50
- </svg>
51
- </div>
52
- )}
53
- </Link>
54
-
55
- <div className="absolute top-2 left-2 flex flex-col items-start gap-1 pointer-events-none">
56
- {leftBadges.map((b) => (
57
- <span key={b.type} className={badgeClass[b.type]}>{b.label}</span>
58
- ))}
59
- </div>
60
- <div className="absolute top-2 right-2 pointer-events-none">
61
- {promoBadge && <span className={badgePromo}>{promoBadge.label}</span>}
62
- </div>
63
-
64
- {(!isSoldOut || isPreorder) && (
65
- <div className="absolute inset-x-2 bottom-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
66
- {isQuickAddable ? (
67
- <button onClick={quickAdd} disabled={adding}
68
- className="w-full py-2.5 px-4 rounded-full bg-primary text-primary-foreground border-none text-sm font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-wait">
69
- {adding ? "Adding…" : "Quick add"}
70
- </button>
71
- ) : (
72
- <Link to={`/product/${product.slug}`}
73
- className="block w-full text-center py-2.5 px-4 rounded-full bg-primary text-primary-foreground no-underline text-sm font-semibold">
74
- {isSoldOut ? "Pre-order" : "Choose options"}
75
- </Link>
76
- )}
77
- </div>
78
- )}
79
- </div>
80
-
81
- <div className="p-3 flex flex-col gap-1">
82
- <div className="flex flex-col gap-1 md:flex-row md:items-baseline md:justify-between md:gap-3">
83
- <h3 className="m-0 font-display text-[15px] font-semibold line-clamp-2">
84
- <Link to={`/product/${product.slug}`} className="no-underline text-foreground">{product.name}</Link>
85
- </h3>
86
- <div className="flex gap-2 items-baseline flex-wrap md:justify-end">
87
- <span className="font-semibold whitespace-nowrap">{priceDisplay}</span>
88
- {compareAtDisplay && (
89
- <span className="text-muted-foreground line-through text-[13px] whitespace-nowrap">{compareAtDisplay}</span>
90
- )}
91
- </div>
92
- </div>
93
-
94
- {(colors.length > 0 || optionLabel) && (
95
- <div className="flex items-center gap-2 text-[13px] text-muted-foreground min-h-[18px]">
96
- {colors.length > 0 && (
97
- <span className="flex items-center gap-1" aria-label={`${colors.length} colors available`}>
98
- {colors.slice(0, 5).map((c, i) => (
99
- <span key={`${c}-${i}`} className="w-3 h-3 rounded-full ring-1 ring-inset ring-black/20" style={{ backgroundColor: c }} />
100
- ))}
101
- {colors.length > 5 && <span>+{colors.length - 5}</span>}
102
- </span>
103
- )}
104
- {optionLabel && <span>{optionLabel}</span>}
105
- </div>
106
- )}
107
- </div>
108
- </div>
109
- );
110
- }
@@ -1,51 +0,0 @@
1
- // PDP image gallery — main image plus thumbnails from productGallery(product), which puts
2
- // media.main first and drops the duplicate of it that media.itemsInfo.items carries. A product with a
3
- // single image renders just the main frame, so this is safe for a one-photo catalog.
4
- // Styled with base44 design tokens (shadcn Tailwind classes).
5
- import { useEffect, useState } from "react";
6
-
7
- export default function ProductGallery({ images, name, focusUrl }) {
8
- const [index, setIndex] = useState(0);
9
- // Reset when navigating between products — the component stays mounted across a slug change.
10
- // `images` must keep a stable identity per product (the PDP memoises it), or every parent render —
11
- // a quantity bump, an option click — would land here and throw away the image the buyer picked.
12
- useEffect(() => { setIndex(0); }, [images]);
13
-
14
- // Picking a colour swatch shows that colour: focusUrl is the choice's linked photo. Runs after the
15
- // reset effect, so a first paint with a colour pre-selected opens on the right image.
16
- useEffect(() => {
17
- const i = images?.findIndex((img) => img.url === focusUrl) ?? -1;
18
- if (i >= 0) setIndex(i);
19
- }, [focusUrl, images]);
20
-
21
- const current = images?.[index];
22
-
23
- return (
24
- <div className="flex flex-col gap-3">
25
- <div className="aspect-square bg-card rounded-lg overflow-hidden">
26
- {current ? (
27
- <img src={current.url} alt={current.altText || name} className="w-full h-full object-cover" />
28
- ) : (
29
- <div className="w-full h-full bg-muted flex items-center justify-center" aria-hidden="true">
30
- <svg viewBox="0 0 24 24" className="w-10 h-10 text-muted-foreground/40" fill="none" stroke="currentColor" strokeWidth="1.5">
31
- <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" />
32
- </svg>
33
- </div>
34
- )}
35
- </div>
36
-
37
- {images?.length > 1 && (
38
- <div className="flex gap-2 flex-wrap" role="group" aria-label={`${name} images`}>
39
- {images.map((img, i) => (
40
- <button key={img.url} onClick={() => setIndex(i)} aria-label={`Show image ${i + 1}`} aria-current={i === index}
41
- className={`w-16 h-16 rounded-sm overflow-hidden cursor-pointer bg-card border ${
42
- i === index ? "border-primary" : "border-border"
43
- }`}>
44
- <img src={img.url} alt="" loading="lazy" className="w-full h-full object-cover" />
45
- </button>
46
- ))}
47
- </div>
48
- )}
49
- </div>
50
- );
51
- }
@@ -1,49 +0,0 @@
1
- // Reference implementation of a product grid — 2-col mobile, auto-fill desktop, with a skeleton
2
- // loading state and a descriptive empty state. Build your own layout instead (see INSTRUCTIONS.md
3
- // STEP 3) — read this for the skeleton/empty-state patterns, don't use it directly.
4
- import ProductCard from "./ProductCard";
5
-
6
- // Skeleton tiles match ProductCard's aspect-square + two text lines, so nothing shifts when the real
7
- // products land.
8
- export function ProductGridSkeleton({ count = 8 }) {
9
- return (
10
- <div className="grid gap-4 grid-cols-2 md:[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]" aria-hidden="true">
11
- {Array.from({ length: count }).map((_, i) => (
12
- <div key={i} className="bg-card border border-border rounded-lg overflow-hidden">
13
- <div className="aspect-square bg-muted animate-pulse" />
14
- <div className="p-3 flex flex-col gap-2">
15
- <div className="flex justify-between gap-3">
16
- <div className="h-3.5 w-1/2 bg-muted rounded-sm animate-pulse" />
17
- <div className="h-3.5 w-1/5 bg-muted rounded-sm animate-pulse" />
18
- </div>
19
- <div className="h-3 w-1/3 bg-muted rounded-sm animate-pulse" />
20
- </div>
21
- </div>
22
- ))}
23
- </div>
24
- );
25
- }
26
-
27
- export default function ProductGrid({ products, loading, empty = "No products yet.", emptyHint }) {
28
- if (loading) return <ProductGridSkeleton />;
29
-
30
- if (!products?.length) {
31
- return (
32
- <div className="flex flex-col items-center justify-center text-center gap-3 py-16 px-4">
33
- <svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"
34
- className="text-muted-foreground/40" aria-hidden="true">
35
- <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><path d="M3 6h18" />
36
- <path d="M16 10a4 4 0 0 1-8 0" />
37
- </svg>
38
- <h2 className="font-display m-0 text-lg font-semibold">{empty}</h2>
39
- {emptyHint && <p className="m-0 text-muted-foreground max-w-[46ch]">{emptyHint}</p>}
40
- </div>
41
- );
42
- }
43
-
44
- return (
45
- <div className="grid gap-4 grid-cols-2 md:[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]">
46
- {products.map((p) => <ProductCard key={p.id} product={p} />)}
47
- </div>
48
- );
49
- }
@@ -1,92 +0,0 @@
1
- // PDP choice controls built on useVariantOptions — swap this component for your own if you want
2
- // a different look; the data contract (optionGroups / modifierGroups) stays the same.
3
- // Styled with base44 design tokens (shadcn Tailwind classes).
4
- import { useVariantOptions } from "@/hooks/useVariantOptions";
5
-
6
- const chipBase = "py-1.5 px-3 cursor-pointer text-sm font-body border rounded-sm";
7
- const chipIdle = "border-border bg-card text-foreground";
8
- const chipActive = "border-primary bg-primary text-primary-foreground";
9
- const label = "block mb-1.5 text-[13px] font-semibold text-muted-foreground";
10
-
11
- export default function VariantPicker({ options, modifiers, selectedOptions, selectOption, modifierValues, setModifier }) {
12
- const { optionGroups, modifierGroups } = useVariantOptions(options, modifiers, selectedOptions, modifierValues);
13
-
14
- return (
15
- <>
16
- {optionGroups.map((group) => (
17
- <div key={group.id} className="mb-4">
18
- <label className={label}>
19
- {group.name}
20
- {group.isColor && (
21
- <span className="ml-1.5 font-normal text-foreground">
22
- {group.choices.find((c) => c.selected)?.name}
23
- </span>
24
- )}
25
- </label>
26
- <div className="flex flex-wrap gap-2">
27
- {group.choices.map((c) =>
28
- c.isColorSwatch ? (
29
- <button
30
- key={c.choiceId}
31
- type="button"
32
- disabled={!c.inStock}
33
- onClick={() => selectOption(group.id, c.choiceId)}
34
- aria-pressed={c.selected}
35
- aria-label={`${c.name}${!c.inStock ? " (out of stock)" : ""}`}
36
- title={c.name}
37
- className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
38
- c.selected ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
39
- } ${!c.inStock ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
40
- style={{ backgroundColor: c.colorCode }}
41
- >
42
- {!c.inStock && (
43
- <span aria-hidden="true" className="absolute inset-0 grid place-items-center">
44
- <span className="w-full h-px bg-destructive rotate-45" />
45
- </span>
46
- )}
47
- </button>
48
- ) : (
49
- <button
50
- key={c.choiceId}
51
- type="button"
52
- disabled={!c.inStock}
53
- aria-pressed={c.selected}
54
- onClick={() => selectOption(group.id, c.choiceId)}
55
- className={`${chipBase} ${c.selected ? chipActive : chipIdle} ${!c.inStock ? "opacity-40 line-through" : ""}`}
56
- >
57
- {c.name}
58
- </button>
59
- )
60
- )}
61
- </div>
62
- </div>
63
- ))}
64
-
65
- {modifierGroups.map((m) => (
66
- <div key={m.key} className="mb-4">
67
- <label className={label}>{m.name}{m.mandatory && " *"}</label>
68
- {m.type === "text" ? (
69
- <input
70
- value={m.value}
71
- onChange={(e) => setModifier(m.key, e.target.value)}
72
- className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground"
73
- />
74
- ) : (
75
- <div className="flex flex-wrap gap-2">
76
- {m.choices.map((c) => (
77
- <button
78
- key={c.key}
79
- aria-pressed={c.selected}
80
- onClick={() => setModifier(m.key, c.key)}
81
- className={`${chipBase} ${c.selected ? chipActive : chipIdle}`}
82
- >
83
- {c.name}
84
- </button>
85
- ))}
86
- </div>
87
- )}
88
- </div>
89
- ))}
90
- </>
91
- );
92
- }
@@ -1,139 +0,0 @@
1
- // PDP — thin view over useProductDetail (all logic lives in the hook). plainDescription is HTML
2
- // despite the name; render via dangerouslySetInnerHTML. Styled with base44 design tokens (shadcn Tailwind classes).
3
- //
4
- // The price shown follows the selection: once options resolve to a variant the hook returns that
5
- // variant's price, so a picked size can cost more than the "from" figure the grid tile showed.
6
- import { useMemo } from "react";
7
- import { Link, useParams } from "react-router-dom";
8
- import { useProductDetail } from "@/hooks/useProductDetail";
9
- import { productGallery } from "@/lib/storeImage";
10
- import ProductGallery from "@/components/ProductGallery";
11
- import VariantPicker from "@/components/VariantPicker";
12
-
13
- export default function ProductDetail() {
14
- const { slug } = useParams();
15
- const d = useProductDetail(slug);
16
- // Memoised so the gallery keeps its selected image across re-renders (see ProductGallery).
17
- const images = useMemo(() => productGallery(d.product), [d.product]);
18
-
19
- if (d.error) {
20
- return (
21
- <Centered>
22
- <p className="m-0">{d.error}</p>
23
- <button onClick={d.retry} className="border border-border bg-card text-foreground rounded-sm py-2 px-4 cursor-pointer text-sm">Try again</button>
24
- </Centered>
25
- );
26
- }
27
- if (d.notFound) {
28
- return (
29
- <Centered>
30
- <p className="m-0">We couldn't find that product.</p>
31
- <Link to="/shop" className="text-primary">Back to shop</Link>
32
- </Centered>
33
- );
34
- }
35
- if (!d.product) return <ProductDetailSkeleton />;
36
-
37
- const compareAt = d.product?.compareAtPriceRange?.minValue?.formattedAmount;
38
-
39
- return (
40
- <main className="max-w-[1200px] mx-auto p-4">
41
- <nav aria-label="Breadcrumb" className="mb-4 text-sm text-muted-foreground">
42
- <Link to="/shop" className="text-muted-foreground hover:text-foreground">Shop</Link>
43
- <span aria-hidden="true"> / </span>
44
- <span className="text-foreground">{d.product.name}</span>
45
- </nav>
46
-
47
- <div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
48
- <ProductGallery images={images} name={d.product.name} focusUrl={d.focusMediaUrl} />
49
-
50
- <div>
51
- <h1 className="font-display m-0 mb-2">{d.product.name}</h1>
52
-
53
- <div className="flex items-baseline gap-2 mb-4">
54
- <p className="text-[22px] font-semibold m-0">{d.price}</p>
55
- {compareAt && compareAt !== d.price && (
56
- <span className="text-muted-foreground line-through">{compareAt}</span>
57
- )}
58
- </div>
59
-
60
- {d.product.plainDescription && (
61
- <div className="text-muted-foreground leading-relaxed mb-6"
62
- dangerouslySetInnerHTML={{ __html: d.product.plainDescription }} />
63
- )}
64
-
65
- <VariantPicker
66
- options={d.options} modifiers={d.modifiers}
67
- selectedOptions={d.selectedOptions} selectOption={d.selectOption}
68
- modifierValues={d.modifierValues} setModifier={d.setModifier}
69
- />
70
-
71
- {/* Tell the buyer WHY the button is dead: an unresolvable combination reads as a broken page. */}
72
- {d.options.length > 0 && !d.variant && (
73
- <p className="text-[13px] text-muted-foreground m-0 mt-1">Pick an option from each list to continue.</p>
74
- )}
75
-
76
- <div className="flex items-stretch gap-3 mt-4">
77
- <QuantityStepper value={d.quantity} onChange={d.setQuantity} disabled={!d.inStock} />
78
- <button disabled={!d.canAdd || d.adding} onClick={d.submit}
79
- className="flex-1 h-12 px-6 bg-primary text-primary-foreground border-none rounded-md text-[15px] font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed">
80
- {d.adding ? "Adding…" : d.inStock ? "Add to cart" : "Out of stock"}
81
- </button>
82
- </div>
83
- </div>
84
- </div>
85
- </main>
86
- );
87
- }
88
-
89
- // Segmented −/+ stepper, height-matched to the Add-to-cart button so the row reads as one control.
90
- // A native number input is avoided on purpose: its spinners are ~10px, OS-rendered (so they ignore
91
- // the app's tokens), effectively untappable, and they accept "e"/"+"/"1e5" plus scroll-wheel edits.
92
- // The value stays typeable — inputMode="numeric" brings up a keypad — but only digits survive, and
93
- // blur restores 1 when the field is left empty. `tabular-nums` keeps the width steady across digits.
94
- function QuantityStepper({ value, onChange, disabled }) {
95
- const set = (n) => onChange(Math.max(1, n));
96
- return (
97
- <div data-disabled={disabled || undefined}
98
- className="inline-flex items-stretch h-12 rounded-md border border-input bg-background overflow-hidden data-[disabled]:opacity-50">
99
- <StepButton label="Decrease quantity" disabled={disabled || value <= 1} onClick={() => set(value - 1)}>−</StepButton>
100
- <label className="sr-only" htmlFor="pdp-qty">Quantity</label>
101
- <input id="pdp-qty" type="text" inputMode="numeric" autoComplete="off" disabled={disabled}
102
- value={value} aria-live="polite"
103
- onChange={(e) => { const n = e.target.value.replace(/\D/g, ""); onChange(n === "" ? "" : Math.max(1, Number(n))); }}
104
- onBlur={(e) => { if (!e.target.value) set(1); }}
105
- className="w-12 text-center bg-transparent text-foreground font-semibold tabular-nums border-x border-input outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed" />
106
- <StepButton label="Increase quantity" disabled={disabled} onClick={() => set(Number(value || 0) + 1)}>+</StepButton>
107
- </div>
108
- );
109
- }
110
-
111
- function StepButton({ children, label, onClick, disabled }) {
112
- return (
113
- <button type="button" onClick={onClick} disabled={disabled} aria-label={label}
114
- className="w-11 grid place-items-center text-lg leading-none text-muted-foreground bg-transparent border-none cursor-pointer transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent">
115
- {children}
116
- </button>
117
- );
118
- }
119
-
120
- function ProductDetailSkeleton() {
121
- return (
122
- <main className="max-w-[1200px] mx-auto p-4" aria-busy="true">
123
- <div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
124
- <div className="aspect-square bg-muted rounded-lg animate-pulse" />
125
- <div className="flex flex-col gap-3">
126
- <div className="h-7 w-2/3 bg-muted rounded-sm animate-pulse" />
127
- <div className="h-6 w-24 bg-muted rounded-sm animate-pulse" />
128
- <div className="h-4 w-full bg-muted rounded-sm animate-pulse mt-3" />
129
- <div className="h-4 w-5/6 bg-muted rounded-sm animate-pulse" />
130
- <div className="h-11 w-full bg-muted rounded-sm animate-pulse mt-6" />
131
- </div>
132
- </div>
133
- </main>
134
- );
135
- }
136
-
137
- function Centered({ children }) {
138
- return <div className="p-12 flex flex-col items-center gap-3 text-center text-muted-foreground">{children}</div>;
139
- }