@wix/agent-skills 1.18.0 → 1.18.2

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 (76) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +4 -4
  3. package/skills/wix-app/references/APP_VALIDATION.md +7 -33
  4. package/skills/wix-app/references/DASHBOARD_PAGE.md +18 -14
  5. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +75 -92
  6. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +23 -26
  7. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +7 -6
  8. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +71 -4
  9. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +67 -7
  10. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -1
  11. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +9 -6
  12. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +59 -89
  13. package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +23 -4
  14. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +29 -14
  15. package/skills/wix-base44-connector/SKILL.md +168 -48
  16. package/skills/wix-base44-connector/scripts/{utils.js → utils.cjs} +69 -48
  17. package/skills/wix-docs/SKILL.md +109 -111
  18. package/skills/wix-docs/references/API_SPEC_SEARCH.md +35 -23
  19. package/skills/wix-docs/references/CALLING.md +91 -0
  20. package/skills/wix-docs/references/EXTRACTING.md +5 -3
  21. package/skills/wix-headless/references/DOC_DISCOVERY.md +9 -3
  22. package/skills/wix-headless/references/inline-recipes/setup-forms.md +57 -10
  23. package/skills/wix-manage/SKILL.md +106 -92
  24. package/skills/wix-manage/references/dashboard-navigation/routes.json +12 -52
  25. package/skills/wix-manage/references/ecommerce/shipping/ecom-shipping-api.md +0 -2
  26. package/skills/wix-manage/references/forms/create-form.md +272 -184
  27. package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +13 -10
  28. package/skills/wix-manage/references/forms/update-form.md +122 -0
  29. package/skills/wix-manage/references/media/generate-image-with-ai.md +86 -0
  30. package/skills/wix-manage/references/seo/manage-content-plan.md +247 -0
  31. package/skills/wix-manage/references/seo/manage-seo-tags.md +59 -6
  32. package/skills/wix-manage/references/stores/change-store-currency.md +10 -0
  33. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +16 -1
  34. package/skills/wix-manage/references/stores/update-product-with-options.md +9 -0
  35. package/skills/wix-vibe-headless/SKILL.md +17 -18
  36. package/skills/wix-vibe-headless/install/pin-agents-md.cjs +1 -1
  37. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +118 -0
  38. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +55 -62
  39. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +31 -29
  40. package/skills/wix-vibe-headless/platforms/base44.md +12 -5
  41. package/skills/wix-vibe-headless/platforms/generic.md +13 -12
  42. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +4 -13
  43. package/skills/wix-vibe-headless/references/blog/seed/SEED.md +4 -8
  44. package/skills/wix-vibe-headless/references/blog/seed/{seed-blog.js → seed-blog.cjs} +2 -3
  45. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +5 -14
  46. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +5 -9
  47. package/skills/wix-vibe-headless/references/bookings/seed/{seed-bookings.js → seed-bookings.cjs} +2 -2
  48. package/skills/wix-vibe-headless/references/bookings/seed/{seed-rentals.js → seed-rentals.cjs} +3 -3
  49. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -1
  50. package/skills/wix-vibe-headless/references/cms/seed/SEED.md +2 -2
  51. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +4 -13
  52. package/skills/wix-vibe-headless/references/events/seed/SEED.md +3 -7
  53. package/skills/wix-vibe-headless/references/events/seed/{seed-events.js → seed-events.cjs} +2 -2
  54. package/skills/wix-vibe-headless/references/forms/INSTRUCTIONS.md +24 -7
  55. package/skills/wix-vibe-headless/references/forms/seed/SEED.md +91 -17
  56. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +4 -13
  57. package/skills/wix-vibe-headless/references/members/seed/SEED.md +2 -3
  58. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +4 -13
  59. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectMedia.jsx +1 -1
  60. package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +3 -7
  61. package/skills/wix-vibe-headless/references/portfolio/seed/{seed-portfolio.js → seed-portfolio.cjs} +2 -2
  62. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +5 -14
  63. package/skills/wix-vibe-headless/references/pricing-plans/app/components/OrderCard.jsx +1 -1
  64. package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +3 -7
  65. package/skills/wix-vibe-headless/references/pricing-plans/seed/{seed-pricing-plans.js → seed-pricing-plans.cjs} +2 -2
  66. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +5 -15
  67. package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +3 -7
  68. package/skills/wix-vibe-headless/references/restaurants/seed/{seed-restaurants.js → seed-restaurants.cjs} +2 -2
  69. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +359 -327
  70. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +1 -0
  71. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +2 -2
  72. package/skills/wix-vibe-headless/references/storefront/app/hooks/useShop.js +63 -78
  73. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +59 -43
  74. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +46 -10
  75. package/skills/wix-vibe-headless/references/storefront/seed/{seed-store.js → seed-store.cjs} +163 -25
  76. package/skills/wix-vibe-headless/references/storefront/app/pages/Shop.jsx +0 -75
@@ -1,224 +1,367 @@
1
1
  # Wix Storefront — ready-made client
2
2
 
3
- The storefront **commerce engine ships as real files** — the hooks, the server cart, the REST
4
- transport, the image helpers, and the `Shop` listing page, all correct (variant resolution,
5
- modifiers, stock gating, checkout). It reads from your app's design tokens (base44's `src/index.css`
6
- — the shadcn palette the design phase already set).
3
+ The storefront ships the commerce hooks, server cart, REST transport, image helpers, and cart UI.
4
+ You build the presentation: Shop page, product grid and cards, variant controls, product detail
5
+ (PDP) page, Home, header, and footer. Use the interfaces below and proceed directly to implementation.
7
6
 
8
- **The presentation doesn't ship — you build it** (STEP 3): the product **card**, **grid**, **variant
9
- controls**, and the whole **product (PDP) page**, plus **Home** and the **header/footer**. Each is
10
- built on a shipped hook whose return type + field shapes are documented in the outline right next to
11
- it — the data is handed to you done, the layout and look are yours. `Shop` renders once your
12
- `ProductGrid` exists; the PDP renders once you build its page — that's the point.
13
-
14
- Talks to Wix directly over the public `WIX_CLIENT_ID` (anonymous visitor tokens). Never mock
15
- products; never hand-build a `/checkout` URL — the shipped cart goes through the eCom
16
- redirect-session.
7
+ The shipped client handles visitor authentication using its deployed configuration. No ID lookup
8
+ or configuration changes are needed to build the UI. Never mock products or hand-build a
9
+ `/checkout` URL; the shipped cart uses the eCom redirect session.
17
10
 
18
11
  ## Prerequisites
19
- - The site's **Wix Stores** catalog is the read/cart target. It's installed and seeded separately (see **Seeding** below), in parallel with this build — so it may be empty at build time; the client renders the shipped empty state until products land.
20
- - The public headless **`WIX_CLIENT_ID`** from your prompt (buyer-facing, safe to hardcode/commit).
12
+ - The site's **Wix Stores** catalog is the read/cart target. It's installed and seeded separately, in parallel with this build — so it may be empty at build time; render the empty state until products land.
21
13
 
22
- ## STEP 1 — The client is already in `src/`
23
- The install step (base44.md STEP 1) deployed the whole storefront UI client + REST scaffolds into
24
- `src/` (imports use the `@/` alias → `src/`). Here's every file and what it is — **this is your map,
25
- so you don't need to open them:**
14
+ ## Already installed in `src/`
15
+ Successful deployment verified these files are in place; use this map without needing to read their source (`@/` → `src/`).
26
16
 
27
17
  | file | what it is |
28
18
  |---|---|
29
- | `context/CartContext.jsx` | `useCart()` provider: server cart, add/update/remove, checkout |
30
- | `hooks/useProductDetail.js` | PDP data — product + variant resolution for a slug, plus load/add state; **your product page (STEP 3) is built on it** |
31
- | `hooks/useShop.js` | catalog listing — category menu, cursor paging, sort, failure state |
32
- | `hooks/useProductCard.js` | headless data layer for a grid tile — returns `leftBadges`, `promoBadge`, `priceDisplay`, `compareAtDisplay`, `colors`, `optionLabel`, `isQuickAddable`, `image`, `hoverImage`; **your `ProductCard` (STEP 3) is built on it** |
33
- | `lib/storeImage.js` | `productImage()` / `productGallery()` / `storeImage()` / `choiceImage()` — normalise Wix image urls; `productGallery(product)` → `[{ url, altText }]` (your PDP gallery); `choiceImage(choice)` resolves an option choice's photo (V3 read shape: `media.items[].mediaId`, not `linkedMedia`) |
34
- | `components/CartButton.jsx` | header cart **icon** button with a live-count badge |
35
- | `components/CartDrawer.jsx` | slide-over cart (mount once; opens from `useCart`) |
36
- | `hooks/useVariantOptions.js` | headless data layer for options/modifiers — returns `optionGroups` + `modifierGroups` (normalised, render-agnostic); **your PDP's variant controls (STEP 3) render from it** |
37
- | `components/WixManageBanner.jsx` | preview-only manage banner — drop it into your Layout (STEP 3) |
38
- | `pages/Shop.jsx` | the shipped `/shop` listing page — renders your `ProductGrid` (you build `/product/:slug` and `/`) |
39
- | `rest/wix-config.js` | the two ids, written by the install step |
40
- | `rest/wix-client.js` + `rest/wix-store-*.js` | REST transport + catalog/cart helpers |
41
-
42
- Everything in the table is already in place — go **straight to wiring + building your presentation**
43
- (card, grid, variant controls, the product page, Home — STEP 3), nothing to verify first.
44
- **Don't `read_file` the shipped page/hook source to inspect it** — the table says what each is and
45
- every field shape you need is in the outlines below. Read a shipped file's source **only** on a real
46
- fallback — a runtime error, or a field the outlines don't cover (see "Fallback only" at the end).
47
- (Shipped files missing? the install's `deploy` result lists what it wrote; re-run install, or copy
48
- `references/storefront/app/` → `src/`.)
49
-
50
-
51
- ## STEP 2 — Theme
52
- The shipped pages carry **no palette of their own** — they render from base44's design tokens
53
- in `src/index.css` (`:root`/`.dark`: `--background`, `--foreground`, `--card`, `--primary`,
54
- `--muted`, `--border`, `--radius`, `--font-*`) via shadcn Tailwind classes (`bg-card`,
55
- `text-foreground`, `bg-primary`, `text-muted-foreground`, `border-border`, `rounded-lg`,
56
- `font-display`). Those tokens are **already set to the brand by the design phase**, so the shipped
57
- pages are themed with zero work here. To adjust the palette, edit `index.css` (`:root` **and**
58
- `.dark`) — the base44 way; **never add a parallel theme file (e.g. a `theme.css`) or restyle the
59
- shipped pages.** Style the components you build (STEP 3) and the Home/Header you add from the
60
- **same** base44 tokens/classes so everything matches automatically. A dark brand is just base44's
61
- dark palette in `index.css` — no per-component work.
62
-
63
- ## STEP 3 — Wire routes + provider (surgical `find_replace` on `src/App.jsx`, never a rewrite)
64
- **No file reads needed to wire this.** Every shipped page and `WixManageBanner` is a default export that takes **no props** — wire them exactly as the snippet shows; nothing in those files needs looking up.
65
- `App.jsx` carries required platform auth scaffolding (`AuthProvider`/`useAuth`) — edit it in, don't
66
- replace it.
67
- - Wrap the routed tree in `<CartProvider>` (from `@/context/CartContext`).
68
- - Put your **header + footer in a `Layout`** that renders `<Outlet/>` between them, and nest every
69
- route under one pathless `<Route element={<Layout/>}>`. Your brand chrome then wraps **every** page
70
- — including the shipped `Shop` and your own pages — so you **never edit the shipped `Shop` to add a
71
- header/footer** (it renders inside `<Outlet/>` as-is). Mount `<CartDrawer/>` once in the Layout.
72
- - **Pin the top chrome as one fixed block.** Put `<WixManageBanner/>` (shipped, preview-only) **above**
73
- your `<Header/>` inside a single `position:fixed` top region — the header itself is plain in-flow
74
- markup, the region owns the fixing — so banner + header ride together (no scroll drift/gap). Pad
75
- the content by the region's measured height so it clears the chrome and self-corrects when the
76
- banner is dismissed.
77
- - Routes under the Layout: `/shop` → `Shop` (shipped, renders your grid); `/product/:slug` → **your
78
- `ProductDetail`**; `/` → **your `Home`**.
19
+ | `context/CartContext.jsx` | `CartProvider` and `useCart()`: server cart, add/update/remove, checkout |
20
+ | `hooks/useProductDetail.js` | PDP product, variant resolution, selection, load/add state |
21
+ | `hooks/useShop.js` | Catalog listing, categories, cursor paging, sort, failure state; use in your `Shop` |
22
+ | `hooks/useProductCard.js` | Card badges, prices, options summary, quick-add flag, images |
23
+ | `hooks/useVariantOptions.js` | Render-agnostic option and modifier groups for PDP controls |
24
+ | `lib/storeImage.js` | Wix image URL and gallery helpers |
25
+ | `components/CartButton.jsx` | Cart icon button with a live-count badge |
26
+ | `components/CartDrawer.jsx` | Cart drawer; mount once, opens through `useCart` |
27
+ | `components/WixManageBanner.jsx` | Preview-only manage banner; include only when the entry guide enables it |
28
+ | `rest/wix-config.js` | Deployed configuration; consumed internally by the shipped client |
29
+ | `rest/wix-client.js` | REST transport and visitor authentication |
30
+ | `rest/wix-store-catalog.js` | Product and category queries |
31
+ | `rest/wix-store-cart.js` | Cart mutations and hosted checkout |
32
+
33
+ Build using the interfaces below without reading the shipped source. If you encounter an error
34
+ after building the client, read or change whatever you need to diagnose and fix it.
35
+ If deployment failed or files are missing, re-run the install/deploy step.
36
+
37
+ ## Theme
38
+ Use the existing Base44 theme in `src/index.css` so your pages and the shipped components
39
+ share the same colors and typography.
40
+
41
+ ## Presentation interfaces
42
+ Build `pages/Shop.jsx`, `components/ProductGrid.jsx`, `components/ProductCard.jsx`, and
43
+ `pages/ProductDetail.jsx` from these contracts. All paths are under `src/`. Home, header, and
44
+ footer are yours to design; their integration is in **Routes and provider** below.
45
+
46
+ ### Shop page and product grid
47
+ Create both files below. The hook owns catalog data; your page owns its presentation.
48
+
49
+ ```jsx
50
+ // src/pages/Shop.jsx
51
+ import { useShop, SORTS } from "@/hooks/useShop";
52
+ import ProductGrid from "@/components/ProductGrid";
53
+
54
+ export default function Shop() {
55
+ const s = useShop();
56
+ // Category controls: s.categories ({ id, name }), s.activeCategory;
57
+ // select with s.setActiveCategory(category), or null for all products.
58
+ // Sort controls: Object.entries(SORTS) -> [key, { label }];
59
+ // selected value s.sort; change with s.setSort(key).
60
+ // Optional filters: s.setFilters({ minPrice: 20, maxPrice: 100,
61
+ // inStockOnly: true, search: "linen" }); s.setFilters({}) clears them.
62
+ // Apply a submitted/debounced search value; the hook queries on every change.
63
+ // Render s.error with s.retry separately from loading/empty results.
64
+ // On a later-page error, keep already loaded products visible.
65
+ // Pagination: when s.hasMore, call s.loadMore(); disable while s.loadingMore.
66
+ return (
67
+ <ProductGrid
68
+ products={s.products}
69
+ loading={s.loading}
70
+ emptyMessage={s.error ? "" : "No products yet."}
71
+ emptyHint={s.error ? "" : "Try another category or check back later."}
72
+ />
73
+ ); // Add the controls and states above; layout and styling are yours.
74
+ }
75
+ ```
76
+
77
+ ```jsx
78
+ // src/components/ProductGrid.jsx
79
+ import ProductCard from "@/components/ProductCard";
80
+
81
+ export default function ProductGrid({ products, loading, emptyMessage, emptyHint }) {
82
+ // products: array | null; emptyMessage and emptyHint are display text, not state.
83
+ // Layout and styling are yours; this component does not fetch products.
84
+ if (loading && !products?.length) return null; // Replace null with your loading UI.
85
+ if (!products?.length) {
86
+ // The page handles errors separately and suppresses emptyMessage on error.
87
+ return emptyMessage ? <div><p>{emptyMessage}</p><p>{emptyHint}</p></div> : null;
88
+ }
89
+ return products.map(product => <ProductCard key={product.id} product={product} />);
90
+ }
91
+ ```
79
92
 
80
- **Build the presentation — it's all yours.** Each piece below is built on a shipped hook whose return
81
- type + field shapes are in the outline right after this list — the data is done, the render is yours.
82
- `Shop` won't show products until `ProductGrid` exists, and `/product/:slug` won't render until you
83
- build the product page, so these aren't optional:
84
-
85
- - **`components/ProductGrid.jsx`** — `Shop` and your Home render it. It receives `products`, `loading`,
86
- `empty`, `emptyHint` and owns three states (loading, empty, list) plus the layout. A boutique might
87
- want a 3-col editorial layout; a high-volume store a dense 4-col grid; a home strip a horizontal scroll.
88
- - **`components/ProductCard.jsx`** — your grid maps each product to it. Built on `useProductCard`
89
- (badges, price display, colour dots, quick-add flag, images) — you decide layout, shape, hover, CTA.
90
- A lifestyle brand might want full-bleed images with an overlay gradient; a tech store a compact item.
91
- - **`pages/ProductDetail.jsx`** — the **whole product page**, built on `useProductDetail`; route
92
- `/product/:slug` to it. Gallery, price, description, the variant controls (options/modifiers via
93
- `useVariantOptions`), the buy box — you arrange and style all of it (nothing PDP ships). This is the
94
- surface that usually looks most generic, so make the layout the brand's: an editorial split, a sticky
95
- buy column, a full-bleed gallery, large swatches vs a compact dropdown — your call.
93
+ ### Product card
94
+ Pass a catalog product to `ProductCard`: use `product.id` for identity/quick-add, `product.name`
95
+ for the title, and `product.slug` for the PDP link. Pass the full object to `useProductCard`.
96
96
 
97
97
  ```jsx
98
- // components/ProductCard.jsx — your grid maps each product to it.
99
- import { Link } from "react-router-dom";
100
- import { useCart } from "@/context/CartContext"; // addToCart(product.id) for quick-add
101
98
  import { useProductCard } from "@/hooks/useProductCard";
99
+ import { useCart } from "@/context/CartContext";
102
100
 
103
101
  export default function ProductCard({ product }) {
104
- const { addToCart } = useCart();
102
+ const { addToCart, loading, error } = useCart();
105
103
  const {
106
- isSoldOut, isPreorder,
107
- leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }] — image top-left
108
- promoBadge, // { type: 'discount'|'ribbon', label } | null — image top-right
109
- priceDisplay, // "€10" | "€10 – €20" (range when variants differ)
110
- compareAtDisplay, // original price string | null — strike-through
111
- colors, // hex strings → colour dots
112
- optionLabel, // "3 sizes · 2 materials" | ""
113
- isQuickAddable, // true → addToCart(product.id); else <Link to={`/product/${product.slug}`}>
114
- image, hoverImage,// URLs | null
104
+ isSoldOut, isPreorder, isPartiallyOutOfStock, // booleans
105
+ leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
106
+ promoBadge, // { type: 'discount'|'ribbon', label } | null
107
+ priceDisplay, // formatted price or min–max range; may be undefined if absent
108
+ compareAtDisplay, // formatted compare-at price | null
109
+ colors, // hex colour strings
110
+ optionLabel, // e.g. "3 sizes · 2 materials", or ""
111
+ isQuickAddable, // no product options and not sold out; does NOT check modifiers
112
+ image, hoverImage,// normalised primary / second gallery URL | null
115
113
  } = useProductCard(product);
116
- // …you implement the tile.
114
+ // Render your card.
117
115
  }
118
116
  ```
119
-
120
- ```jsx
121
- // Variant controls — render the product's options/modifiers from useVariantOptions.
122
- // The inputs (options, modifiers, selectedOptions, modifierValues) come from useProductDetail (below).
123
- import { useVariantOptions } from "@/hooks/useVariantOptions";
124
-
125
- const { optionGroups, modifierGroups } = useVariantOptions(options, modifiers, selectedOptions, modifierValues);
126
- // optionGroups: [{ id, name, isColor, choices: [{ choiceId, name, colorCode, isColorSwatch, inStock, selected }] }]
127
- // modifierGroups: [{ key, name, mandatory, type: 'choices'|'text', choices?: [{ key, name, selected }], value?: string }]
128
- // pick a choice: selectOption(group.id, choice.choiceId)
129
- // set a modifier: setModifier(m.key, choice.key) · text type: setModifier(m.key, e.target.value)
117
+ For quick-add, call `addToCart(product.id)` only when `isQuickAddable` and no mandatory
118
+ modifier needs input (`product.modifiers` entries have a `mandatory` boolean). Otherwise link to `/product/${product.slug}` for selection, including
119
+ pre-orders. Listing results have no full variants; the PDP hook loads and resolves them.
120
+ Use cart-context `loading` to disable repeated adds. Failures open the shipped drawer with
121
+ `error`; you can also display it beside the card/PDP controls (see **Cart**).
122
+
123
+ ### Catalog hook reference
124
+ `useShop` and `SORTS` are named exports from `@/hooks/useShop`; the Shop skeleton above uses them.
125
+
126
+ ```js
127
+ const {
128
+ categories, activeCategory, setActiveCategory,
129
+ products, loading, error, retry,
130
+ hasMore, loadMore, loadingMore, sort, setSort, filters, setFilters,
131
+ } = useShop({ pageSize: 24 }); // optional argument; default pageSize 24
132
+ // categories: category[]; activeCategory: category | null (null = all products)
133
+ // setActiveCategory(categoryOrNull): starts a fresh product query, keeping sort/filters
134
+ // products: product[] | null; loading: boolean (fresh query, including sort/filter changes)
135
+ // error: string | null; retry(): reloads the active category's first page
136
+ // hasMore, loadingMore: booleans; loadMore(): appends another page when available
137
+ // sort: 'featured' | 'priceAsc' | 'priceHigh' | 'name' | 'newest'; setSort(key)
138
+ // filters: { minPrice?, maxPrice?, inStockOnly?, search? }
139
+ // setFilters(object): replaces filters; setFilters(previous => ({ ...previous, ...patch })) merges
140
+ // SORTS: { [key]: { label } } for those keys
130
141
  ```
131
-
132
- ```jsx
133
- // components/ProductGrid.jsx — Shop and your Home render it.
134
- import ProductCard from "./ProductCard"; // your card, above
135
-
136
- export default function ProductGrid({ products, loading, empty, emptyHint }) {
137
- // products — array | null (null while loading)
138
- // loading — boolean
139
- // empty — heading string for the no-products state
140
- // emptyHint — sub-text string for the no-products state
141
- // …you implement: the loading state, the empty state (empty + emptyHint), and the layout
142
- // mapping products → <ProductCard product={p} />.
143
- }
142
+ Wix filters and sorts the full matching catalog before paging; the hook never sorts a loaded subset.
143
+ Changing category, sort, or filters resets products and the cursor; stale responses are ignored.
144
+ `featured` retains its existing key but means **Default order**, not merchant-curated placement.
145
+ Prices use the product's minimum actual variant price in site currency, in either direction and
146
+ for both bounds; this is not a selected-variant or checkout-discount price. Bounds are inclusive,
147
+ non-negative numbers (numeric strings accepted); null/empty omits a bound. Min must not exceed max.
148
+ `inStockOnly` matches `IN_STOCK` products, excluding partially stocked and preorder-only products.
149
+ `search` searches product names (up to 100 characters). These filters can be combined.
150
+
151
+ Initial product failure sets `error` and an empty array;
152
+ a later page failure preserves products and cursor; call `loadMore()` again to retry that page.
153
+ `retry()` starts over at page one with current selections. Render the error separately from
154
+ an empty catalog. Category loading fetches only the first 100 categories, excludes `visible: false`
155
+ and `slug: "all-products"`, and falls back to `[]` on failure without setting `error`.
156
+
157
+ For a standalone product selection or category menu, import these named functions from
158
+ `@/rest/wix-store-catalog`. They return promises and reject on invalid query options or request failure.
159
+
160
+ | Function | Result |
161
+ |---|---|
162
+ | `searchProducts({ limit = 100, cursor, categoryId, sort, minPrice, maxPrice, inStockOnly, search } = {})` | `{ products: product[], nextCursor: string or null }` — same sort/filter semantics as the hook |
163
+ | `queryProducts(options = {})` | `{ products: product[], nextCursor: string or null }` — visible catalog products |
164
+ | `queryCategories({ limit = 100, cursor } = {})` | `{ categories: category[], nextCursor: string or null }` — one category page |
165
+ | `queryProductsByCategory(categoryId, options = {})` | `{ products: product[], nextCursor: string or null }` — visible products in the category identified by `id` |
166
+
167
+ `queryProducts` and `queryProductsByCategory` accept the same options as `searchProducts`.
168
+ A cursor continues the original query; changed filters/sort require starting without one.
169
+ Products are the full listing objects consumed by `useProductCard`. Category menu fields are
170
+ `id`, `name`, `slug`, and `visible`. Filter out `visible === false` and the system category
171
+ `slug === "all-products"`; an empty category list is valid.
172
+
173
+ ```js
174
+ const { categories, nextCursor: categoryCursor } = await queryCategories({ limit: 100 });
175
+ const menu = categories.filter((c) => c.visible !== false && c.slug !== "all-products");
176
+ // Once a category has been selected:
177
+ const { products, nextCursor } = await queryProductsByCategory(selectedCategory.id, { limit: 24 });
144
178
  ```
179
+ Destructure the arrays; these calls never return bare arrays. Pass a result's `nextCursor` back
180
+ as `cursor` to the same query for the next page, stopping at null. Keep category and product
181
+ cursors separate; changing category, sort, or filters starts without a cursor. Handle failure separately from
182
+ loading so a failed request does not leave a spinner.
145
183
 
184
+ ### Product detail
146
185
  ```jsx
147
- // pages/ProductDetail.jsx — YOU build the whole product page. Route `/product/:slug` to it.
148
- // A thin view over useProductDetail: keep the data logic in the hook, render however fits the brand.
149
- import { useMemo } from "react";
150
- import { useParams, Link } from "react-router-dom";
186
+ import { useParams } from "react-router-dom";
151
187
  import { useProductDetail } from "@/hooks/useProductDetail";
188
+ import { useVariantOptions } from "@/hooks/useVariantOptions";
152
189
  import { productGallery } from "@/lib/storeImage";
153
190
 
154
191
  export default function ProductDetail() {
155
192
  const { slug } = useParams();
156
193
  const d = useProductDetail(slug);
157
- // useProductDetail(slug) returns:
158
- // product Wix product | null (loading) — has .name, .plainDescription (HTML), .slug
159
- // notFound bool — dead link (distinct from error)
160
- // error string | null — failed load; call retry() to reload
161
- // retry () => void
162
- // price formatted string, follows the selection ("€24.00")
163
- // compareAtPrice formatted string — a "was" price; strike it only when it differs from price
164
- // options, modifiers → feed useVariantOptions for the variant controls (block above)
165
- // selectedOptions, selectOption, modifierValues, setModifier → also for the variant controls
166
- // variant resolved variant | null (null until every option is picked)
167
- // focusMediaUrl image url for the selected choice — make the gallery show it
168
- // quantity, setQuantity(n) number (may be "" mid-edit) — keep min 1
169
- // inStock, canAdd, adding booleans that gate the buy button
170
- // submit async () => adds the resolved variant + quantity + modifiers to the cart
171
- const images = useMemo(() => productGallery(d.product), [d.product]); // [{ url, altText }], main first, de-duped
172
-
173
- // …you implement the page. Handle in order:
174
- // error → show d.error + a retry button (onClick={d.retry})
175
- // notFound → a "not found" message + a <Link to="/shop">
176
- // !product → a loading placeholder
177
- // else the product view, laid out to fit the brand:
178
- // • gallery from `images` — main + thumbnails, a carousel, a full-bleed hero (your call);
179
- // make the shown image follow d.focusMediaUrl when a choice is selected
180
- // • d.product.name · d.price (strike d.compareAtPrice when it differs) ·
181
- // d.product.plainDescription (HTML → dangerouslySetInnerHTML)
182
- // • variant controls — render d.options/d.modifiers via useVariantOptions (block above);
183
- // d.selectOption / d.setModifier update the selection
184
- // • a "pick an option to continue" hint when d.options.length && !d.variant
185
- // • a quantity control (d.quantity / d.setQuantity, min 1 — a −/+ stepper reads better than a
186
- // native number input) + the buy button:
187
- // <button disabled={!d.canAdd || d.adding} onClick={d.submit}>
188
- // {d.adding ? "Adding…" : d.inStock ? "Add to cart" : "Out of stock"}</button>
194
+ const { optionGroups, modifierGroups } = useVariantOptions(
195
+ d.options, d.modifiers, d.selectedOptions, d.modifierValues
196
+ );
197
+ // Keep all hooks above every conditional return, including while the product loads.
198
+ // d contains:
199
+ // product: Wix product | null; .name, .slug, .plainDescription (HTML)
200
+ // notFound: boolean; error: string | null (load error); retry: () => void
201
+ // price, compareAtPrice: formatted strings ("" if absent), follow the selected variant
202
+ // options, modifiers: arrays consumed by useVariantOptions above
203
+ // selectedOptions: { [optionId]: choiceId }; selectOption(optionId, choiceId)
204
+ // modifierValues: { [key]: value }; setModifier(key, value)
205
+ // variant: resolved variant | null; null if required choices are missing or no match exists
206
+ // focusMediaUrl: selected choice image, then variant image, or null
207
+ // quantity, setQuantity(n): number (may be "" mid-edit)
208
+ // inStock, canAdd, adding: booleans
209
+ // submit: async () => adds product + resolved variant + quantity + modifiers
210
+ // resolves undefined on success, null on failure (not a cart object or a boolean)
211
+ // Replace these placeholders with your error, not-found, and loading UI.
212
+ if (d.error) return null; // show d.error and offer d.retry()
213
+ if (d.notFound) return null;
214
+ if (!d.product) return null;
215
+
216
+ const images = productGallery(d.product); // [{ url, altText }], main first, de-duplicated
217
+ // Render your PDP using d, images, optionGroups, and modifierGroups.
218
+ return null;
219
+ }
220
+ ```
221
+ - Render `product.plainDescription` as HTML; strike `compareAtPrice` only when present and different from `price`.
222
+ - Make the gallery follow `focusMediaUrl` when the selected option changes.
223
+ - Render the option/modifier controls below; show a selection hint when `options.length && !variant`.
224
+ - Keep quantity at least 1. Disable adding when `!canAdd || adding`; call `submit()` only with a loaded product and `canAdd`. `submit()` coerces quantity to at least 1 but does not itself enforce `canAdd`.
225
+ - `canAdd` checks variant resolution, variant stock, and mandatory modifier values. `inStock` defaults to true without a resolved variant; use `canAdd` for the full gate.
226
+ - `submit()` resets `adding` after completion and preserves the cart result: `undefined` on success, `null` on failure. Add failures live in `useCart().error`, not the PDP load `error`; the shipped drawer opens to display them.
227
+
228
+ For an optional **Buy Now** button, check the add result before checkout:
229
+ ```jsx
230
+ // Inside ProductDetail, with the other hooks above any conditional returns:
231
+ const { checkout, loading: cartLoading } = useCart(); // named import from @/context/CartContext
232
+ async function buyNow() {
233
+ if (!d.product || !d.canAdd || d.adding || cartLoading) return;
234
+ const result = await d.submit();
235
+ if (result === null) return; // add failed; preserve its error and do not check out the old cart
236
+ await checkout();
189
237
  }
238
+ // <button disabled={!d.canAdd || d.adding || cartLoading} onClick={buyNow}>Buy now</button>
239
+ ```
240
+ For direct `addToCart(...)` calls, use the same `result === null` check before checkout.
241
+ A resolved promise alone does not mean success; a truthiness check also rejects successful `undefined`.
242
+
243
+ ### Variant and modifier controls
244
+ The product-detail example above provides both groups:
245
+ ```js
246
+ // optionGroups: [{ id, name, isColor, choices: [
247
+ // { choiceId, name, colorCode: string | null, isColorSwatch, inStock, selected }
248
+ // ] }]
249
+ // modifierGroups: [{ key, name, mandatory, type: 'choices'|'text',
250
+ // choices?: [{ key, name, selected }], value?: string }]
251
+ // Select option: d.selectOption(group.id, choice.choiceId)
252
+ // Select modifier: d.setModifier(m.key, choice.key)
253
+ // Text modifier: d.setModifier(m.key, text)
254
+ ```
255
+ Retired option choices are filtered out. Respect each choice's `inStock` and modifier's
256
+ `mandatory` flag; the hook supplies colour values and selection state without prescribing layout.
257
+
258
+ ### Images
259
+ `useProductCard` returns normalized `image`/`hoverImage`, and `useProductDetail` returns normalized
260
+ `focusMediaUrl`; use them directly. For a gallery or images outside those hooks, import from
261
+ `@/lib/storeImage`:
262
+
263
+ | Helper | Contract |
264
+ |---|---|
265
+ | `productGallery(product)` | `[{ url, altText }]`, main image first, de-duplicated; skips entries without an image URL; empty array when no images |
266
+ | `productImage(product)` | Normalized primary image URL or null, for a standalone catalog image |
267
+ | `storeImage(value)` | Accepts a URL string, `{ image: { url } }`, or `{ url }`; prefixes `//` with `https:`, returns other URLs unchanged or null when absent |
268
+
269
+ Gallery URLs are normalized too. Handle missing images; don't reconstruct media URLs or resolve
270
+ choice/variant media yourself.
271
+
272
+ ### Cart
273
+ Use the shipped `CartDrawer` and `CartButton` with `CartProvider`, as wired below. The drawer
274
+ already renders cart lines, quantities, subtotal, and checkout; you do not need to build a cart UI.
275
+ Your card/PDP adds products through the named `useCart` export from `@/context/CartContext`
276
+ within `CartProvider` (the PDP hook calls it for you).
277
+
278
+ ```js
279
+ const { addToCart, isOpen, setIsOpen, loading, error, clearError } = useCart();
280
+ // addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields? }?)
281
+ // isOpen: boolean; setIsOpen(true): open the drawer; setIsOpen(false): close it
282
+ // loading: mutation-in-progress boolean; disable repeated adds while true
283
+ // error: string | null; clearError(): clears it
190
284
  ```
285
+ `addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras are string maps:
286
+ `modifierChoices: { [modifier.key]: choiceKey }` and
287
+ `customTextFields: { [modifier.freeTextSettings.key]: userInput }`; include mandatory values.
288
+
289
+ The context's add/remove/update/checkout methods return promises resolving to `undefined` on
290
+ success or `null` on failure, storing the failure in `error`. They clear the previous error and
291
+ set `loading` during the operation. Successful add updates the server-cart snapshot and opens the
292
+ drawer; mutation failures also open it to display the error. Your card/PDP can additionally
293
+ surface `error` inline. These context methods do not return the updated cart or checkout URL.
294
+ The lower-level REST helpers can reject; don't apply their rejection contract to `useCart()`.
295
+
296
+ #### Custom cart UI — optional
297
+ Only use these additional contracts if you choose to render your own cart surface. They come
298
+ from the same `useCart()` context; mutation results and error handling are described above.
299
+
300
+ ```js
301
+ const { cart, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
302
+ // cart: server cart | null; itemCount: sum of confirmed line quantities
303
+ // removeItem(lineItemId); updateQuantity(lineItemId, qty)
304
+ // checkout(); refreshCart()
305
+ ```
306
+ For custom cart presentation, `cart?.lineItems ?? []` is the list. Each line has `id`,
307
+ `name.original`, and optional `attributes.image.url`. Option/modifier labels are in
308
+ `attributes.descriptionLines`: `[{ name: { original }, plainText?: { original },
309
+ colorInfo?: { original, code } }]`; use `plainText.original` or `colorInfo.original` for the value.
310
+ Update/remove use the line's `id`, not a catalog product id.
311
+
312
+ Normalize custom cart images with `storeImage(line.attributes?.image?.url)` (see **Images**).
313
+
314
+ `checkout()` navigates to the hosted checkout URL; it refuses empty carts and lines with a status
315
+ other than `IN_STOCK`, and refreshes the cart after failure. `refreshCart()` replaces the snapshot
316
+ and resolves to `undefined`; its underlying read returns null for no cart or a failed request. It
317
+ does not set mutation `loading` or `error`.
318
+
319
+ Cart money is `{ amount, convertedAmount }`, with no formatted string. `amount` is in site
320
+ currency; `convertedAmount` is in display currency. Format
321
+ `money.convertedAmount ?? money.amount` with `Intl.NumberFormat` and
322
+ `cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode` (the shipped drawer falls
323
+ back to `USD`). Use server `cart.subtotal` and line `pricing.totalPrice`; never sum lines yourself.
324
+ The cart has no `subtotalAfterDiscounts`, `discount`, or `appliedDiscounts`; discounted summary
325
+ totals require a currentCartV2 estimate/calculate `summary.priceSummary`, which these helpers do
326
+ not fetch. Tax and shipping resolve at checkout.
327
+
328
+ Line `quantityInfo.confirmedQuantity` is the current quantity; `availableQuantity` caps increases
329
+ when finite. `status` can be `IN_STOCK`, `PARTIALLY_IN_STOCK`, `OUT_OF_STOCK`, or
330
+ `REMOVED_FROM_CATALOG`; surface unavailable lines and prevent checkout until resolved.
331
+
332
+ ## Routes and provider
333
+ **No shipped source reads needed to wire this.** `CartDrawer` and `CartButton`
334
+ are default exports that take **no props**. `CartProvider` is a named export accepting `children`;
335
+ wire these exactly as shown below.
336
+ When adding storefront routes and providers to `src/App.jsx`, preserve the existing platform
337
+ authentication setup, including `AuthProvider`, `useAuth`, and their `@/lib/AuthContext` imports.
338
+ Do not remove or replace that authentication logic.
339
+ - Wrap the routed tree in `<CartProvider>` (from `@/context/CartContext`).
340
+ - Put your **header + footer in a `Layout`** that renders `<Outlet/>` between them, and nest every
341
+ route under one pathless `<Route element={<Layout/>}>`. Your brand chrome then wraps **every** page
342
+ — including your Shop and product-detail pages. Mount `<CartDrawer/>` once in the Layout.
343
+ - Routes under the Layout: `/shop` → **your `Shop`** (renders your grid); `/product/:slug` → **your
344
+ `ProductDetail`**; `/` → **your `Home`**.
345
+
346
+ Mount the default export `CartButton` from `@/components/CartButton` once in your header. It opens
347
+ the drawer and shows the live count, inheriting `currentColor`; use it as-is, without a nested button.
191
348
 
192
349
  ```jsx
193
- import { useRef, useState, useEffect } from "react";
194
350
  import { Routes, Route, Outlet } from "react-router-dom";
195
351
  import { CartProvider } from "@/context/CartContext";
196
352
  import CartDrawer from "@/components/CartDrawer";
197
- import WixManageBanner from "@/components/WixManageBanner"; // shipped, preview-only · default export, no props
198
- import Shop from "@/pages/Shop"; // shipped · default export, no props
353
+ import Shop from "@/pages/Shop"; // YOU build
199
354
  import ProductDetail from "@/pages/ProductDetail"; // YOU build
200
355
  import Home from "@/pages/Home"; // YOU build
201
- import Header from "@/components/Header"; // YOU build — plain in-flow markup, NOT position:fixed
356
+ import Header from "@/components/Header"; // YOU build
202
357
  import Footer from "@/components/Footer"; // YOU build
203
358
 
204
359
  function Layout() {
205
- const topRef = useRef(null);
206
- const [offset, setOffset] = useState(0);
207
- useEffect(() => { // measure the fixed region → pad content below it
208
- const ro = new ResizeObserver(() => setOffset(topRef.current?.offsetHeight ?? 0));
209
- if (topRef.current) ro.observe(topRef.current);
210
- return () => ro.disconnect();
211
- }, []);
212
360
  return (<>
213
- <div ref={topRef} style={{ position: "fixed", top: 0, left: 0, right: 0, zIndex: 50 }}>
214
- <WixManageBanner /> {/* null on the published site / when dismissed */}
215
- <Header /> {/* your brand header, in-flow inside this fixed block */}
216
- </div>
217
- <div style={{ paddingTop: offset }}> {/* clears the chrome; shrinks when the banner is dismissed */}
218
- <Outlet /> {/* shipped Shop + your pages render here */}
219
- <Footer />
220
- </div>
221
- <CartDrawer /> {/* overlays every page */}
361
+ <Header />
362
+ <Outlet />
363
+ <Footer />
364
+ <CartDrawer />
222
365
  </>);
223
366
  }
224
367
 
@@ -226,167 +369,56 @@ function Layout() {
226
369
  <Routes>
227
370
  <Route element={<Layout />}> {/* chrome wraps all */}
228
371
  <Route path="/" element={<Home />} /> {/* yours */}
229
- <Route path="/shop" element={<Shop />} /> {/* shipped, as-is */}
372
+ <Route path="/shop" element={<Shop />} /> {/* yours */}
230
373
  <Route path="/product/:slug" element={<ProductDetail />} /> {/* yours */}
231
374
  </Route>
232
375
  </Routes>
233
376
  </CartProvider>
234
377
  ```
235
378
 
236
- ## What you build (not shipped)
237
- The **home / landing page**, the **`Header`** (mount `<CartButton/>` in it) and a **`Footer`** — the
238
- two you drop into the `Layout` (STEP 3) so they wrap every route — plus the overall brand story,
239
- styled from the same base44 tokens/classes. **Reuse your own `ProductGrid`** (STEP 3) and the shipped
240
- cart pieces — a featured strip is just `queryProducts` + your `ProductGrid`; the nav is a `<CartButton/>`
241
- (a clean cart-**icon** button with a live-count badge — render it as-is, don't wrap it in your own
242
- text button) + a link to `/shop`:
243
-
244
- ```jsx
245
- import { useState, useEffect } from "react";
246
- import { Link } from "react-router-dom";
247
- import { queryProducts } from "@/rest/wix-store-catalog";
248
- import ProductGrid from "@/components/ProductGrid"; // yours (STEP 3)
249
- import CartButton from "@/components/CartButton"; // shipped
250
-
251
- // Responsive header: choose ONE branch with a state flag, so <CartButton/> mounts once.
252
- // Do NOT render a desktop nav AND a mobile nav toggled by `hidden md:flex` / `md:hidden`:
253
- // these navs are inline-styled, and an inline `display` beats a Tailwind class, so `hidden`
254
- // never applies — BOTH branches render and you get two cart buttons. One branch = one cart.
255
- export function Header() { // in your nav
256
- const [mobile, setMobile] = useState(() => window.innerWidth < 768);
257
- useEffect(() => {
258
- const onResize = () => setMobile(window.innerWidth < 768);
259
- window.addEventListener("resize", onResize); // keep it reactive to viewport changes
260
- return () => window.removeEventListener("resize", onResize);
261
- }, []);
262
- return (
263
- <nav style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
264
- {/* brand/logo */}
265
- {mobile
266
- ? <YourMenu /> // your hamburger + <CartButton/> here
267
- : <div style={{ display: "flex", gap: 24 }}><Link to="/shop">Shop</Link><CartButton /></div>}
268
- </nav>
269
- );
270
- }
271
- export function Featured() { // on your home page
272
- const [products, setProducts] = useState(null); // null → ProductGrid shows skeletons
273
- // NB: queryProducts returns { products, nextCursor } — destructure the array.
274
- useEffect(() => {
275
- queryProducts({ limit: 8 })
276
- .then(({ products }) => setProducts(products))
277
- .catch(() => setProducts([])); // land on the empty state, not a spinner
278
- }, []);
279
- return <ProductGrid products={products} loading={products === null} empty="Products coming soon." />;
280
- }
281
- ```
282
- Everything reads base44's design tokens (`index.css`), so your home/nav match the shipped pages
283
- automatically. `<CartButton/>` is an icon button (live-count badge) — drop it in as-is, it inherits `currentColor`.
284
-
285
- **Editing a component and the change doesn't show? It's the preview, not your code.** The dev preview
286
- can serve a stale module after a write. Before diagnosing a visual bug you just "fixed", do a fresh
287
- full navigate/reload of the preview and re-check — don't keep rewriting correct code against a stale
288
- render.
289
-
290
- ## Using the client from your own UI (cart, hand-built images)
379
+ ### Optional banner integration — enabled entry flows only
380
+ Follow the entry guide's banner choice. If it disables the banner, use the common wiring above
381
+ without a banner import, mount, or banner-specific fixed region.
291
382
 
292
- > Migrating from Cart V1 / Checkout V1? These helpers are V2-only — see the [migration guide](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/migration-guide) for the before/after.
383
+ When enabled, `WixManageBanner` is a default export with no props. It reads its own configuration,
384
+ links to the site's dashboard, and renders only in preview; it returns null
385
+ when dismissed or while the site id is a placeholder. Mount it once above the header in one fixed
386
+ region; keep the header in flow within that region and offset the content by its measured height
387
+ so dismissal or resizing leaves no gap or overlap. Replace only the example's `Layout` with:
293
388
 
294
389
  ```jsx
295
- import { Link } from "react-router-dom";
296
- import { useCart } from "@/context/CartContext";
297
-
298
- // useCart() gives:
299
- // { cart, itemCount, isOpen, setIsOpen, loading, error, clearError(),
300
- // addToCart(productId, variantId?, qty=1, { modifierChoices?, customTextFields? }?),
301
- // removeItem(lineItemId), updateQuantity(lineItemId, qty), checkout(), refreshCart() }
302
- // Every mutation catches its own failure into `error` (the shipped CartDrawer renders it), so a
303
- // refusal — an empty cart, or a line item whose status left IN_STOCK — reaches the buyer instead
304
- // of becoming an unhandled rejection.
305
- //
306
- // Cart money: every amount is a ConvertedMoney { amount, convertedAmount } with NO formatted string —
307
- // `amount` is in the site currency, `convertedAmount` in the display currency; format the number
308
- // yourself (Intl.NumberFormat with the cart's currency —
309
- // `cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode`) so the symbol and grouping
310
- // match the locale. The V2 cart has no `subtotalAfterDiscounts`/`discount`/`appliedDiscounts`: it
311
- // carries only a raw `cart.subtotal` (ConvertedMoney). The authoritative discounted totals come from
312
- // a currentCartV2 estimate/calculate `summary.priceSummary`, not the cart — call that when you need
313
- // the after-coupon figure; otherwise show `cart.subtotal`. Never sum line items yourself — tax and
314
- // shipping resolve at checkout.
315
- // A line's `status` (IN_STOCK / PARTIALLY_IN_STOCK / OUT_OF_STOCK) tells a quantity control whether
316
- // another increment is still fulfillable.
317
-
318
- function CartCount() { // header badge
319
- const { itemCount, setIsOpen } = useCart();
320
- return <button onClick={() => setIsOpen(true)}>Cart ({itemCount})</button>;
321
- }
322
-
323
- // Buy from a card → link to the PDP; your ProductDetail (on useProductDetail) owns options/variants + add-to-cart.
324
- // (Listing helpers return no variants — only getProductBySlug does — so buying happens on the PDP.)
325
- const CardBuy = ({ product }) => <Link to={`/product/${product.slug}`}>View</Link>;
326
-
327
- // Doing add-to-cart yourself? addToCart resolves for an option-less product; wrap it — it rejects on
328
- // out-of-stock / empty cart / a missing required selection, and you show that message.
329
- async function quickAdd(addToCart, product) {
330
- try { await addToCart(product.id); } catch (e) { alert(e.message); }
331
- }
390
+ import { useRef, useState, useEffect } from "react";
391
+ import WixManageBanner from "@/components/WixManageBanner";
332
392
 
333
- // An image you render yourself (hero / custom card): normalise the url through lib/storeImage — Wix
334
- // returns these protocol-relative — and keep a token bg so a just-generated url that 404s for a
335
- // second reads as a surface, not a blank block.
336
- import { storeImage, productImage, productGallery } from "@/lib/storeImage";
337
- function BrandImage({ url, alt }) {
338
- return <div className="bg-card"><img src={storeImage(url)} alt={alt} /></div>;
393
+ function Layout() {
394
+ const topRef = useRef(null);
395
+ const [offset, setOffset] = useState(0);
396
+ useEffect(() => {
397
+ const ro = new ResizeObserver(() => setOffset(topRef.current?.offsetHeight ?? 0));
398
+ if (topRef.current) ro.observe(topRef.current);
399
+ return () => ro.disconnect();
400
+ }, []);
401
+ return (<>
402
+ <div ref={topRef} style={{ position: "fixed", top: 0, left: 0, right: 0, zIndex: 50 }}>
403
+ <WixManageBanner />
404
+ <Header />
405
+ </div>
406
+ <div style={{ paddingTop: offset }}>
407
+ <Outlet />
408
+ <Footer />
409
+ </div>
410
+ <CartDrawer />
411
+ </>);
339
412
  }
340
- // productImage(product) → the catalog image · productGallery(product) → every image, main first,
341
- // de-duplicated (media.itemsInfo.items repeats the main one and video items carry no image url).
342
- ```
343
-
344
- ## Extending the client
345
- Building something beyond the shipped pages? Copy these:
346
-
347
- ```jsx
348
- // Every catalog/cart list helper returns { <plural>, nextCursor } — destructure the array:
349
- const { products, nextCursor } = await queryProducts({ limit: 24 });
350
- const { categories } = await queryCategories();
351
- const menu = categories.filter((c) => c.slug !== "all-products"); // drop Wix's system category
352
- const { products: inCategory } = await queryProductsByCategory(menu[0].id, { limit: 24 });
353
-
354
- // product.plainDescription is HTML → render as HTML (the PDP does this):
355
- <div dangerouslySetInnerHTML={{ __html: product.plainDescription }} />
356
- // image urls live at: product.media.main.image.url · cart lineItems[].attributes.image.url
357
413
  ```
358
414
 
359
- Fallback only — when you hit an error or need something not shown here (coupons, members, a field
360
- these snippets don't have): read the relevant shipped file under `src/`, or look it up via the
361
- **`wix-docs`** skill. Each helper in `wix-store-catalog.js` / `wix-store-cart.js` links its own
362
- reference page inline; these are the areas they sit in:
363
- - Stores catalog (products, categories, inventory): https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3.md
364
- - eCommerce (cart, checkout, orders): https://dev.wix.com/docs/api-reference/business-solutions/e-commerce.md
365
- - Headless redirect session (hosted checkout): https://dev.wix.com/docs/api-reference/business-management/headless/redirects.md
415
+ ## Missing capabilities
416
+ For anything these interfaces do not cover, consult the official Wix API documentation using
417
+ the documentation skill available in your environment.
418
+ For a specifically missing field/interface or an observed runtime error, read only the relevant
419
+ shipped file; catalog and cart helpers link their API references inline.
366
420
 
367
421
  ## Hard rules
368
- - Style via base44 design tokens (`index.css` / shadcn Tailwind classes), never by rewriting the shipped pages or adding a parallel theme file. Everything you build (card, grid, PDP, variant controls, Home) draws from the same tokens.
369
- - Header/footer live in a `Layout` around `<Outlet/>` (STEP 3) — never edit the shipped `Shop` to add chrome.
370
- - The Layout's fixed top region owns positioning: `<WixManageBanner/>` above `<Header/>`; your `Header` is plain in-flow markup (not `position:fixed`).
422
+ - Header/footer live in a `Layout` around `<Outlet/>` (see **Routes and provider**) — keep shared chrome out of individual pages.
371
423
  - Checkout goes through the shipped cart (redirect-session) — never a hand-built `/checkout` URL.
372
- - Render live Wix data or the shipped empty state — never mock products.
373
-
374
- ## Point the user to their dashboard
375
- Provide deep links so the owner can edit content (substitute the site's `metaSiteId`):
376
- - **Products** — `https://manage.wix.com/dashboard/{metaSiteId}/wix-stores/products`
377
- - **Categories** — `https://manage.wix.com/dashboard/{metaSiteId}/wix-stores/categories/list`
378
-
379
- ## Seeding
380
- Seed the catalog per `seed/SEED.md` (the build-time `setupStore` module) — separate from this
381
- client build; run in parallel.
382
-
383
- ## Verify (before declaring done)
384
- - [ ] Client files copied into `src/`; `WIX_CLIENT_ID` set (not the placeholder).
385
- - [ ] **The pieces you own exist and are wired:** `components/ProductCard.jsx`, `components/ProductGrid.jsx` (imported by `Shop` + your Home), and `pages/ProductDetail.jsx` (routed at `/product/:slug`, with working variant controls). `/shop` and a PDP compile and render (they won't until these exist).
386
- - [ ] Brand palette lives in `index.css` (`:root`/`.dark`); no parallel theme file; the shipped `Shop` page not restyled or rewritten.
387
- - [ ] **Opened `/shop` and a product detail page** (not just the home page) and confirmed your cards render themed (surface, text, brand color) with images; the grid shows its loading + empty states; the PDP shows the gallery, price, variant controls, and add-to-cart, and its error/not-found/loading states hold.
388
- - [ ] `Layout` (fixed `<WixManageBanner/>` + `<Header/>` region, then `<Outlet/>` + Footer) wraps all routes; the shipped `Shop` untouched; content clears the fixed chrome; `<CartProvider>` wraps the tree; `<CartDrawer/>` mounted; `<CartButton/>` in the header.
389
- - [ ] Cart survives reload (same visitor); add / update-qty / remove work; checkout redirects; the drawer shows a **subtotal**.
390
- - [ ] Empty catalog shows the shipped empty state; no mock products anywhere.
391
- - [ ] A product with several images shows **thumbnails** on the PDP, and one with per-variant prices shows a **range** on its card.
392
- - [ ] Categories seeded? The `/shop` menu lists them (minus the auto-created `all-products`) and filters; a catalog past one page shows **Load more**.
424
+ - Render live Wix data or your empty state — never mock products.