@wix/agent-skills 1.8.0 → 1.9.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 (37) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
  3. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
  4. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
  5. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
  6. package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
  7. package/skills/wix-design-system/SKILL.md +1 -1
  8. package/skills/wix-headless/references/BUILD-astro.md +5 -7
  9. package/skills/wix-headless/references/SETUP.md +1 -1
  10. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  11. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
  12. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
  13. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
  14. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  15. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
  16. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
  17. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  18. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
  19. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
  20. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
  21. package/skills/wix-headless/references/bookings/FLOW.md +185 -0
  22. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
  23. package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
  24. package/skills/wix-headless/references/verticals/bookings.md +6 -8
  25. package/skills/wix-manage/SKILL.md +12 -0
  26. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  27. package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
  28. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  29. package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
  30. package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
  31. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  32. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  33. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  34. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  35. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  36. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  37. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -1,337 +1,45 @@
1
- # Bookings Components — Phase 3
1
+ # Bookings Components — the client islands (astro)
2
2
 
3
- You are the Phase 3 Components agent for the bookings vertical. Your scope: write the React islands and Astro shells that power the bookings UI. The orchestrator has already copied `src/styles/components-bookings.css` from the skill template — do NOT write it.
4
-
5
- > **Start from the canonical templates — don't invent the SDK wiring.** Every component in this scope has a verified template at `<SKILL_ROOT>/references/astro/templates/bookings/` (`ServiceCard.astro`, `AvailabilityCalendar.tsx`, `BookingForm.tsx`, `ServiceBookingFlow.tsx`, `ManageBooking.tsx`, `SeoTags.astro`). **Read and adapt them** (brand copy, styling) — this doc explains the *why* and the gotchas behind that wiring. The booking/confirm/availability/waitlist flow is subtle and live-verified; re-authoring it from scratch is a common source of API-shape bugs.
3
+ The `components` scope of the bookings vertical. You write the React islands that
4
+ power the booking UI. The **logic** is in `../../bookings/FLOW.md` (read it first);
5
+ this doc is the astro wiring + the gotchas. `src/styles/components-bookings.css`
6
+ and `src/components/bookingDriver.ts` are pre-copied — do **not** write them.
6
7
 
7
8
  Read `references/shared/IMPLEMENTER.md` + `references/shared/STYLING.md` first.
8
9
 
9
- ---
10
-
11
- ## Files you own
12
-
13
- | File | Purpose |
14
- |------|---------|
15
- | `src/components/ServiceCard.astro` | Static card for the services listing grid |
16
- | `src/components/AvailabilityCalendar.tsx` | React island — date picker + slot grid |
17
- | `src/components/BookingForm.tsx` | React island — booking details form + submission |
18
-
19
- Do NOT write any `.css` files — `components-bookings.css` is pre-copied and must not be modified.
20
-
21
- ---
22
-
23
- ## ServiceCard.astro
24
-
25
- A static Astro component. No client-side interactivity — just props in, HTML out.
26
-
27
- **Props:**
28
- ```typescript
29
- interface Props {
30
- id: string;
31
- slug: string;
32
- name: string;
33
- tagLine?: string;
34
- durationMinutes: number;
35
- price?: string; // formatted string, e.g. "$75.00" — already formatted by caller
36
- currency?: string;
37
- imageUrl?: string;
38
- type: 'APPOINTMENT' | 'CLASS';
39
- }
40
- ```
41
-
42
- **Structure:**
43
- - Outer `<a href={`/services/${slug}`}>` wrapping the entire card (`class="service-card"`)
44
- - Image region: if `imageUrl` is present, `<img src={imageUrl} alt={name} class="service-card-image">`. Else, a `<div class="service-card-image service-card-image--placeholder" aria-hidden="true">` using `background-color: var(--color-surface-secondary)` inline.
45
- - Content region (`class="service-card-meta"`):
46
- - `<h3 class="service-card-name">{name}</h3>`
47
- - `{tagLine && <p class="service-card-tagline">{tagLine}</p>}`
48
- - Meta row: duration badge + price (when present) in a `<div class="service-card-price">`:
49
- - Duration: `<span>{durationMinutes} min</span>`
50
- - Price: `<span>{price}</span>` when price is defined
51
- - No price text when price is absent (service may be free or "Contact for pricing")
52
-
53
- ---
54
-
55
- ## AvailabilityCalendar.tsx
56
-
57
- A `client:only="react"` React island. Fetches and displays available slots for a specific service.
58
-
59
- **Props:**
60
- ```typescript
61
- interface AvailabilityCalendarProps {
62
- serviceId: string;
63
- serviceName: string;
64
- serviceType: 'APPOINTMENT' | 'CLASS'; // drives which time-slots API + slot shape
65
- onSlotSelected: (slot: SelectedSlot) => void;
66
- }
67
-
68
- // APPOINTMENT and CLASS slots have DIFFERENT shapes — see "SDK wiring" below.
69
- // APPOINTMENT slots carry `scheduleId` + `startDate`; CLASS (event) slots carry
70
- // `eventInfo.eventId` and have NO `scheduleId`. Carry both optionally + the type.
71
- interface SelectedSlot {
72
- serviceType: 'APPOINTMENT' | 'CLASS';
73
- localStartDate: string; // YYYY-MM-DDThh:mm:ss — from slot.localStartDate
74
- localEndDate: string; // YYYY-MM-DDThh:mm:ss — from slot.localEndDate
75
- serviceId: string;
76
- timezone: string; // result.timeZone from the list response
77
- // APPOINTMENT only:
78
- scheduleId?: string; // timeSlot.scheduleId — undefined for CLASS
79
- locationId?: string; // timeSlot.location.id (BUSINESS locations only)
80
- locationType?: string; // timeSlot.location.locationType
81
- // CLASS only:
82
- eventId?: string; // timeSlot.eventInfo.eventId — undefined for APPOINTMENT
83
- }
84
- ```
85
-
86
- **Behavior:**
87
- 1. On mount, default to today's date. Fetch available slots for today via `fetchSlots(serviceId, serviceType, today)` (which picks `listAvailabilityTimeSlots` for APPOINTMENT or `listEventTimeSlots` for CLASS — see SDK wiring below).
88
- 2. Render a simple date-navigation header (← Prev / date display / Next →) with `<button>` controls. Move by ±1 day. No full calendar grid — a date navigator is sufficient.
89
- 3. Below the date navigator, render the available slots as a grid of `<button>` elements (`className="time-slot time-slot--available"`). On click, mark the slot as selected (`className="time-slot time-slot--selected"`) and call `onSlotSelected(slot)`. **Guard the click on the type-appropriate id** — `slot.localStartDate && (slot.serviceType === 'CLASS' ? slot.eventId : slot.scheduleId)`. Guarding on `scheduleId` alone silently drops every CLASS click (event slots have no `scheduleId`), so the booking form never appears.
90
- 4. If no slots are available for the selected date, display: `<p className="availability-empty">No availability on this date — try another day.</p>`.
91
- 5. Loading state: display `<p className="availability-loading">Checking availability…</p>` while fetching.
92
- 6. Error state: display `<p className="availability-error">Could not load availability — please try again.</p>` and a "Retry" button.
93
-
94
- > **`.tsx` files use `className=`, NOT `class=`.** React JSX requires the `className` attribute on intrinsic elements; using `class=` produces a `tsc` error per occurrence (`Type '{ class: string; ...}' is not assignable to type 'DetailedHTMLProps<...>'`). Only `.astro` files (like `ServiceCard.astro` above) use `class=`. The same rule applies to `BookingForm.tsx` and `ServiceBookingFlow.tsx`.
95
-
96
- **SDK wiring:**
97
-
98
- > **Two different namespaces, one per service type** — both are real `@wix/bookings` exports (`availability` is not):
99
- > - **APPOINTMENT** → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: [id], … bookable: true })`. Slots carry `scheduleId` + `startDate` at the top level.
100
- > - **CLASS** → `eventTimeSlots.listEventTimeSlots({ serviceIds: [id], … includeNonBookable: false })`. Note the **different namespace** (`eventTimeSlots`, not `availabilityTimeSlots`), the **plural `serviceIds`**, and `includeNonBookable` (not `bookable`). Class slots carry their session id at **`eventInfo.eventId`** and have **no `scheduleId`** (and no top-level `startDate`).
101
- >
102
- > Branch on the `serviceType` prop. For CLASS the calendar is empty until the merchant schedules sessions for the class (see SERVICES_DATA.md § "CLASS session gap") — an empty state there is expected, not a bug.
103
-
104
- > **Client ID in browser islands comes from `astro:env/client`, NOT `import.meta.env`.** In `@wix/astro` the client ID is exposed to the browser through the `astro:env/client` virtual module: `import { WIX_CLIENT_ID } from "astro:env/client"`. `import.meta.env.WIX_CLIENT_ID` (and `PUBLIC_WIX_CLIENT_ID`) is **`undefined` in the browser bundle**, so `OAuthStrategy` POSTs an empty `client_id` and every client-side SDK call fails at `POST https://www.wixapis.com/oauth2/token` with `400 {"error":"invalid_request"}`. (The var is `WIX_CLIENT_ID` — no `PUBLIC_` prefix.)
105
-
106
- ```typescript
107
- import { createClient, OAuthStrategy } from '@wix/sdk';
108
- import { availabilityTimeSlots, eventTimeSlots, bookings } from '@wix/bookings';
109
- import { WIX_CLIENT_ID } from 'astro:env/client';
110
-
111
- const wixClient = createClient({
112
- modules: { availabilityTimeSlots, eventTimeSlots, bookings },
113
- auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
114
- });
115
-
116
- // Local date strings (YYYY-MM-DDThh:mm:ss) — NOT ISO UTC strings with Z. The API
117
- // expects local time in the business timezone, alongside an explicit timeZone.
118
- const pad = (n: number) => String(n).padStart(2, '0');
119
- const localDate = (d: Date) =>
120
- `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
121
-
122
- // Returns SelectedSlot[] normalized across both service types so the rest of the
123
- // component (and BookingForm) never re-branches on raw API shapes.
124
- const fetchSlots = async (
125
- serviceId: string,
126
- serviceType: 'APPOINTMENT' | 'CLASS',
127
- date: Date,
128
- ): Promise<{ slots: SelectedSlot[]; timezone: string }> => {
129
- const start = new Date(date); start.setHours(0, 0, 0, 0);
130
- const end = new Date(date); end.setHours(23, 59, 59, 0);
131
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
132
-
133
- if (serviceType === 'CLASS') {
134
- const result = await wixClient.eventTimeSlots.listEventTimeSlots({
135
- serviceIds: [serviceId], // plural — CLASS API
136
- fromLocalDate: localDate(start),
137
- toLocalDate: localDate(end),
138
- timeZone,
139
- includeNonBookable: false,
140
- });
141
- const tz = result.timeZone ?? timeZone;
142
- const slots: SelectedSlot[] = (result.timeSlots ?? []).map((ts: any) => ({
143
- serviceType: 'CLASS',
144
- serviceId,
145
- localStartDate: ts.localStartDate,
146
- localEndDate: ts.localEndDate,
147
- eventId: ts.eventInfo?.eventId, // session id lives here — there is NO scheduleId
148
- timezone: tz,
149
- }));
150
- return { slots, timezone: tz };
151
- }
152
-
153
- // APPOINTMENT
154
- const result = await wixClient.availabilityTimeSlots.listAvailabilityTimeSlots({
155
- serviceId, // a single GUID STRING — NOT an array (the array form is the CLASS `serviceIds`)
156
- fromLocalDate: localDate(start),
157
- toLocalDate: localDate(end),
158
- timeZone,
159
- bookable: true,
160
- cursorPaging: { limit: 50 },
161
- });
162
- const tz = result.timeZone ?? timeZone;
163
- const slots: SelectedSlot[] = (result.timeSlots ?? []).map((ts: any) => ({
164
- serviceType: 'APPOINTMENT',
165
- serviceId,
166
- localStartDate: ts.localStartDate,
167
- localEndDate: ts.localEndDate,
168
- scheduleId: ts.scheduleId,
169
- locationId: ts.location?.id,
170
- locationType: ts.location?.locationType,
171
- timezone: tz,
172
- }));
173
- return { slots, timezone: tz };
174
- };
175
- ```
176
-
177
- Slot fields differ by type — both have `localStartDate`/`localEndDate` at the TOP LEVEL (not nested under a `slot` property):
178
- - **APPOINTMENT** (`listAvailabilityTimeSlots`): `timeSlot.scheduleId`, `timeSlot.serviceId`, `timeSlot.location.{id,locationType}`, `timeSlot.bookable`. `timeSlot.availableResources` is **EMPTY by default** — do NOT read a resource ID here.
179
- - **CLASS** (`listEventTimeSlots`): `timeSlot.eventInfo.eventId` (the session id), `timeSlot.totalCapacity`/`remainingCapacity`. **No `scheduleId`, no top-level `startDate`.**
180
-
181
- > **Do NOT try to read `resource._id` from list results.** Omit `resource` from the `createBooking` payload — Wix auto-assigns an available resource during booking confirmation. You only need resource details if you want to show "book with specific staff" UI (advanced use case).
182
-
183
- Format slot times for display: `new Date(timeSlot.localStartDate).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })`.
184
-
185
- `SelectedSlot` (defined once at the top of this component) carries the info needed for booking — including `serviceType` and, for CLASS, `eventId` instead of `scheduleId`. `BookingForm` reads `serviceType` to choose the `createBooking` payload shape.
186
-
187
- ---
188
-
189
- ## BookingForm.tsx
190
-
191
- A `client:only="react"` React island. Renders after the user selects a slot.
192
-
193
- **Props:**
194
- ```typescript
195
- interface BookingFormProps {
196
- serviceId: string;
197
- serviceName: string;
198
- serviceType: 'APPOINTMENT' | 'CLASS'; // selects the createBooking slot shape
199
- slot: SelectedSlot;
200
- onSuccess: (bookingId: string, startDate: string) => void; // startDate for confirmation page URL
201
- onCancel: () => void;
202
- }
203
- ```
204
-
205
- **Fields:**
206
- - First name (required)
207
- - Last name (optional)
208
- - Email (required)
209
- - Phone (optional)
210
- - Submit button: "Confirm Booking"
211
- - Cancel link that calls `onCancel()`
212
-
213
- **On submit:**
214
- 1. Validate email + first name are non-empty.
215
- 2. Call `bookings.createBooking(...)`:
216
-
217
- ```typescript
218
- import { createClient, OAuthStrategy } from '@wix/sdk';
219
- import { bookings } from '@wix/bookings';
220
- import { WIX_CLIENT_ID } from 'astro:env/client'; // browser client ID — NOT import.meta.env (see Bug-10 note)
221
-
222
- const wixClient = createClient({
223
- modules: { bookings },
224
- auth: OAuthStrategy({ clientId: WIX_CLIENT_ID }),
225
- });
226
-
227
- // The slot shape differs by service type — build bookedEntity.slot accordingly.
228
- // Omit `resource` either way — Wix auto-assigns an available resource on confirm.
229
- const slot = props.slot.serviceType === 'CLASS'
230
- // CLASS: a class session is identified by eventId. Wix derives start/end/
231
- // timezone/resource/location from the event — do NOT send scheduleId/startDate.
232
- ? {
233
- serviceId: props.slot.serviceId,
234
- eventId: props.slot.eventId,
235
- }
236
- // APPOINTMENT: identified by scheduleId + explicit start/end/timezone/location.
237
- : {
238
- serviceId: props.slot.serviceId,
239
- scheduleId: props.slot.scheduleId,
240
- startDate: props.slot.localStartDate, // pass localStartDate as startDate
241
- endDate: props.slot.localEndDate, // pass localEndDate as endDate
242
- timezone: props.slot.timezone,
243
- ...(props.slot.locationId
244
- ? { location: { _id: props.slot.locationId, locationType: props.slot.locationType as any } }
245
- : { location: { locationType: 'OWNER_BUSINESS' as const } }),
246
- };
247
-
248
- const bookingPayload = {
249
- totalParticipants: 1,
250
- contactDetails: {
251
- firstName: formData.firstName,
252
- lastName: formData.lastName || undefined,
253
- email: formData.email,
254
- phone: formData.phone || undefined,
255
- },
256
- selectedPaymentOption: 'OFFLINE' as const, // "OFFLINE" = pay at session; use "ONLINE" if the service has online payment enabled
257
- bookedEntity: { slot },
258
- };
259
-
260
- const result = await wixClient.bookings.createBooking(bookingPayload);
261
- ```
262
-
263
- 3. **Confirm the booking — this is mandatory, not optional.** `createBooking` returns a booking in **`CREATED`** status, which **does not hold a seat or block availability** (only `CONFIRMED` does — [lifecycle docs](https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/introduction)). If you stop at `createBooking`, the class **overbooks** and `remainingCapacity` never drops. Confirm via a server endpoint (next section) — `confirmBooking` needs Manage Bookings, so it can't run from this visitor client.
264
- 4. On success (confirm returns ok): call `onSuccess(result.booking?._id ?? '', props.slot.localStartDate)`. The booking ID field is `_id` (underscore prefix), not `id`.
265
- 5. On `SESSION_CAPACITY_EXCEEDED` (full session): route to the **waitlist** (see "Capacity, instructor & waitlist" below).
266
- 6. On other errors: `<p className="booking-error">This time slot was just taken. Please select another.</p>` + `onCancel()`. Loading state "Confirming…" while in flight. Always wrap in try/catch.
267
-
268
- ---
269
-
270
- ## Booking lifecycle, capacity, instructor & waitlist (verified live)
271
-
272
- These four behaviors were all verified against a live Bookings site — they're easy to get wrong and pass `astro build` while breaking at runtime.
273
-
274
- ### Confirm step (overbooking guard) — server endpoint `src/pages/api/confirm-booking.ts`
275
- `createBooking` → `CREATED` (holds nothing). Confirm it server-side to occupy the seat:
276
- ```typescript
277
- // POST { bookingId, revision } — elevated (Manage Bookings).
278
- import { auth } from "@wix/essentials";
279
- import { bookings } from "@wix/bookings";
280
- const res = await auth.elevate(bookings.confirmBooking)(bookingId, String(revision), {
281
- paymentStatus: "NOT_PAID", // pay-on-site class
282
- flowControlSettings: { checkAvailabilityValidation: true }, // 409s if it just filled — the real overbooking guard
283
- });
284
- ```
285
- `confirmBooking` is a real SDK method, so `auth.elevate(bookings.confirmBooking)` elevates correctly. The island POSTs `{ bookingId, revision }` here right after `createBooking`, and only redirects to the confirmation page when this returns ok.
286
-
287
- ### Capacity (multi-participant)
288
- The list slot carries `totalCapacity` / `remainingCapacity` / **`bookableCapacity`** (use `bookableCapacity` — it accounts for waitlist holds). Show "N spots left"; render a "Number of spots" selector capped at `bookableCapacity` and pass it as `booking.totalParticipants` (the verified field; not `numberOfParticipants`). A slot with `bookableCapacity <= 0` is full → waitlist.
289
-
290
- ### Instructor (per CLASS session)
291
- `listEventTimeSlots` does **not** return resources. Resolve the instructor for a chosen slot via `eventTimeSlots.getEventTimeSlot(eventId)` → `timeSlot.availableResources[].resources[].name` (take the first). Display it on the form ("with Jordan Rivera").
292
-
293
- ### Waitlist (native, v1 — there is no v2/v3 waitlist)
294
- A full session's `createBooking` throws `SESSION_CAPACITY_EXCEEDED`, whose message contains the **v1 session id** (*"…on session `<id>`"*). Extract it and POST to a server endpoint that registers on the native waitlist (`POST /bookings/v1/waitlist/register`, needs `contactId` + Manage Bookings → elevated). See the SSR-endpoints section in `SERVICES_PAGES.md`. Front-end extraction:
295
- ```typescript
296
- const full = `${err?.message ?? ""} ${JSON.stringify(err?.details ?? {})} ${String(err)}`;
297
- const sessionId = full.match(/on session ([^\s"}]+)/)?.[1]; // → waitingResource
298
- ```
299
-
300
- ---
301
-
302
- ## Wiring note — how the page uses these islands
303
-
304
- The `/services/[slug].astro` page (written by the `pages` scope) mounts both islands in a coordinated flow:
305
-
306
- ```tsx
307
- // Pseudocode — actual wiring is done by the pages scope.
308
- // `serviceType` (from service.type) MUST be threaded through so the calendar
309
- // picks the right time-slots API and the form builds the right createBooking shape.
310
- const [selectedSlot, setSelectedSlot] = useState(null);
311
-
312
- if (!selectedSlot) {
313
- return <AvailabilityCalendar serviceId={id} serviceType={type} onSlotSelected={setSelectedSlot} />;
314
- }
315
- return <BookingForm serviceId={id} serviceType={type} slot={selectedSlot} onSuccess={handleSuccess} onCancel={() => setSelectedSlot(null)} />;
316
- ```
317
-
318
- The components need to export cleanly so the page's React coordinator island can import both. Export each as a default export from its file.
319
-
320
- ---
321
-
322
- ## Return contract
323
-
324
- ```json
325
- {
326
- "status": "complete",
327
- "phase": "components",
328
- "vertical": "bookings",
329
- "files": [
330
- "src/components/ServiceCard.astro",
331
- "src/components/AvailabilityCalendar.tsx",
332
- "src/components/BookingForm.tsx",
333
- "src/components/ServiceBookingFlow.tsx",
334
- "src/components/ManageBooking.tsx"
335
- ]
336
- }
337
- ```
10
+ > **Start from the templates — don't re-author the SDK wiring.** Each island has
11
+ > an example at `<SKILL_ROOT>/references/astro/templates/bookings/`. Read and adapt
12
+ > (brand copy, styling, class names already in `components-bookings.css`); keep the
13
+ > SDK calls + payload shapes.
14
+
15
+ ## Islands you write
16
+
17
+ | File | From template | Role |
18
+ |------|---------------|------|
19
+ | `src/components/ServiceBookingFlow.tsx` | `…/ServiceBookingFlow.tsx` | `client:only` coordinator — holds the selected slot, swaps calendar → form, redirects on success. Mounted by `services/[slug].astro`. |
20
+ | `src/components/AvailabilityCalendar.tsx` | `…/AvailabilityCalendar.tsx` | the **week calendar** — week strip + nav → the picked day's slots. APPOINTMENT → `availabilityTimeSlots`; CLASS → `eventTimeSlots`. |
21
+ | `src/components/BookingForm.tsx` | `…/BookingForm.tsx` | the **schema-driven** form — renders the `@wix/forms` field list (passed in from the SSR page), keys values by `target`, calls `bookingDriver.book()`. |
22
+
23
+ ## astro-specific rules
24
+
25
+ 1. **Islands call `@wix` ambiently — no `createClient`/`OAuthStrategy`.** In an
26
+ astro browser island the `@wix/astro` visitor client is ambient (the same way
27
+ the ecom `CartView` island works): `import { availabilityTimeSlots } from "@wix/bookings"`
28
+ and call it directly. Do **not** build an `OAuthStrategy` client or read
29
+ `WIX_CLIENT_ID` — that pattern is for own/static builds (`../../custom/bookings/WIRING.md`).
30
+ 2. **The booking sequence is imported, not re-authored.** Import `book`,
31
+ `navigateToCheckout`, `BookResultType`, and the `SelectedSlot` type from the
32
+ pre-copied `./bookingDriver`. The form island calls `book(...)` and branches:
33
+ `CheckoutRequired` → `navigateToCheckout(cartId, postFlowUrl)`; `CheckoutSkipped`
34
+ → redirect to `/booking-confirmation`.
35
+ 3. **Mount everything `client:only="react"`.** Availability + booking are
36
+ timezone/session-specific; never SSR them.
37
+ 4. **The form fields come from SSR.** `services/[slug].astro` fetches the booking
38
+ form schema and passes a `fields` array into `ServiceBookingFlow` → `BookingForm`
39
+ (see `SERVICES_PAGES.md`). The island renders the fields generically — it does
40
+ not fetch the schema itself.
41
+ 5. **No CSS.** The classes (`.availability-*`, `.time-slot*`, `.booking-*`) are in
42
+ the pre-copied `components-bookings.css`; reference them, don't write them.
43
+
44
+ ## Return
45
+ `{ status, phase, scope: "components", summary, data, files, errors }`. If a pre-copied dependency (`bookingDriver.ts`, `components-bookings.css`) is missing, return `status: "partial"` with the coded error.