@wix/agent-skills 1.9.0 → 1.11.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 (47) hide show
  1. package/package.json +4 -1
  2. package/skills/wix-headless/references/DESIGN_SYSTEM.md +6 -16
  3. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +1 -1
  4. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +19 -0
  5. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +2 -2
  6. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +3 -3
  7. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +2 -2
  8. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
  9. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
  10. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +7 -7
  11. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +4 -4
  12. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +2 -2
  13. package/skills/wix-headless/references/astro/templates/Footer.astro +4 -4
  14. package/skills/wix-headless/references/astro/templates/Navigation.astro +4 -4
  15. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +147 -6
  16. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +21 -4
  17. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +11 -2
  18. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +8 -8
  19. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +94 -26
  20. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +53 -10
  21. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +128 -20
  22. package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +3 -3
  23. package/skills/wix-headless/references/astro/templates/ecom/cart.astro +3 -3
  24. package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +17 -17
  25. package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +3 -3
  26. package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +4 -4
  27. package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +19 -18
  28. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +3 -3
  29. package/skills/wix-headless/references/astro/templates/global.css +17 -15
  30. package/skills/wix-headless/references/astro/templates/index.astro +4 -4
  31. package/skills/wix-headless/references/astro/templates/stores/components-stores.css +31 -31
  32. package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +4 -4
  33. package/skills/wix-headless/references/bookings/FLOW.md +93 -32
  34. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +8 -3
  35. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +60 -0
  36. package/skills/wix-headless/references/custom/bookings/WIRING.md +60 -9
  37. package/skills/wix-headless/references/shared/DESIGN_MD.md +2 -3
  38. package/skills/wix-headless/references/shared/IMPLEMENTER.md +4 -4
  39. package/skills/wix-headless/references/shared/STYLING.md +15 -18
  40. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +2 -2
  41. package/skills/wix-headless/references/verticals/bookings.md +5 -3
  42. package/skills/wix-headless/scripts/compose.mjs +4 -19
  43. package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -3
  44. package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -28
  45. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +1 -8
  46. package/skills/wix-manage/references/bookings/create-class-service.md +3 -28
  47. package/skills/wix-manage/references/bookings/create-course-service.md +3 -28
@@ -10,10 +10,12 @@
10
10
 
11
11
  @theme {
12
12
  /* {{theme}} — Composer substitutes the full palette here from designTokens:
13
- every --color-*, --font-*, --spacing-* (2xs..4xl), --container-*, and
14
- --radius-* line. The required-token set (enforced by compose.mjs)
15
- MUST all be present, or components render unstyled. Container scale is
16
- separate from spacing — never alias --container-3xl to --spacing-3xl. */
13
+ every --color-*, --font-*, --container-*, and --radius-* line. The
14
+ required-token set (enforced by compose.mjs) MUST all be present, or
15
+ components render unstyled. Spacing is NOT a token here — it uses
16
+ Tailwind's built-in numeric scale (gap-4, py-24, …); never add a named
17
+ --spacing-* scale, since that namespace also powers the width utilities
18
+ (max-w-*/w-*) and would shadow them. */
17
19
  }
18
20
 
19
21
  /* Reading-width helper so prose pages center without guessing Tailwind sizes. */
@@ -25,7 +27,7 @@
25
27
  /* Shared button primitive — declared with @utility so the variants below can
26
28
  @apply it (a plain contract class is NOT @apply-able in Tailwind v4). */
27
29
  @utility btn {
28
- @apply inline-flex items-center justify-center px-lg py-md rounded-md font-medium;
30
+ @apply inline-flex items-center justify-center px-6 py-4 rounded-md font-medium;
29
31
  transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease;
30
32
  }
31
33
 
@@ -130,7 +132,7 @@
130
132
  height: 1px;
131
133
  background: var(--color-accent);
132
134
  opacity: 0.6;
133
- margin-block: var(--spacing-lg);
135
+ margin-block: 1.5rem;
134
136
  }
135
137
 
136
138
  /* ── Universal site chrome ────────────────────────────────────────────────── */
@@ -204,8 +206,8 @@ body[data-navigating="true"] .product-grid {
204
206
  min-width: 12rem;
205
207
  display: flex;
206
208
  flex-direction: column;
207
- gap: var(--spacing-2xs);
208
- padding: var(--spacing-sm);
209
+ gap: 0.25rem;
210
+ padding: 0.75rem;
209
211
  background: var(--color-paper);
210
212
  border: 1px solid var(--color-rule);
211
213
  border-radius: var(--radius-md);
@@ -223,7 +225,7 @@ body[data-navigating="true"] .product-grid {
223
225
  .site-nav-sublink {
224
226
  font-size: 0.875rem;
225
227
  color: var(--color-ink-soft, var(--color-mute));
226
- padding-block: var(--spacing-2xs);
228
+ padding-block: 0.25rem;
227
229
  }
228
230
  .site-nav-sublink:hover,
229
231
  .site-nav-sublink[aria-current="page"] {
@@ -237,13 +239,13 @@ body[data-navigating="true"] .product-grid {
237
239
  .category-rail-inner {
238
240
  display: flex;
239
241
  flex-wrap: wrap;
240
- gap: var(--spacing-sm);
241
- padding-block: var(--spacing-md);
242
+ gap: 0.75rem;
243
+ padding-block: 1rem;
242
244
  }
243
245
  .category-pill {
244
246
  display: inline-flex;
245
247
  align-items: center;
246
- padding: var(--spacing-2xs) var(--spacing-md);
248
+ padding: 0.25rem 1rem;
247
249
  border: 1px solid var(--color-rule);
248
250
  border-radius: var(--radius-sm);
249
251
  font-size: 0.8125rem;
@@ -262,12 +264,12 @@ body[data-navigating="true"] .product-grid {
262
264
  border-color: var(--color-ink);
263
265
  }
264
266
  .pagination {
265
- margin-block: var(--spacing-xl);
267
+ margin-block: 2rem;
266
268
  }
267
269
  .pagination-inner {
268
270
  display: flex;
269
271
  justify-content: space-between;
270
- gap: var(--spacing-md);
272
+ gap: 1rem;
271
273
  }
272
274
  .pagination-link {
273
275
  color: var(--color-ink);
@@ -282,5 +284,5 @@ body[data-navigating="true"] .product-grid {
282
284
  .breadcrumbs {
283
285
  font-size: 0.8125rem;
284
286
  color: var(--color-mute);
285
- margin-bottom: var(--spacing-sm);
287
+ margin-bottom: 0.75rem;
286
288
  }
@@ -17,8 +17,8 @@ import Layout from '../layouts/Layout.astro';
17
17
  ---
18
18
  <Layout>
19
19
  <section class="relative">
20
- <div class="max-w-6xl mx-auto px-lg py-4xl grid grid-cols-1 md:grid-cols-2 gap-3xl items-center">
21
- <div class="flex flex-col gap-lg">
20
+ <div class="max-w-6xl mx-auto px-6 py-24 grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
21
+ <div class="flex flex-col gap-6">
22
22
  <h1 class="font-display text-4xl md:text-5xl leading-tight">{{shell.heroHeadline}}</h1>
23
23
  <p class="text-lg text-mute max-w-prose">{{shell.heroSub}}</p>
24
24
  </div>
@@ -34,7 +34,7 @@ import Layout from '../layouts/Layout.astro';
34
34
 
35
35
  {{home-markers}}
36
36
 
37
- <section class="max-w-6xl mx-auto px-lg py-4xl grid grid-cols-1 md:grid-cols-2 gap-3xl items-center">
37
+ <section class="max-w-6xl mx-auto px-6 py-24 grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
38
38
  <div
39
39
  class="about-image rounded-md order-last md:order-first"
40
40
  data-decorative-slot="about"
@@ -42,7 +42,7 @@ import Layout from '../layouts/Layout.astro';
42
42
  >
43
43
  <!-- orchestrator injects <img src={decorativeImages.about} …> here -->
44
44
  </div>
45
- <div class="flex flex-col gap-md max-w-prose">
45
+ <div class="flex flex-col gap-4 max-w-prose">
46
46
  <hr class="editorial-rule w-16 ml-0" />
47
47
  <h2 class="font-display text-3xl leading-tight">The story behind {{brand.name}}</h2>
48
48
  <p class="text-ink leading-relaxed">
@@ -28,7 +28,7 @@
28
28
  .product-grid {
29
29
  display: grid;
30
30
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
31
- gap: var(--spacing-xl);
31
+ gap: 2rem;
32
32
  transition: opacity 200ms ease;
33
33
  }
34
34
  body[data-navigating="true"] .product-grid {
@@ -40,8 +40,8 @@ body[data-navigating="true"] .product-grid {
40
40
  position: relative;
41
41
  display: flex;
42
42
  flex-direction: column;
43
- gap: var(--spacing-md);
44
- padding-bottom: var(--spacing-md);
43
+ gap: 1rem;
44
+ padding-bottom: 1rem;
45
45
  background: var(--color-paper, var(--color-cream));
46
46
  border-radius: var(--radius-xl);
47
47
  overflow: hidden;
@@ -59,7 +59,7 @@ body[data-navigating="true"] .product-grid {
59
59
 
60
60
  .product-card-media {
61
61
  position: relative;
62
- margin: var(--spacing-md);
62
+ margin: 1rem;
63
63
  aspect-ratio: 4 / 5;
64
64
  background: var(--color-paper-warm, var(--color-cream));
65
65
  border-radius: var(--radius-md);
@@ -75,8 +75,8 @@ body[data-navigating="true"] .product-grid {
75
75
  .product-card-body {
76
76
  display: flex;
77
77
  flex-direction: column;
78
- gap: var(--spacing-xs);
79
- padding-inline: var(--spacing-md);
78
+ gap: 0.5rem;
79
+ padding-inline: 1rem;
80
80
  }
81
81
  .product-card-name {
82
82
  margin: 0;
@@ -95,8 +95,8 @@ body[data-navigating="true"] .product-grid {
95
95
 
96
96
  .product-card-ribbon {
97
97
  position: absolute;
98
- top: var(--spacing-md);
99
- left: var(--spacing-md);
98
+ top: 1rem;
99
+ left: 1rem;
100
100
  padding: 0.375rem 0.625rem;
101
101
  background: var(--color-accent);
102
102
  color: var(--color-ink);
@@ -110,8 +110,8 @@ body[data-navigating="true"] .product-grid {
110
110
  }
111
111
  .product-card-index {
112
112
  position: absolute;
113
- top: var(--spacing-md);
114
- right: var(--spacing-md);
113
+ top: 1rem;
114
+ right: 1rem;
115
115
  padding: 0.375rem 0.5rem;
116
116
  background: rgba(255, 255, 255, 0.85);
117
117
  color: var(--color-ink);
@@ -132,19 +132,19 @@ body[data-navigating="true"] .product-grid {
132
132
  background: var(--color-paper-warm, var(--color-cream));
133
133
  border: 1px solid var(--color-rule);
134
134
  border-left: 2px solid var(--color-accent);
135
- padding: var(--spacing-lg);
135
+ padding: 1.5rem;
136
136
  border-radius: var(--radius-md);
137
137
  display: flex;
138
138
  flex-direction: column;
139
- gap: var(--spacing-md);
139
+ gap: 1rem;
140
140
  }
141
141
  .offer-callout-item {
142
142
  display: flex;
143
143
  flex-direction: column;
144
- gap: var(--spacing-xs);
144
+ gap: 0.5rem;
145
145
  }
146
146
  .offer-callout-item + .offer-callout-item {
147
- padding-top: var(--spacing-md);
147
+ padding-top: 1rem;
148
148
  border-top: 1px solid var(--color-rule);
149
149
  }
150
150
  .offer-callout-badge {
@@ -190,7 +190,7 @@ body[data-navigating="true"] .product-grid {
190
190
  of this template used `@apply font-heading text-bark/70 bg-parchment …`,
191
191
  which assumed a different token vocabulary than current designer output
192
192
  (`paper`/`ink`/`font-display` …) and built-failed on every run that loaded
193
- it. Direct `var(--color-paper)` / `var(--spacing-sm)` calls work against
193
+ it. Direct `var(--color-paper)` / `0.75rem` calls work against
194
194
  any designer-published token set as long as the core scales exist (they
195
195
  always do). Use fallback chains (`var(--color-paper-warm, var(--color-cream))`)
196
196
  when a token is sometimes-published.
@@ -200,8 +200,8 @@ body[data-navigating="true"] .product-grid {
200
200
  .option-group {
201
201
  display: flex;
202
202
  flex-direction: column;
203
- gap: var(--spacing-sm);
204
- margin-bottom: var(--spacing-lg);
203
+ gap: 0.75rem;
204
+ margin-bottom: 1.5rem;
205
205
  }
206
206
  .option-label {
207
207
  font-family: var(--font-display);
@@ -213,10 +213,10 @@ body[data-navigating="true"] .product-grid {
213
213
  .option-choices {
214
214
  display: flex;
215
215
  flex-wrap: wrap;
216
- gap: var(--spacing-sm);
216
+ gap: 0.75rem;
217
217
  }
218
218
  .option-pill {
219
- padding: var(--spacing-sm) var(--spacing-md);
219
+ padding: 0.75rem 1rem;
220
220
  border: 1px solid var(--color-ink);
221
221
  background: transparent;
222
222
  color: var(--color-ink);
@@ -255,14 +255,14 @@ body[data-navigating="true"] .product-grid {
255
255
  cursor: not-allowed;
256
256
  }
257
257
  .quantity-value {
258
- padding: var(--spacing-sm) var(--spacing-md);
258
+ padding: 0.75rem 1rem;
259
259
  font-variant-numeric: tabular-nums;
260
260
  }
261
261
 
262
262
  /* --- Modifier free-text input (ProductPurchase.tsx, FREE_TEXT render type) --- */
263
263
  .modifier-text {
264
264
  width: 100%;
265
- padding: var(--spacing-sm);
265
+ padding: 0.75rem;
266
266
  font-family: var(--font-body);
267
267
  font-size: 0.875rem;
268
268
  border: 1px solid var(--color-rule);
@@ -290,9 +290,9 @@ body[data-navigating="true"] .product-grid {
290
290
  .back-in-stock-form {
291
291
  display: flex;
292
292
  flex-direction: column;
293
- gap: var(--spacing-md);
294
- margin-top: var(--spacing-md);
295
- padding: var(--spacing-lg);
293
+ gap: 1rem;
294
+ margin-top: 1rem;
295
+ padding: 1.5rem;
296
296
  border: 1px solid var(--color-rule);
297
297
  border-radius: var(--radius-md);
298
298
  background: var(--color-paper, var(--color-cream));
@@ -300,7 +300,7 @@ body[data-navigating="true"] .product-grid {
300
300
  .back-in-stock-headline {
301
301
  display: flex;
302
302
  flex-direction: column;
303
- gap: var(--spacing-sm);
303
+ gap: 0.75rem;
304
304
  }
305
305
  .back-in-stock-eyebrow {
306
306
  font-family: var(--font-display);
@@ -326,11 +326,11 @@ body[data-navigating="true"] .product-grid {
326
326
  display: flex;
327
327
  flex-direction: row;
328
328
  flex-wrap: wrap;
329
- gap: var(--spacing-sm);
329
+ gap: 0.75rem;
330
330
  }
331
331
  .back-in-stock-input {
332
332
  flex: 1;
333
- padding: var(--spacing-sm) var(--spacing-md);
333
+ padding: 0.75rem 1rem;
334
334
  font-family: var(--font-body);
335
335
  font-size: 0.875rem;
336
336
  border: 1px solid var(--color-rule);
@@ -345,7 +345,7 @@ body[data-navigating="true"] .product-grid {
345
345
  border-color: var(--color-ink);
346
346
  }
347
347
  .back-in-stock-submit {
348
- padding: var(--spacing-sm) var(--spacing-lg);
348
+ padding: 0.75rem 1.5rem;
349
349
  font-family: var(--font-display);
350
350
  font-size: 0.75rem;
351
351
  text-transform: uppercase;
@@ -370,8 +370,8 @@ body[data-navigating="true"] .product-grid {
370
370
  color: var(--color-error, var(--color-accent));
371
371
  }
372
372
  .back-in-stock-success {
373
- margin-top: var(--spacing-md);
374
- padding: var(--spacing-lg);
373
+ margin-top: 1rem;
374
+ padding: 1.5rem;
375
375
  border: 1px solid var(--color-rule);
376
376
  border-radius: var(--radius-md);
377
377
  background: var(--color-paper, var(--color-cream));
@@ -390,7 +390,7 @@ body[data-navigating="true"] .product-grid {
390
390
  margin: 0;
391
391
  }
392
392
  .stock-status.back-in-stock-alt {
393
- margin-top: var(--spacing-sm);
393
+ margin-top: 0.75rem;
394
394
  font-family: var(--font-body);
395
395
  font-size: 0.875rem;
396
396
  font-style: italic;
@@ -89,14 +89,14 @@ const viewContentPayload = {
89
89
  hasSeoTags={hasSeoTags}
90
90
  >
91
91
  {hasSeoTags && <SeoTags slot="head" tags={seoTags} />}
92
- <main class="py-3xl">
93
- <div class="max-w-wide mx-auto px-xl">
92
+ <main class="py-16">
93
+ <div class="max-w-wide mx-auto px-8">
94
94
  <a href="/products" class="text-mute text-sm tracking-wide uppercase">&larr; Back to products</a>
95
- <div class="grid grid-cols-1 md:grid-cols-[7fr_5fr] gap-2xl md:gap-4xl mt-xl items-start">
95
+ <div class="grid grid-cols-1 md:grid-cols-[7fr_5fr] gap-12 md:gap-24 mt-8 items-start">
96
96
  <div class="aspect-[4/5] bg-cream rounded-sm overflow-hidden">
97
97
  {imageUrl && <img src={imageUrl} alt={product.name ?? "Product"} class="w-full h-full object-cover" />}
98
98
  </div>
99
- <div class="flex flex-col gap-lg md:sticky md:top-2xl">
99
+ <div class="flex flex-col gap-6 md:sticky md:top-12">
100
100
  <h1 class="font-display text-4xl leading-tight">{product.name}</h1>
101
101
  {price && <p class="text-xl text-ink-soft">{price}</p>}
102
102
  {description && <p class="text-ink-soft leading-relaxed">{description}</p>}
@@ -13,27 +13,32 @@ framework, translate the examples** — the SDK calls and payloads are identical
13
13
  only the UI idiom (`useState`/`useEffect` → a store / `onMounted` / signals) and
14
14
  the client acquisition differ (see "Client identity" below).
15
15
 
16
- The flow is **appointment booking**. CLASS sign-up reuses the same sequence with
17
- an event-slot branch (noted inline) and is a fast-follow, not v1. **Location and
18
- staff selection are likewise a fast-follow (a separate PR); add-ons are out of
19
- scope.** v1 threads a single service selection — the shared state below holds no
20
- location / staff / add-on choice yet.
16
+ The flow covers **appointments and classes**. APPOINTMENT and CLASS share the
17
+ same booking sequence; they differ only in the availability call (`availabilityTimeSlots`
18
+ vs `eventTimeSlots`, noted inline). **Courses and add-ons are out of scope.**
21
19
 
22
20
  ---
23
21
 
24
22
  ## 1. The step model
25
23
 
26
- A single customer journey, five steps, holding one shared selection:
24
+ A single customer journey, holding one shared selection. The catalog optionally
25
+ filters by **location** and **category**; the slots step optionally filters by
26
+ **staff**. All three auto-skip when there's nothing to choose (≤1 location, ≤1
27
+ category, ≤1 staff), so a plain single-location single-staff site sees the simple
28
+ five-step path:
27
29
 
28
30
  ```
29
- 1. Catalog list bookable services → pick a service
30
- 2. Slots week calendar of availability for that service → pick a time slot
31
- 3. Details the service's booking form (collect contact) → submit
31
+ 0. Location (optional) pick a location — only shown when the site has >1 locations → scopes the catalog + availability
32
+ 1. Catalog list bookable services (optionally filtered by location/category) → pick a service
33
+ 2. Slots availability calendar (optionally filtered by staff) → pick a time slot
34
+ 3. Details the service's booking form (collect contact) → submit
32
35
  4. Book run the SDK sequence (§3)
33
36
  5a. paid → redirect to the Wix-hosted checkout → returns to the confirmation page
34
37
  5b. free/offline → place the order → confirmation page
35
38
  ```
36
39
 
40
+ The location/category filters and the staff picker are covered in §§7–8.
41
+
37
42
  **The catalog is the entry point but optional** — a visitor can land directly on a
38
43
  service page and start at the Slots step. Every step rehydrates from the URL (§2),
39
44
  so deep links work.
@@ -49,8 +54,8 @@ for SEO and run slots + form + book in a `client:only` island.
49
54
  Steps 1–4 build up one selection (this is exactly `BookParams` + `SelectedSlot`
50
55
  in `../astro/templates/bookings/bookingDriver.ts`):
51
56
 
52
- - **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`).
53
- - **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType? }`.
57
+ - **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`; the slots step reads `service.staffMemberDetails.staffMembers` for the staff picker — §8).
58
+ - **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType?, resource? }`. `resource = { _id, name }` is set only when a specific staff was chosen (§8); otherwise the driver emits the ANY_RESOURCE fallback. `locationId`/`locationType` come from the slot's own `location` (the booking books at that location).
54
59
  - **`formSubmission`** — the booking-form values, **keyed by each field's `target`** (§4).
55
60
 
56
61
  Hold it however the framework prefers (React state lifted to a coordinator, a
@@ -89,11 +94,12 @@ Key facts the driver encodes (do not deviate):
89
94
  cart holds the seat** instead — `placeOrder` (free/offline) or the hosted checkout
90
95
  (paid) drives confirmation — so a client-only site completes the whole flow with no
91
96
  server elevation.
92
- - **ANY_RESOURCE fallback (staff)** — v1 has **no staff picker**, so `createBooking`
93
- always sends `resourceSelections:[{ resourceTypeId:"1cd44cf8-…", selectionMethod:"ANY_RESOURCE" }]`
97
+ - **ANY_RESOURCE fallback (staff)** — when no staff is chosen (the default, and on
98
+ single-staff services), `createBooking` sends
99
+ `resourceSelections:[{ resourceTypeId:"1cd44cf8-756f-41c3-bd90-3e2ffcaf1155", selectionMethod:"ANY_RESOURCE" }]`
94
100
  and Wix auto-assigns a bookable staff resource (appointment slots return
95
- `availableResources:[]` yet book fine this way). (Explicit staff selection is a
96
- fast-follow.)
101
+ `availableResources:[]` yet book fine this way). A specific staff choice sets
102
+ `slot.resource` instead (§8).
97
103
  - **Checkout decision** — `isCheckoutRequired`: if the service's booking policy charges
98
104
  a **cancellation fee** (`service.bookingPolicy.cancellationFeePolicy.enabled`) →
99
105
  checkout (a card must be on file); else total 0 → place; else `FULL_PAYMENT_OFFLINE`
@@ -106,9 +112,11 @@ Key facts the driver encodes (do not deviate):
106
112
  Mirror what the Bookings SDK does (`services.queryServices` is the raw SDK, not a component):
107
113
 
108
114
  ```js
109
- // Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find():
115
+ // Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find().
116
+ // Merge the optional category/location filters into the SAME filter object (§7):
110
117
  services.queryServices({
111
- query: { filter: { appId: BOOKING_APP_ID }, paging: { limit: 100 } },
118
+ query: { filter: { appId: BOOKING_APP_ID /* , "category.id": { $eq: catId },
119
+ "locations.business.id": { $hasSome: [locId] } */ }, paging: { limit: 100 } },
112
120
  conditionalFields: ["STAFF_MEMBER_DETAILS"],
113
121
  }).find(); // → result.items ; filter out s.hidden
114
122
 
@@ -116,13 +124,21 @@ services.queryServices({
116
124
  services.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
117
125
  .eq("mainSlug.name", slug).eq("appId", BOOKING_APP_ID).limit(1).find();
118
126
 
119
- // Availability (APPOINTMENT) — serviceId is a single GUID STRING:
127
+ // Availability (APPOINTMENT) — serviceId is a single GUID STRING. The staff (§8)
128
+ // and location filters are OPTIONAL params on the same call:
120
129
  availabilityTimeSlots.listAvailabilityTimeSlots({
121
130
  serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
131
+ // staff: resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [staffResourceId] }],
132
+ // includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
133
+ // location: locations: [{ _id: locationId, locationType: "BUSINESS" }],
122
134
  }); // slots: result.timeSlots[] (localStartDate/localEndDate/scheduleId at top level)
123
135
 
124
- // Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId:
125
- eventTimeSlots.listEventTimeSlots({ serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false });
136
+ // Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId.
137
+ // CLASS filters staff + location via eventFilter instead:
138
+ eventTimeSlots.listEventTimeSlots({
139
+ serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false,
140
+ // eventFilter: { "resources.id": { $hasSome: [staffResourceId] }, "location.id": [locationId] },
141
+ });
126
142
  ```
127
143
 
128
144
  `fromLocalDate`/`toLocalDate` are **local** date strings `YYYY-MM-DDThh:mm:ss`
@@ -157,14 +173,16 @@ The booking form is a **`@wix/forms` form** attached to the service
157
173
  `target`. The default booking form's targets are snake_case `first_name` /
158
174
  `last_name` / `email` / `phone`. Reference example: `../astro/templates/bookings/BookingForm.tsx`.
159
175
 
160
- ## 5. The day-calendar (slots step)
176
+ ## 5. The availability calendar (slots step)
161
177
 
162
- Show a **week calendar**, not a flat list of every slot: a 7-day strip with
163
- week navigation → the picked day's times. A flat grid with time-only labels
164
- leaves the visitor unable to tell which day a slot is on. Fetch availability for
165
- the visible window (`fromLocalDate`/`toLocalDate` = the week bounds), group slots
166
- by calendar day, and offer a **"check next availability"** action that probes
167
- forward when a week is empty. Reference example:
178
+ **Group slots by day — don't render a flat list of every slot with time-only
179
+ labels** (the visitor can't tell which day a slot is on; that flat grid was the
180
+ original usability failure). The calendar **shape is your / the brand's choice** —
181
+ week, month, or N-day. The reference example uses a **week view** (a 7-day strip
182
+ with week navigation → the picked day's times); fetch availability for the visible
183
+ window (`fromLocalDate`/`toLocalDate` = that window's bounds), group by calendar
184
+ day, and offer a **"check next availability"** action that probes forward when the
185
+ window is empty. Reference example:
168
186
  `../astro/templates/bookings/AvailabilityCalendar.tsx`.
169
187
 
170
188
  ## 6. Client identity (per framework)
@@ -178,8 +196,51 @@ The SDK calls are identical everywhere; only how you get the client differs:
178
196
  `createClient({ modules, auth: OAuthStrategy({ clientId: appId }) })` and call
179
197
  the same functions on it. CDN imports for `none`, bundled for `own`.
180
198
 
181
- ## 7. Out of v1
182
-
183
- Waitlist, on-site manage/cancel, location selector, staff filter, payment
184
- breakdown, and CLASS sign-up are out of v1. Show bookable slots only;
185
- post-booking self-service is handled by the Wix-hosted flow / member area.
199
+ ## 7. Catalog filters — location & category (both auto-skip)
200
+
201
+ Both filter the **catalog query** and re-render the list. The cleanest
202
+ framework-agnostic shape is a re-query driven by a query param (`?locationId`,
203
+ `?category`) — links/SSR on a static catalog, or a store/router on a SPA. **Show
204
+ each filter only when there is more than one choice**, exactly as the SoT does.
205
+
206
+ **Location** (the SoT is location-first; here it auto-skips ≤1 location):
207
+ 1. `services.queryLocations()` → `{ businessLocations: { locations: [{ _id, type, business: { name } }] }, customLocations: { exists }, customerLocations: { exists } }`.
208
+ 2. Count = `businessLocations.locations.length + (customLocations.exists ? 1 : 0) + (customerLocations.exists ? 1 : 0)`. **Show the selector only when count > 1.**
209
+ 3. The chosen location filters the catalog query:
210
+ - a real business id → `filter["locations.business.id"] = { $hasSome: [id] }`;
211
+ - the synthetic `"custom"` / `"customer"` → `filter["locations.type"] = { $hasSome: ["CUSTOM"|"CUSTOMER"] }` (the SoT's synthetic-id mapping).
212
+ 4. **Scope availability to exactly ONE location — always, on a multi-location service.** `listAvailabilityTimeSlots` returns **one slot per location** per time ("if `locations` is not specified, returns time slots for all locations where the service is available"), so an unscoped call on a 2-location service shows every time **twice**. Pass a single-element filter: APPOINTMENT → `locations: [{ _id, locationType: "BUSINESS" }]`; CLASS → `eventFilter: { "location.id": [id] }`. (Staff does **not** multiply rows — one slot carries many `availableResources`; only location does.) The slots step builds its location list from **`queryLocations()`** (the site's real business locations, whose ids the availability engine recognizes) **intersected with the service's own location ids** — not from `service.locations` alone, whose entries can carry an id the availability engine doesn't recognize (scoping to it returns zero slots). A **location picker** defaults to the catalog-carried location (or the first) and scopes the call when there's a real location to scope to; when the site has no business locations the list is empty and the call stays unscoped (one location → no duplicates). The booked slot carries its own `location`, so the booking books at that location (the driver maps it).
213
+
214
+ **Category:**
215
+ 1. `categoriesV2.queryCategories().find()` → `result.items: [{ _id, name }]`. **Non-fatal** — if it fails, render the catalog without the bar.
216
+ 2. **Show the bar only when `items.length > 1`** (a fresh site has one default category).
217
+ 3. The chosen category filters the catalog query: `filter["category.id"] = { $eq: categoryId }`. A service carries its category at `service.category._id`.
218
+
219
+ Reference example: `../astro/templates/bookings/services/index.astro` (both filters, link-driven, auto-skipping).
220
+
221
+ ## 8. Staff / resource selection (auto-skips ≤1 staff)
222
+
223
+ A service carries its staff on the service object via the **`STAFF_MEMBER_DETAILS`
224
+ conditional field** (already requested in the catalog/detail query):
225
+ `service.staffMemberDetails.staffMembers: [{ staffMemberId, name }]`. The id used
226
+ for filtering and booking is **`staffMemberId`** — which, **despite the name, IS the
227
+ resource GUID** (it matches the service's `staffMemberIds` and the Staff Members API
228
+ `resourceId`). Below, `resourceId` is that value.
229
+
230
+ 1. Show a "Any staff member / pick one" control above the calendar **only when
231
+ there is more than one staff**.
232
+ 2. Selecting a staff re-fetches availability filtered to that resource:
233
+ - APPOINTMENT → `resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [resourceId] }]` (plus `includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID]`);
234
+ - CLASS → `eventFilter: { "resources.id": { $hasSome: [resourceId] } }`.
235
+ 3. When a slot is picked **with a specific staff selected**, set `slot.resource = { _id: resourceId, name }`. The driver books that resource. With **"any staff"**, leave `resource` unset — the driver emits the **ANY_RESOURCE fallback** and Wix auto-assigns a bookable resource (appointment slots can return `availableResources: []` yet book fine this way).
236
+
237
+ `STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155"` (exported from `bookingDriver.ts`). Reference example: `../astro/templates/bookings/AvailabilityCalendar.tsx` (the staff `<select>` + the filtered fetch).
238
+
239
+ ## 9. Out of scope
240
+
241
+ Waitlist, on-site manage/cancel, payment/deposit breakdown, and multi-service /
242
+ day-range are out of scope. (Waitlist and manage/cancel have **no headless SoT** —
243
+ neither the components nor the vibe plugin implement join/cancel logic, only
244
+ display-only policy flags — so they are deliberately not built here.) Show
245
+ bookable slots only; post-booking self-service is handled by the Wix-hosted flow /
246
+ member area.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bookings-implementer
3
- description: "Implements the Wix Bookings vertical — services catalog, a service detail page with a week-calendar availability picker, a schema-driven booking form, and confirmation. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
3
+ description: "Implements the Wix Bookings vertical — services catalog (with optional location + category filters), a service detail page with a week-calendar availability picker (with optional staff selection), a schema-driven booking form, and confirmation, for appointments and classes. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
4
4
  ---
5
5
 
6
6
  # Bookings Implementer
@@ -82,9 +82,14 @@ Bookings-specific CSS lives in `src/styles/components-bookings.css` (pre-copied
82
82
  | Pass UTC ISO strings to `listAvailabilityTimeSlots` | Use **local** date strings `YYYY-MM-DDThh:mm:ss` (no `Z`) + a separate `timeZone`. |
83
83
  | `availability.queryAvailability` / `import { availability }` | APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: <string> })`; CLASS → `eventTimeSlots.listEventTimeSlots({ serviceIds: [<string>] })`. The `availability` namespace does not exist. |
84
84
  | Read slot time from `timeSlot.slot.startDate` | Slot fields are at the **top level**: `localStartDate`, `localEndDate`, `scheduleId` (APPOINTMENT). CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`. |
85
- | Dump every slot in one flat, time-only grid | Show a **week calendar**: a day strip → the picked day's times. A flat grid hides which day a slot is on. |
85
+ | Dump every slot in one flat, time-only grid | **Group slots by day** — a flat time-only grid hides which day a slot is on. The calendar shape (week / month / N-day) is your choice; the example uses a week strip → the picked day's times. |
86
+ | Read the staff id from `staffMember._id` / `.resourceId` on `staffMemberDetails.staffMembers` | The field is **`staffMemberId`** (on `service.staffMemberDetails.staffMembers[]`) — despite the name it IS the resource GUID (matches the service's `staffMemberIds`). Filter availability: APPOINTMENT `resourceTypes:[{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds:[staffMemberId] }]`; CLASS `eventFilter:{ "resources.id": { $hasSome:[staffMemberId] } }`. Record the choice as `slot.resource = { _id: staffMemberId, name }`; leave it unset for "any staff" (ANY_RESOURCE fallback). Show the picker only when >1 staff. |
87
+ | Always show the location selector / staff picker / category bar | **Auto-skip** each when there's nothing to choose: location count ≤1 (`businessLocations.length + custom?.exists + customer?.exists`), staff ≤1, categories ≤1. Single-location single-staff sites see the simple flow. |
88
+ | Filter the catalog by a synthetic location id, or hardcode the location filter | Real business id → `filter["locations.business.id"] = { $hasSome:[id] }`; synthetic `"custom"`/`"customer"` → `filter["locations.type"] = { $hasSome:["CUSTOM"|"CUSTOMER"] }`. Carry the choice to the detail page so availability is scoped too. |
89
+ | Call `listAvailabilityTimeSlots` **without** a `locations` filter on a multi-location service | It returns **one slot per location** per time → every time appears **N×** (duplicated rows). **Always scope availability to one business location** (`locations: [{ _id, locationType:"BUSINESS" }]`; CLASS: `eventFilter:{ "location.id":[id] }`). Build the calendar's location list from **`queryLocations()`** (the site's real business locations) intersected with the service's location ids — NOT from `service.locations` alone (its entries can carry an id the availability engine doesn't recognize → scoping to it returns 0 slots even though the service has availability). Default to the carried/first, picker when >1; no business locations → unscoped. Staff does **not** cause duplicates — one slot carries many `availableResources`; only location multiplies rows. |
90
+ | Let a `queryCategories` failure break the catalog | Categories are **non-fatal** — wrap in try/catch and render the catalog without the bar. Category filter: `filter["category.id"] = { $eq: categoryId }`; a service's category is `service.category._id`. |
86
91
  | Read service slug from `service.slug` / name from `service.info.name` | V2 is flat: slug `service.mainSlug.name` (fallback `supportedSlugs[0].name`), `service.name`, `service.description`, `service.tagLine`; duration `service.schedule.availabilityConstraints.sessionDurations[0]`; price `service.payment.fixed.price.value` (string); media `service.media.mainMedia.image.url`. |
87
92
  | Use `service.id` / `result.booking.id` | Entity ids are `_id` (underscore): `service._id`, `booking._id`. |
88
93
  | Mount the calendar/form without `client:only="react"` | Availability + booking are timezone/session-specific — always `client:only="react"`. SSR only the read pages (catalog/detail) for SEO. |
89
94
  | Omit try/catch on the booking | `createBooking` can reject (e.g. a slot taken between fetch and submit, or strict phone validation). Catch and surface a friendly message; don't crash. |
90
- | Ship a location selector, staff filter, payment breakdown, CLASS sign-up, or waitlist | Out of v1 — appointment booking only. |
95
+ | Ship a payment/deposit breakdown, multi-service stacking, waitlist, or on-site manage/cancel | Out of scope. **Waitlist and manage/cancel have no headless SoT** (the components + vibe plugin expose only display-only policy flags — no join/cancel logic) — do not invent them on the REST/anonymous-token layer. Post-booking self-service is handled by the Wix-hosted flow / member area. |
@@ -93,6 +93,66 @@ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H
93
93
 
94
94
  ---
95
95
 
96
+ ## Step 3b — Create categories (OPTIONAL — only to surface the catalog category filter)
97
+
98
+ A fresh Bookings install has **one** default category ("Our Services"), so the
99
+ front-end's category filter bar stays hidden (it shows only when >1 category
100
+ exists). Create extra categories **only when the build wants a category filter**
101
+ (e.g. `intent.bookings.categories` is set, or several distinct service groups).
102
+ Skip this step otherwise — it's not needed for a basic seed.
103
+
104
+ ```bash
105
+ # Create a category. Returns { category: { id, ... } } — save the id.
106
+ curl -sS -X POST \
107
+ -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
108
+ -d '{ "category": { "name": "<Category name, e.g. Massage>" } }' \
109
+ "https://www.wixapis.com/bookings/v2/categories"
110
+ ```
111
+
112
+ Then **assign each service to a category** by including `category` in the Step-4
113
+ create payload: `"category": { "id": "<categoryId>" }`. (If a create rejects
114
+ `category`, assign it after creation via Update Service — `PATCH
115
+ /_api/bookings/v2/services/{id}` with `service.category.id` + the current
116
+ `revision` — per the docs' "create a category and connect a service" flow.) A
117
+ service can belong to **one** category. Distribute services across 2+ categories
118
+ so the filter is meaningful.
119
+
120
+ > The front-end reads categories via `categoriesV2.queryCategories().find()` and
121
+ > filters services with `filter["category.id"] = { $eq: id }`. No category data →
122
+ > no filter bar (graceful).
123
+
124
+ ---
125
+
126
+ ## Step 3c — Multiple business locations (OPTIONAL — only for a multi-location demo)
127
+
128
+ The front-end's location selector auto-skips when the site has ≤1 location, so a
129
+ single-location seed needs nothing here (and the booking flow is unaffected).
130
+ Create a second business location **only** for a multi-location build
131
+ (`intent.bookings.multiLocation`):
132
+
133
+ ```bash
134
+ # Create a business location. Returns { location: { id, ... } }.
135
+ curl -sS -X POST \
136
+ -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
137
+ -d '{ "location": { "name": "<Branch name>", "status": "ACTIVE", "locationType": "BUSINESS",
138
+ "address": { "city": "<City>", "country": "<ISO country>" } } }' \
139
+ "https://www.wixapis.com/locations/v1/locations"
140
+ ```
141
+
142
+ Then attach locations to each service's `locations` array on create (Step 4):
143
+ `"locations": [ { "type": "BUSINESS", "business": { "id": "<locationId>" } }, … ]`.
144
+
145
+ > **Caveat (verified-behavior limitation, not a bug):** filtering *availability*
146
+ > by location also depends on each staff member's **working-hours location
147
+ > coverage** (the `STAFF_WORKING_HOURS_LOCATIONS` setting). Seeding that reliably
148
+ > is as fragile as custom working hours (see Step 3's warning), so for a seed,
149
+ > create the location + attach it to services to exercise the **catalog** location
150
+ > filter, and configure per-location staff hours from the Bookings dashboard if you
151
+ > need per-location availability to differ. The selector + service filtering work
152
+ > from the seed alone; per-location *slot* differences need the dashboard step.
153
+
154
+ ---
155
+
96
156
  ## Step 4 — Create services (Services V2 API)
97
157
 
98
158
  **Endpoint:** `POST https://www.wixapis.com/_api/bookings/v2/services`