@wix/agent-skills 1.20.0 → 1.21.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 (183) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +14 -112
  3. package/skills/wix-app/references/APP_TOOLS.md +2 -1
  4. package/skills/wix-app/references/BACKEND_EVENT.md +3 -3
  5. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +41 -152
  6. package/skills/wix-app/references/DASHBOARD_MODAL.md +4 -4
  7. package/skills/wix-app/references/SDK_MODULE_MAP.md +44 -0
  8. package/skills/wix-app/references/SITE_PLUGIN.md +36 -190
  9. package/skills/wix-app/references/app-tools/TOOLS.md +3 -1
  10. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +21 -0
  11. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +17 -0
  12. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +4 -18
  13. package/skills/wix-app/references/site-plugin/EXAMPLES.md +1 -1
  14. package/skills/wix-headless-fast/SKILL.md +302 -51
  15. package/skills/wix-headless-fast/entry/skill.md +11 -2
  16. package/skills/wix-headless-fast/install/AGENTS.md +32 -0
  17. package/skills/wix-headless-fast/install/agents-md.mjs +39 -0
  18. package/skills/wix-headless-fast/install/attach.mjs +360 -0
  19. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  20. package/skills/wix-headless-fast/install/setup.mjs +239 -0
  21. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +226 -60
  22. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  23. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  24. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  25. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  26. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  27. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  28. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  29. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  30. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  32. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  33. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  34. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  35. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  36. package/skills/wix-headless-fast/references/blog/seed/read-site.mjs +22 -0
  37. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +181 -33
  38. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  39. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  40. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  41. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  42. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  43. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  44. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  48. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  49. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  50. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  51. package/skills/wix-headless-fast/references/bookings/seed/read-site.mjs +28 -0
  52. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +238 -62
  53. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  54. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  55. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  56. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  57. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  59. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  60. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  61. package/skills/wix-headless-fast/references/cms/seed/read-site.mjs +20 -0
  62. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +241 -83
  63. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  64. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  66. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  67. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  68. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  69. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  71. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  74. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  75. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  76. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  77. package/skills/wix-headless-fast/references/events/seed/read-site.mjs +26 -0
  78. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +216 -26
  79. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  80. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  85. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  86. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  87. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  88. package/skills/wix-headless-fast/references/forms/seed/read-site.mjs +20 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +316 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/members/seed/read-site.mjs +15 -0
  100. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +240 -59
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  104. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  107. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  112. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  113. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  114. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  115. package/skills/wix-headless-fast/references/portfolio/seed/read-site.mjs +20 -0
  116. package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +19 -21
  117. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +214 -51
  118. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  119. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  120. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  122. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  126. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  127. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  129. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  130. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  131. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  132. package/skills/wix-headless-fast/references/pricing-plans/seed/read-site.mjs +21 -0
  133. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +242 -47
  134. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  135. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  136. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  137. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  142. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  143. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  144. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  145. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  146. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  147. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  148. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  149. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  150. package/skills/wix-headless-fast/references/restaurants/seed/read-site.mjs +25 -0
  151. package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +25 -5
  152. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  153. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  154. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  155. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  156. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  157. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  158. package/skills/wix-headless-fast/references/shared/seed/read-site.mjs +99 -0
  159. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +186 -63
  160. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  161. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  162. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  163. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  166. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +314 -0
  167. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  168. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  169. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  170. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +2 -0
  171. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  172. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  173. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +31 -9
  174. package/skills/wix-headless-fast/references/storefront/seed/read-site.mjs +38 -0
  175. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +57 -50
  176. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  177. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-coupon.md +10 -19
  178. package/skills/wix-manage/references/events/create-wix-event.md +5 -1
  179. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  180. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  181. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  182. package/skills/wix-headless-fast/install/fast-path.mjs +0 -186
  183. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -8,25 +8,31 @@ be exact. You never write commerce code; you never skip designing the store.
8
8
 
9
9
  ## The file map (deployed into `src/`)
10
10
 
11
- **Don't read the shipped files** — this table and the contracts below are everything you
12
- need. Open a shipped file's source **only** on a real fallback: a runtime error, or a field
13
- this playbook doesn't cover. Files you edit: `SiteLayout.astro` and `styles/global.css`.
11
+ **On Astro and React the shipped files are tested and work as they are** — this table and the
12
+ contracts below are everything you need to use them, so don't spend the run reading their source;
13
+ wire them and build your surfaces. Reading them is the right move when something is off (a runtime
14
+ error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
15
+ then read the file that owns it and change or extend it. On `lib`, `static`, and a port the
16
+ components don't deploy at all, and each wiring section below opens with the files to read before
17
+ writing their equivalents. Files you edit: `SiteLayout.astro` and `styles/global.css`.
14
18
  Files you **create** (skeletons below): the shop, category, and PDP pages with their island
15
19
  components, plus your home page.
16
20
 
17
21
  | file | what it is |
18
22
  |---|---|
19
23
  | `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
20
- | `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `imgSrcSet()` / `formatMoney()` — already used by everything shipped; `imgSrcSet` + `sizes` for responsive tiles |
24
+ | `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(p.imageUrl, "25vw")} alt={p.name} />`; `imgSrc()` / `imgSrcSet()` / `formatMoney()` underneath, already used by everything shipped |
21
25
  | `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`, `Facet`) — contracts inlined below |
22
- | `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/facets/search + cursor paging + result count, all server-side), `fetchFacets`, `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` |
26
+ | `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/facets/search + cursor paging + result count, all server-side), `fetchFacets`, `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` — the transport; the rules and DTO mappers are in `catalog-core.ts` / `cart-core.ts` beside it (shared with the REST layer) |
23
27
  | `wix/storefront/cart.ts` · `cart-store.ts` | Cart V2 + shared cart state (module store — spans Astro islands) |
28
+ | `wix/storefront/shop-store.ts` · `product-detail-store.ts` | the listing and product-detail state machines, framework-free (`createShopStore()`, `createProductDetailStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
24
29
  | `hooks/storefront/useCart.ts` | cart state + actions — contract below |
25
- | `hooks/storefront/useShop.ts` | listing: category scope, sort, filters, option facets, result count, paging — contract below |
26
- | `hooks/storefront/useProductDetail.ts` | option selection → variant resolution → add-to-cart — contract below |
30
+ | `hooks/storefront/useShop.ts` | React binding of `shop-store.ts`: category scope, sort, filters, option facets, result count, paging — contract below |
31
+ | `hooks/storefront/useProductDetail.ts` | React binding of `product-detail-store.ts`: option selection → variant resolution → add-to-cart — contract below |
27
32
  | `components/storefront/CartButton.tsx` · `CartDrawer.tsx` | header badge + slide-over cart — **wire as-is** (drawer once per page) |
28
33
  | `components/storefront/FilterPanel.tsx` | the gallery's filter LAYOUT — toolbar (result count, sort), active chips, then a 16rem sidebar of collapsible groups (price as a two-handle slider bounded by the catalog's real prices, availability, one group per option facet with swatches/pills) beside YOUR results; a bottom sheet under `md` — **wire as-is** in your `ShopView`, your grid as its children: `<FilterPanel shop={shop}>…grid…</FilterPanel>` |
29
34
  | `components/storefront/QuickAdd.tsx` | the tile's purchase control — one click for a product with no options, a picker anchored to the tile (bottom sheet on small screens) for one with options, the product page for free-text customization — **wire as-is** as the last row of every tile's text block (`<QuickAdd product={p} />`) |
35
+ | `components/storefront/OptionPicker.tsx` | the purchase controls for one product — option groups (swatches/pills, sold-out choices disabled), choice and text modifiers, an optional quantity stepper, the buy button gated by `useProductDetail` with its plain reason, "Pre-order" when pre-orderable — **wire as-is** in your PDP (`<OptionPicker detail={d} showQuantity />`); QuickAdd's picker is this same component |
30
36
  | `components/storefront/ShopView.tsx` · `ProductDetailView.tsx` | **don't ship — YOU create them** (skeletons below): the client islands your shop, category, and PDP pages mount |
31
37
  | `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (colors, radii, fonts — same token family as the official Wix templates). Everything, shipped and yours, styles from these tokens |
32
38
 
@@ -74,7 +80,9 @@ of its category. Then, by default:
74
80
  - **Home:** what the store sells and one shopping action in the first screen; real products under
75
81
  truthful headings ("Best Sellers" needs data behind it); not a repeat of the shop page. A
76
82
  category tile or link on the home page goes to that category's page (`/category/<slug>`) —
77
- never to an anchor on the shop page that every tile shares.
83
+ never to an anchor on the shop page that every tile shares — and its image is a product from
84
+ THAT category (fetched with the category filter) or the category's own image, never a
85
+ positional guess into the all-products list.
78
86
  - **Shop:** a real product card — image, name, price, link — in the first screen; the shipped
79
87
  `FilterPanel` around the grid (toolbar, then a filter sidebar beside the results on desktop and
80
88
  a sheet on phones) — a store with any filterable catalog ships it, not "when it fits"; the
@@ -88,7 +96,12 @@ of its category. Then, by default:
88
96
  - **Cart:** the shipped drawer — it opens after every add, and checkout is a button in it.
89
97
  - **Overlays you build** (quick-add, mobile nav, filters): mount at the document root (a fixed
90
98
  panel inside the `backdrop-blur` header gets clipped), lock background scroll, close on Escape,
91
- return focus on close — as the shipped `CartDrawer` does.
99
+ return focus on close — as the shipped `CartDrawer` does. An anchored picker is positioned
100
+ inside its tile, not `fixed` with computed offsets. If you add dismiss-on-outside-click, decide
101
+ inside/outside before anything re-renders (a capture-phase listener), or a click on a swatch
102
+ that re-renders the panel reads as "outside" and closes it.
103
+ - **A page whose slug resolves to nothing** (category, product) shows only the not-found state —
104
+ no heading, toolbar, or empty grid rendered around it.
92
105
  - **Copy:** nothing the merchant didn't supply — no invented reviews, scarcity, or delivery
93
106
  promises; no Wix IDs or technical words in visible text.
94
107
 
@@ -160,6 +173,10 @@ only when the catalog has them — never fabricated; `wix-docs` has their contra
160
173
 
161
174
  ### The pages and islands you create — skeletons
162
175
 
176
+ The class names in these skeletons are the Astro/React spelling of layout rules that hold on
177
+ every stack, and each rule is also named in words beside its classes. On a stack where the shipped
178
+ components don't deploy (`lib`, `static`, a port), keep the rule and write it in your own CSS.
179
+
163
180
  Nothing renders until you write these — the store IS your work. Each page is a thin SSR shell
164
181
  (fetch → DTO props → island); each island is a thin view over a hook. The pages' frontmatter
165
182
  is **machinery, not design** — reproduce it as the skeletons show, exactly. Hooks first,
@@ -308,6 +325,7 @@ if (!product) {
308
325
  import { useShop } from "../../hooks/storefront/useShop";
309
326
  import FilterPanel from "./FilterPanel";
310
327
  import QuickAdd from "./QuickAdd";
328
+ import { imgAttrs } from "../../wix/media";
311
329
  import type { Category, ProductSummary } from "../../wix/storefront/types";
312
330
 
313
331
  export default function ShopView(props: {
@@ -329,23 +347,37 @@ export default function ShopView(props: {
329
347
  // • error → a short inline message (retry() re-runs the query)
330
348
  // • products === null (or loading) → skeleton tiles; [] → your honest empty state, and a
331
349
  // distinct "no products match these filters" with clearFilters() when hasActiveFilters
332
- // • else YOUR grid of YOUR tiles (ProductSummary contract above): image (hoverImageUrl on
333
- // hover; imgSrcSet + sizes for responsive delivery), name, price — a range when
350
+ // • else YOUR grid of YOUR tiles (ProductSummary contract above): image via
351
+ // <img {...imgAttrs(p.imageUrl, "(min-width: 768px) 25vw, 50vw")} alt={p.name} /> — src,
352
+ // srcSet, sizes and lazy loading in one spread, so a tile never ships srcSet without src;
353
+ // an empty imageUrl gives {} — render your placeholder then; hoverImageUrl
354
+ // on hover — name, price — a range when
334
355
  // price !== maxPrice, else price + labelled compareAtPrice — EVERY ribbon from ribbons,
335
356
  // swatches as small color dots when present (else optionsSummary as text); tile links to
336
357
  // `/products/${p.slug}`; and <QuickAdd product={p} /> as the LAST ROW of the tile's text
337
358
  // block, under name and price, full width — the shipped buy control (direct add / option
338
359
  // picker / product page, decided from the product). It is a direct child of the tile root,
339
360
  // which carries `relative`: the picker anchors to that root and takes the tile's width.
340
- // Never overlay the control on the image or float it between image and text, and never
341
- // wrap it in a smaller positioned box (the picker would inherit that box's width).
361
+ // THE TILE ROOT IS A <div className="relative flex flex-col">, NOT THE LINK: the <a> wraps
362
+ // the image and the name/price, and <QuickAdd> sits beside it as the flex column's last child
363
+ // (it pins itself to the bottom with mt-auto). A button inside an <a> is invalid HTML and its
364
+ // click navigates to the product page instead of adding.
365
+ // • ROW RHYTHM: the grid stretches every tile in a row to the tallest; with the tile a flex
366
+ // column and the control pinned to the bottom, the buy buttons share one baseline across the
367
+ // row even when one tile has swatches, a struck price, or a two-line name and its neighbours
368
+ // don't. Put the swatches between price and control; never let them push only that tile's
369
+ // button down. Never overlay the control on the image or float it
370
+ // between image and text, and never wrap it in a smaller positioned box (the picker would
371
+ // inherit that box's width).
372
+ // • the name WRAPS (`min-w-0`, `break-words`) — no `truncate` / `line-clamp-1`: a shopper reads
373
+ // "Red Velvet Cupcake 4-Pack", not "Red Velvet Cupc…"; price on its own line under it.
342
374
  // • badges come ONLY from p.ribbons. Do NOT render a "Sale" badge because compareAtPrice
343
375
  // is set — the struck price already says it, and a product the merchant ribboned "Sale"
344
376
  // would show the badge twice.
345
- // • name and price on SEPARATE lines (`min-w-0`, the name may wrap) — never one flex row
346
- // where a long name and a price range fight for width and the price gets clipped at 390px.
347
- // Keep the page intro short enough that a full tile (image, name, price) is in the first
348
- // screen, also on a short desktop window.
377
+ // • name and price on SEPARATE lines — never one flex row where a long name and a price
378
+ // range fight for width and the price gets clipped at 390px. Keep the page intro short
379
+ // enough that a full tile (image, name, price) is in the first screen, also on a short
380
+ // desktop window.
349
381
  // • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
350
382
  }
351
383
  ```
@@ -354,6 +386,7 @@ export default function ShopView(props: {
354
386
  // src/components/storefront/ProductDetailView.tsx — YOU build the whole PDP surface;
355
387
  // your [slug].astro mounts it with the server-fetched product.
356
388
  import { useProductDetail } from "../../hooks/storefront/useProductDetail";
389
+ import OptionPicker from "./OptionPicker";
357
390
  import type { ProductDetail } from "../../wix/storefront/types";
358
391
 
359
392
  export default function ProductDetailView(props: {
@@ -371,16 +404,15 @@ export default function ProductDetailView(props: {
371
404
  // to full-width images stacked down the column (products with per-color
372
405
  // linked media carry several gallery urls, so that stacks big duplicates).
373
406
  // A single-image gallery is just the one primary — no empty strip.
407
+ // When d.variant resolves and carries imageUrl, that image becomes the primary (it is
408
+ // one of the gallery urls) — a shopper who picks a color sees that color.
374
409
  // • name, EVERY ribbon (d.product.ribbons), live d.price (the range until every option
375
410
  // is picked) with d.compareAtPrice as a labelled "was" when present — never invent one;
376
411
  // descriptionHtml rendered as HTML, then d.product.infoSections as sections/accordions
377
- // • option controls from d.optionGroups → d.selectOption(optionName, choiceName)
378
- // (isColor → real swatches via colorCode; disable out-of-stock choices);
379
- // modifiers from d.product.modifiers → d.setModifier(key, value), "*" = mandatory
380
- // • quantity (d.quantity / d.setQuantity), then the buy button gated by d.canAdd
381
- // ONLY — never resolve variants yourself — calling d.add(); label it "Pre-order" when
382
- // d.isPreorder; while disabled, render d.blockedReason beside it as neutral guidance
383
- // (not error styling); d.adding disables, d.error renders inline
412
+ // • <OptionPicker detail={d} showQuantity /> right under the price — the shipped option
413
+ // groups (swatches for a color option, sold-out choices disabled), modifiers, quantity,
414
+ // and the buy button gated by the hook with its plain reason, "Pre-order" when it applies,
415
+ // the add error inline. Never resolve variants or gate the button yourself.
384
416
  // • in the first screen at mobile AND desktop: the image, name, price, the first choice,
385
417
  // and the button with its reason — a shopper decides without scrolling. On a phone that
386
418
  // means the primary image is a bounded band, not a full-height hero: e.g. the gallery
@@ -390,6 +422,31 @@ export default function ProductDetailView(props: {
390
422
  }
391
423
  ```
392
424
 
425
+ ### The reference files for stacks where the components don't deploy
426
+
427
+ On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state
428
+ machines behind the hooks do arrive — `wix/storefront/shop-store.ts`, `product-detail-store.ts`,
429
+ `cart-store.ts` — so you never rewrite them: create a store per surface, `subscribe`, render from
430
+ `getState()`, call its actions. Their `*State` interfaces are the render contract; read those.
431
+ What you write is the rendering — grid, product page, picker, filter panel, drawer — and for that
432
+ read these first; they are tested code for exactly that behaviour, and rewriting them from the
433
+ prose above is where the bugs come from:
434
+
435
+ 1. `components/storefront/QuickAdd.tsx` — the three purchase paths decided from the summary DTO;
436
+ the panel is positioned inside the tile (the tile is `relative`), a bottom sheet on small
437
+ screens, never `fixed` with computed offsets; it closes on Escape, the close button, a
438
+ successful add, or the scrim — there is NO outside-click handler (one that runs after a
439
+ re-render sees the clicked swatch detached and closes on every pick).
440
+ 2. `components/storefront/OptionPicker.tsx` — the purchase controls as working code: swatches vs
441
+ pills, sold-out choices disabled, quantity, the gated button with its reason and the
442
+ "Pre-order" label; the PDP and the tile picker share it.
443
+ 3. `components/storefront/CartDrawer.tsx` — the overlay contract as working code: root-level,
444
+ scrim, scroll lock, Escape, focus in and back.
445
+ 4. `components/storefront/FilterPanel.tsx` — inline commits at once, the sheet stages until Apply;
446
+ the price pair commits only when valid.
447
+
448
+ All under `references/storefront/app/`.
449
+
393
450
  ### Wiring — Astro (default)
394
451
 
395
452
  1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
@@ -398,6 +455,88 @@ export default function ProductDetailView(props: {
398
455
  SSR props; browser-state widgets (cart) are `client:only="react"`.
399
456
  3. Write `pages/index.astro` (home) on `SiteLayout`.
400
457
 
458
+ ### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
459
+
460
+ Read the reference files listed above before writing any surface.
461
+
462
+ `deploy.mjs storefront --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts`
463
+ (the visitor client, configured with the public client id), `media.ts`, `money.ts`, and
464
+ `wix/storefront/` — `catalog.ts`, `cart.ts`, `types.ts`, the `*-core.ts` rules, and the three
465
+ stores `shop-store.ts`, `product-detail-store.ts`, `cart-store.ts`. None of it is React. The
466
+ hooks and components don't ship on this stack; the stores replace the hooks, and you write the
467
+ components in your framework to the contracts on this page:
468
+
469
+ - bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
470
+ `subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
471
+ Solid: a signal set in `subscribe`). `createShopStore(options)` per listing (`start()` when
472
+ mounted, `stop()` when unmounted), `createProductDetailStore({ initial | slug })` per product
473
+ surface, the cart store as-is (module-level). State in, actions out — exactly the hooks'
474
+ contracts above;
475
+ - your filter panel, quick add, and cart drawer to the contracts in "What a complete storefront
476
+ shows" — the shipped `FilterPanel.tsx`, `QuickAdd.tsx`, `CartDrawer.tsx` are readable as
477
+ behaviour specs (the overlay contract, the three purchase paths, the sidebar/sheet split).
478
+
479
+ Routes `/shop`, `/category/:slug` (via `fetchCategoryBySlug`, null → your 404), `/products/:slug`;
480
+ dev server on 4321; a static build goes through `npx @wix/cli@latest release` with
481
+ `site.outputDirectory` pointing at the build folder, an SSR build is hosted by you. Item-page tags
482
+ from the entity's `seoData`.
483
+
484
+ ### Wiring — static site (`--stack static`, no bundler)
485
+
486
+ Read the reference files listed above before writing any surface.
487
+
488
+ `deploy.mjs storefront --stack static --out site` put the REST layer in `site/js/wix/` (browser
489
+ ESM, the `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` —
490
+ pages, styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project
491
+ root (config, plan, seed output) is never the upload. Same function names and DTOs as the table
492
+ above, so the contracts on this page hold unchanged: `searchCatalog`, `fetchFacetData`, `fetchProductBySlug`,
493
+ `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` from `./js/wix/catalog.js`;
494
+ `fetchCart`, `addToCart`, `updateQuantity`, `removeLine`, `checkoutUrl` from `./js/wix/cart.js`.
495
+ The state machines ship too: `createShopStore` from `./js/wix/shop-store.js` (the listing —
496
+ selection, facets, URL sync, cursor paging; `start()` once the page is up), `createProductDetailStore`
497
+ from `./js/wix/product-detail-store.js` (the PDP and every quick-add picker — selections start
498
+ empty, `resolveVariant`, `canAdd`/`blockedReason`, `add()`), and `./js/wix/cart-store.js` (the
499
+ cart, `subscribeCart`/`getCartState`, `addLine`, `updateLineQuantity`, `removeCartLine`,
500
+ `goToCheckout`, `setCartOpen`). No components ship — you write the rendering in plain JS: one
501
+ render function per surface that reads `getState()`, called from `subscribe`, with the
502
+ surface's controls calling the store's actions. The drawer opens after every add on its own;
503
+ overlays follow the CartDrawer contract (root-level, scroll lock, Escape, focus back). Pages are
504
+ `shop.html`, `category.html?slug=…`,
505
+ `product.html?slug=…`. Set `document.title` and the meta description from the entity's `seoData`
506
+ once it loads, on the product AND category pages. The visitor token persists in `localStorage` on
507
+ its own; never mint per page. `npx @wix/cli@latest release` uploads `site/`.
508
+
509
+ ### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
510
+
511
+ Read the reference files listed above before writing any surface.
512
+
513
+ Run `deploy.mjs storefront --stack static` in the project folder anyway: `js/wix/` is both the
514
+ browser-side code and the readable spec. Then split by where the call runs. **Reads on the
515
+ server:** port `js/wix/catalog.ts` and `catalog-core.ts` to your language — the same six functions
516
+ returning the same DTO shapes as dicts, one anonymous visitor token per process for these public
517
+ reads (mint and refresh per `client.ts`) — and render shop, category, and PDP in your templates to
518
+ the contracts above, so product names and prices are in the HTML; item-page tags from the entity's
519
+ `seoData`. **Buying in the browser:** the cart drawer on `./js/wix/cart-store.js`, the PDP's
520
+ option picker and the tile's quick add on `./js/wix/product-detail-store.js` (pass the product's
521
+ slug, or the rendered `ProductDetail` as `initial` in a JSON script tag), exactly as the static
522
+ wiring above — the browser owns the shopper's visitor token, so the server never handles
523
+ per-shopper tokens. Routes stay `/shop`, `/category/<slug>`, `/products/<slug>`. Add your public https origin
524
+ to the OAuth app's allowed domains before checkout can return.
525
+
526
+ **Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for
527
+ the reads, run at build time with one anonymous token; the generator must emit every product and
528
+ category page (a URL generator over `fetchCategories()` plus a full `searchCatalog` walk by
529
+ cursor — never only the first page). Run `deploy.mjs storefront --stack static --out <build dir>`
530
+ so `js/wix/` is inside the output the pages import from, point `site.outputDirectory` at that
531
+ folder, `wix release`. Pages sit at different depths (`/`, `/category/…`, `/products/…`): give
532
+ the templates one base path to `js/wix/` (a template variable, or root-relative `/js/wix/…`),
533
+ never a relative `./js/wix/` — it breaks one level down. The frozen grid is the first paint; the
534
+ shop's sort, filters, facets, search, and load-more still run client-side on it through
535
+ `createShopStore()` from `./js/wix/shop-store.js`, exactly as on a static site, so the gallery
536
+ contract above applies. Close
537
+ with the live URL, the rebuild + release command, and one line for the owner: dashboard edits to
538
+ the catalog reach the site when that command runs; cart and checkout are live regardless.
539
+
401
540
  ### Wiring — React SPA (Vite etc.)
402
541
 
403
542
  Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
@@ -409,22 +548,27 @@ client-side when no `initial` is passed).
409
548
  Mount the shipped `CartButton` in the header and `CartDrawer` once. Deploy wrote the public
410
549
  client id into `wix/config.ts`; nothing else to configure.
411
550
 
551
+ Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly — hash routes, or one HTML file per route, decided before the first route is written; any URL handed to Wix as a return target must be one the host serves (SKILL.md step 1).
552
+
412
553
  ## Hard rules
413
554
 
414
555
  - **Data and commerce logic only through the shipped exports** — never rewrite their
415
556
  internals or re-derive a request shape. Extend by calling the exports or adding a new
416
557
  function in `wix/storefront/` for what they don't cover (API contracts: the `wix-docs` skill).
417
558
  - **Selection→cart goes through `useProductDetail`** — never add a product with options by
418
- picking `variants[0]`, and never gate `canAdd` yourself. In the gallery that is the shipped
419
- `QuickAdd` — a tile never adds a product with options itself, and never hides the buy path
559
+ picking `variants[0]`, and never gate `canAdd` yourself. On the PDP that is the shipped
560
+ `OptionPicker`; in the gallery the shipped `QuickAdd` — a tile never adds a product with options itself, and never hides the buy path
420
561
  behind "go to the product page" for a product that has no options.
421
562
  - **Filters are the shipped `FilterPanel`** — mounted in the gallery whenever the store has a
422
563
  catalog to filter; not rebuilt with fewer controls, not dropped because the brief didn't ask.
423
564
  - **Categories are pages** — `/category/[slug]` with its SEO block, linked from the chrome; a
424
565
  category that exists only as a state toggle on `/shop` has no URL to share or index.
425
566
  - Don't wrap shipped calls in your own API routes — they run client-side by design.
426
- - Theme via the `@theme` tokens; your markup uses Tailwind utilities on the same tokens. No
427
- parallel theme files, no hardcoded palette values in components.
567
+ - Where the shipped components deploy (Astro, React): theme via the `@theme` tokens, and your
568
+ markup uses Tailwind utilities on the same tokens — one design system across shipped and written
569
+ code. No parallel theme files, no hardcoded palette values in components. Where they don't
570
+ (`lib`, `static`, a port): style with whatever your stack does well, on one token set of your
571
+ own; the rule that survives is the token set, not Tailwind.
428
572
  - Checkout only through the shipped cart (`checkout()`) — never a hand-built checkout URL.
429
573
  - Live data or an honest empty state — never mock products, prices, reviews, or counts.
430
574
  - **Prices and ribbons come from the DTOs as-is** — no computed percent-off, no "Sale" badge
@@ -446,44 +590,23 @@ client id into `wix/config.ts`; nothing else to configure.
446
590
 
447
591
  ## Point the user to their dashboard
448
592
 
449
- Give the owner the dashboard, products, and categories links — **the deploy step's JSON
450
- output already printed them ready-made** (`dashboardUrl`, `productsUrl`, `categoriesUrl`);
451
- copy, don't re-derive. Real payments additionally need a premium plan + a connected payment
452
- method (dashboard) — mention it, don't treat it as a code failure.
593
+ Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON also prints
594
+ `dashboardUrl`, `productsUrl`, `categoriesUrl`); an entity id fills the placeholder from the seed result.
595
+
596
+ | page | `https://manage.wix.com/dashboard/{siteId}/` + |
597
+ |---|---|
598
+ | Products | `wix-stores/products` |
599
+ | Edit a product | `wix-stores/products/product/{productId}` |
600
+ | Categories | `wix-stores/categories/list` |
601
+ | Inventory | `wix-stores/inventory` |
602
+ | Orders | `ecom-platform/orders-list` |
603
+ | Store settings | `wix-stores/settings` |
604
+
605
+ Real payments additionally need a premium plan + a connected payment method (Settings → Accept
606
+ payments) — mention it, don't treat it as a code failure.
453
607
 
454
608
  ## Seeding
455
609
 
456
610
  Per `seed/SEED.md` — a plain-data `plan.json` into `seed-store.mjs`, run from the project
457
611
  root. Independent of the frontend work; seed a catalog that exercises the UI (≥1 product with
458
612
  a color option, ≥1 on sale, an image per product) unless the brief says otherwise.
459
-
460
- ## Verify (before declaring done)
461
-
462
- - [ ] `/shop` renders live products SSR (view-source shows product names) through **your**
463
- grid/card; the `FilterPanel` shows the result count, and on desktop a sidebar beside the
464
- grid with the price slider and this catalog's facets (a Color option → swatches); category links lead to `/category/<slug>`, which renders scoped and
465
- carries the SEO tags in view-source; an unknown slug is a 404; empty catalog shows your
466
- honest empty state.
467
- - [ ] Shop first screen: at 1280×720 and at 390px wide, at least one full tile — image, name,
468
- price — is visible without scrolling; the tile with the longest product name and a price
469
- range shows the whole price at 390px, nothing clipped.
470
- - [ ] Tiles: the buy control sits under name and price (not over the image); a product with no
471
- options adds to the cart in one click; a product with options opens the picker across the
472
- full tile width (a bottom sheet at 390px wide) and adds the chosen variant; the drawer
473
- opens after either.
474
- - [ ] Your PDP at 390px wide: image, name, price, the first option, and the buy button are all
475
- visible before scrolling. Color options render as swatches, the button shows
476
- `blockedReason` ("Choose Size") until every option is picked, the price is the range until then and the variant's
477
- price after, a sale shows the labelled "was", a sold-out combination reads "Out of stock",
478
- a pre-orderable one reads "Pre-order".
479
- - [ ] Cards: every ribbon renders; a multi-price product shows a range with no struck price; a
480
- product with a color option shows its swatches.
481
- - [ ] PDP gallery: one thumbnail per distinct photo — never two selectors leading to the same image.
482
- - [ ] Cart: a subscription line shows its plan terms under the product name.
483
- - [ ] Cart: add / quantity ± / remove work; badge count is live; subtotal shows; cart survives
484
- a reload (same visitor token).
485
- - [ ] Checkout button redirects to Wix-hosted checkout.
486
- - [ ] PDP view-source carries the SEO tags (Astro).
487
- - [ ] Shop/PDP/home are YOUR designs on the tokens; the data-layer/hook/cart files are
488
- unedited.
489
- - [ ] Dashboard links handed to the owner.
@@ -0,0 +1,95 @@
1
+ // The purchase controls for one product — option groups (swatches for a color option, pills
2
+ // otherwise, out-of-stock choices disabled), choice modifiers, an optional quantity stepper, and
3
+ // the buy button gated by the hook: disabled with a plain reason ("Choose Size") until every
4
+ // choice is made, "Pre-order" when the resolved variant is pre-orderable, the hook's error inline.
5
+ // QuickAdd's picker and your product page mount the same component, so buying behaves the same
6
+ // in the gallery and on the PDP. Wire as-is; style via the tokens.
7
+ //
8
+ // const d = useProductDetail({ initial }); // the PDP
9
+ // <OptionPicker detail={d} showQuantity />
10
+ //
11
+ // Price, name, gallery, description, and layout stay yours — render d.price / d.compareAtPrice
12
+ // beside this (the range until every option is picked, then the variant's price).
13
+ import type { UseProductDetail } from "../../hooks/storefront/useProductDetail";
14
+
15
+ export default function OptionPicker({
16
+ detail: d,
17
+ showQuantity = false,
18
+ onAdded,
19
+ }: {
20
+ detail: UseProductDetail;
21
+ /** The PDP shows a quantity stepper; a tile picker adds one. */
22
+ showQuantity?: boolean;
23
+ /** Called after a successful add (a tile picker closes itself here). */
24
+ onAdded?: () => void;
25
+ }) {
26
+ if (!d.product) return null;
27
+ return (
28
+ <div className="flex flex-col gap-3">
29
+ {d.optionGroups.map((g) => (
30
+ <fieldset key={g.id}>
31
+ <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
32
+ <div className="flex flex-wrap gap-1.5">
33
+ {g.choices.map((c) =>
34
+ g.isColor && c.colorCode ? (
35
+ <button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
36
+ disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
37
+ className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
38
+ style={{ backgroundColor: c.colorCode }} />
39
+ ) : (
40
+ <button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
41
+ onClick={() => d.selectOption(g.name, c.name)}
42
+ className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
43
+ {c.name}
44
+ </button>
45
+ ),
46
+ )}
47
+ </div>
48
+ </fieldset>
49
+ ))}
50
+ {d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
51
+ <fieldset key={m.key}>
52
+ <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
53
+ {m.name}{m.mandatory ? " *" : ""}
54
+ </legend>
55
+ <div className="flex flex-wrap gap-1.5">
56
+ {m.choices.map((c) => (
57
+ <button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
58
+ onClick={() => d.setModifier(m.key, c.key)}
59
+ className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
60
+ {c.name}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ </fieldset>
65
+ ))}
66
+ {d.product.modifiers.filter((m) => m.type === "text").map((m) => (
67
+ <label key={m.key} className="flex flex-col gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
68
+ {m.name}{m.mandatory ? " *" : ""}
69
+ <input type="text" value={d.modifierValues[m.key] ?? ""} onChange={(e) => d.setModifier(m.key, e.target.value)}
70
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm normal-case tracking-normal text-foreground" />
71
+ </label>
72
+ ))}
73
+ <div className="flex items-stretch gap-2">
74
+ {showQuantity && (
75
+ <div className="flex items-center rounded-full border border-border">
76
+ <button type="button" aria-label="Decrease quantity" disabled={d.quantity <= 1} onClick={() => d.setQuantity(d.quantity - 1)}
77
+ className="px-3 py-2 text-sm disabled:opacity-40">−</button>
78
+ <span className="min-w-6 text-center text-sm tabular-nums">{d.quantity}</span>
79
+ <button type="button" aria-label="Increase quantity" onClick={() => d.setQuantity(d.quantity + 1)} className="px-3 py-2 text-sm">+</button>
80
+ </div>
81
+ )}
82
+ <button
83
+ type="button"
84
+ disabled={!d.canAdd || d.adding}
85
+ onClick={() => d.add().then(() => onAdded?.()).catch(() => {})}
86
+ className="flex-1 rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
87
+ >
88
+ {d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
89
+ </button>
90
+ </div>
91
+ {d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
92
+ {d.error && <p className="text-xs text-red-600">{d.error}</p>}
93
+ </div>
94
+ );
95
+ }
@@ -3,25 +3,45 @@
3
3
  // • options / choice mods → Quick Add: a picker on the card (bottom sheet on small screens)
4
4
  // • free-text modifier → the product page (the gallery can't collect the text)
5
5
  // Mount as the LAST ROW of the tile's text block (under name and price), as a direct child of the
6
- // tile root that carries `relative` — the picker anchors to that root and takes its width. Never
7
- // overlay it on the image, never wrap it in a narrower positioned box.
6
+ // tile root that carries `relative flex flex-col` — the picker anchors to that root and takes its
7
+ // width, and the control pins itself to the tile's bottom (mt-auto) so the action row lines up
8
+ // across a grid row whether or not a neighbour carries swatches or a struck price. Never overlay
9
+ // it on the image, never wrap it in a narrower positioned box.
8
10
  // <QuickAdd product={p} /> Wire as-is; style via the tokens.
9
11
  import { useEffect, useRef, useState } from "react";
10
12
  import { useCart } from "../../hooks/storefront/useCart";
11
13
  import { useProductDetail } from "../../hooks/storefront/useProductDetail";
12
14
  import type { ProductSummary } from "../../wix/storefront/types";
15
+ import OptionPicker from "./OptionPicker";
13
16
 
14
17
  export default function QuickAdd({ product }: { product: ProductSummary }) {
18
+ return (
19
+ // A tile is often one big <a>. A click inside the buy control must never become a navigation
20
+ // to the product page — this boundary cancels the link's default for everything below it
21
+ // (the button, the picker, the sheet). Still: keep the control OUTSIDE the tile's link.
22
+ <div
23
+ className="contents"
24
+ onClick={(e) => {
25
+ e.preventDefault();
26
+ e.stopPropagation();
27
+ }}
28
+ >
29
+ <QuickAddControl product={product} />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function QuickAddControl({ product }: { product: ProductSummary }) {
15
35
  const { addToCart, busy } = useCart();
16
36
  const [open, setOpen] = useState(false);
17
37
  const [error, setError] = useState<string | null>(null);
18
38
 
19
39
  if (product.availability === "OUT_OF_STOCK" && !product.preorder) {
20
- return <span className="text-sm text-muted-foreground">Out of stock</span>;
40
+ return <span className="mt-auto block pt-3 text-sm text-muted-foreground">Out of stock</span>;
21
41
  }
22
42
  if (product.quickAddable) {
23
43
  return (
24
- <div>
44
+ <div className="mt-auto pt-3">
25
45
  <button
26
46
  type="button"
27
47
  disabled={busy}
@@ -39,7 +59,7 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
39
59
  // Options to pick, or a single variant that isn't plainly in stock (pre-order, partial stock):
40
60
  // the picker resolves it through useProductDetail, exactly as the PDP would.
41
61
  return (
42
- <>
62
+ <div className="mt-auto pt-3">
43
63
  <button
44
64
  type="button"
45
65
  onClick={() => setOpen(true)}
@@ -48,11 +68,12 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
48
68
  {product.optionsSummary ? "Choose options" : product.preorder ? "Pre-order" : "Add to cart"}
49
69
  </button>
50
70
  {open && <QuickAddPicker product={product} onClose={() => setOpen(false)} />}
51
- </>
71
+ </div>
52
72
  );
53
73
  }
54
74
 
55
- // The picker fetches the full product only when it opens — cards never carry PDP data.
75
+ // The picker fetches the full product only when it opens — cards never carry PDP data. Its controls
76
+ // are the shipped OptionPicker, the same component the PDP mounts.
56
77
  function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose: () => void }) {
57
78
  const d = useProductDetail({ slug: product.slug });
58
79
  const panelRef = useRef<HTMLElement>(null);
@@ -108,57 +129,7 @@ function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose
108
129
  </a>
109
130
  )}
110
131
 
111
- {d.product && !needsPdp && (
112
- <div className="flex flex-col gap-3">
113
- {d.optionGroups.map((g) => (
114
- <fieldset key={g.id}>
115
- <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
116
- <div className="flex flex-wrap gap-1.5">
117
- {g.choices.map((c) =>
118
- g.isColor && c.colorCode ? (
119
- <button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
120
- disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
121
- className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
122
- style={{ backgroundColor: c.colorCode }} />
123
- ) : (
124
- <button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
125
- onClick={() => d.selectOption(g.name, c.name)}
126
- className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
127
- {c.name}
128
- </button>
129
- ),
130
- )}
131
- </div>
132
- </fieldset>
133
- ))}
134
- {d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
135
- <fieldset key={m.key}>
136
- <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
137
- {m.name}{m.mandatory ? " *" : ""}
138
- </legend>
139
- <div className="flex flex-wrap gap-1.5">
140
- {m.choices.map((c) => (
141
- <button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
142
- onClick={() => d.setModifier(m.key, c.key)}
143
- className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
144
- {c.name}
145
- </button>
146
- ))}
147
- </div>
148
- </fieldset>
149
- ))}
150
- <button
151
- type="button"
152
- disabled={!d.canAdd || d.adding}
153
- onClick={() => d.add().then(onClose).catch(() => {})}
154
- className="rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
155
- >
156
- {d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
157
- </button>
158
- {d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
159
- {d.error && <p className="text-xs text-red-600">{d.error}</p>}
160
- </div>
161
- )}
132
+ {d.product && !needsPdp && <OptionPicker detail={d} onAdded={onClose} />}
162
133
  </section>
163
134
  </>
164
135
  );