@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.
- package/package.json +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
- package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
- package/skills/wix-design-system/SKILL.md +1 -1
- package/skills/wix-headless/references/BUILD-astro.md +5 -7
- package/skills/wix-headless/references/SETUP.md +1 -1
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +58 -428
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +4 -7
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +158 -143
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +17 -13
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +16 -78
- package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +122 -179
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +61 -9
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +10 -2
- package/skills/wix-headless/references/bookings/FLOW.md +185 -0
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +60 -76
- package/skills/wix-headless/references/custom/bookings/WIRING.md +123 -65
- package/skills/wix-headless/references/verticals/bookings.md +6 -8
- package/skills/wix-manage/SKILL.md +12 -0
- package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
- package/skills/wix-manage/references/bookings/create-appointment-service.md +266 -0
- package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
- package/skills/wix-manage/references/bookings/create-class-service.md +245 -0
- package/skills/wix-manage/references/bookings/create-course-service.md +249 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
- package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
- package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
- package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
- package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
- package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
- package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// bookingDriver.ts — the booking sequence in plain @wix SDK calls.
|
|
2
|
+
// Framework-agnostic: no React, no signals, no UI. Any framework's booking step
|
|
3
|
+
// imports book()/navigateToCheckout() and drives them; APPOINTMENT and CLASS
|
|
4
|
+
// share the same sequence.
|
|
5
|
+
//
|
|
6
|
+
// Mechanism = ecom Cart V2 on every path: createBooking leaves the booking
|
|
7
|
+
// CREATED, then the cart holds the seat — a paid service hands off to the
|
|
8
|
+
// Wix-hosted checkout, a free / pay-in-person service places the order directly.
|
|
9
|
+
//
|
|
10
|
+
// On @wix/astro the SDK calls run ambiently (the visitor client is provided by
|
|
11
|
+
// @wix/essentials, like the ecom CartView island) — no createClient/OAuthStrategy
|
|
12
|
+
// needed. On an own/own-build SPA, acquire a visitor client with
|
|
13
|
+
// createClient({ modules, auth: OAuthStrategy({ clientId }) }) and call the same
|
|
14
|
+
// functions through it. Keep the SDK calls, payload shapes, and the sequence;
|
|
15
|
+
// adapt only brand copy/styling in the UI that drives this module.
|
|
16
|
+
|
|
17
|
+
import { bookings } from "@wix/bookings";
|
|
18
|
+
import { createCart, calculateCart, placeOrder } from "@wix/auto_sdk_ecom_cart-v-2";
|
|
19
|
+
import { redirects } from "@wix/redirects";
|
|
20
|
+
|
|
21
|
+
// ── Constants (C:src/services/constants.ts) ────────────────────────────────
|
|
22
|
+
export const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
|
|
23
|
+
export const STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
|
|
24
|
+
|
|
25
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
26
|
+
// FormValues = the submitted booking-form values, keyed by each field's `target`
|
|
27
|
+
// (NOT hardcoded field names). The default booking form's targets are snake_case
|
|
28
|
+
// `first_name`/`last_name`/`email`/`phone`; read them from the form schema and key
|
|
29
|
+
// the values by `target` — never hardcode the keys here.
|
|
30
|
+
export type FormValues = Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
export interface SelectedSlot {
|
|
33
|
+
serviceType: "APPOINTMENT" | "CLASS";
|
|
34
|
+
serviceId: string;
|
|
35
|
+
localStartDate: string; // local "YYYY-MM-DDTHH:mm:ss" (NOT UTC) — maps to slot.startDate
|
|
36
|
+
localEndDate: string; // " " → slot.endDate
|
|
37
|
+
timezone: string;
|
|
38
|
+
// APPOINTMENT:
|
|
39
|
+
scheduleId?: string;
|
|
40
|
+
locationId?: string; // from slot.location._id (the id field is `_id`, not `.id`)
|
|
41
|
+
locationType?: string; // "BUSINESS" | "CUSTOMER" | "CUSTOM"
|
|
42
|
+
// CLASS:
|
|
43
|
+
eventId?: string;
|
|
44
|
+
// Resource chosen on the slot (flow 5). When absent the driver emits the
|
|
45
|
+
// ANY_RESOURCE fallback so Wix auto-assigns a bookable staff member.
|
|
46
|
+
resource?: { _id: string; name?: string };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BookParams {
|
|
50
|
+
service: any; // the @wix/bookings Service (read payment.options + bookingPolicy)
|
|
51
|
+
slot: SelectedSlot;
|
|
52
|
+
formSubmission: FormValues; // values from the @wix/forms Form, keyed by target
|
|
53
|
+
timezone: string;
|
|
54
|
+
totalParticipants?: number;
|
|
55
|
+
/** consumer override; when omitted, derived from service.payment.options */
|
|
56
|
+
selectedPaymentOption?: "ONLINE" | "OFFLINE";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const BookResultType = {
|
|
60
|
+
CheckoutRequired: "checkout_required",
|
|
61
|
+
CheckoutSkipped: "checkout_skipped",
|
|
62
|
+
} as const;
|
|
63
|
+
export type BookResult =
|
|
64
|
+
| { type: typeof BookResultType.CheckoutRequired; cartId: string }
|
|
65
|
+
| { type: typeof BookResultType.CheckoutSkipped; orderId: string };
|
|
66
|
+
|
|
67
|
+
// ── Payment derivation ───────────────────────────────────────────────────────
|
|
68
|
+
function deriveSelectedPaymentOption(service: any): "ONLINE" | "OFFLINE" {
|
|
69
|
+
const options = service?.payment?.options;
|
|
70
|
+
if (options?.online && !options?.inPerson) return "ONLINE";
|
|
71
|
+
if (!options?.online && options?.inPerson) return "OFFLINE";
|
|
72
|
+
return "ONLINE"; // both / neither → ONLINE (consumer override wins, see below)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// LocationType maps from the availability slot's type → the booking endpoint enum.
|
|
76
|
+
function mapLocationType(slotType?: string): string {
|
|
77
|
+
switch (slotType) {
|
|
78
|
+
case "BUSINESS":
|
|
79
|
+
return "OWNER_BUSINESS";
|
|
80
|
+
case "CUSTOMER":
|
|
81
|
+
return "CUSTOM";
|
|
82
|
+
case "CUSTOM":
|
|
83
|
+
return "OWNER_CUSTOM";
|
|
84
|
+
default:
|
|
85
|
+
return "OWNER_BUSINESS";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── buildBookingRequest ───────────────────────────────────────────────────────
|
|
90
|
+
function buildBookingRequest(params: BookParams) {
|
|
91
|
+
const { service, slot, formSubmission, timezone } = params;
|
|
92
|
+
const resource = slot.resource; // prefer the slot's chosen resource when present
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
booking: {
|
|
96
|
+
// Consumer override wins over the service-config heuristic.
|
|
97
|
+
selectedPaymentOption:
|
|
98
|
+
params.selectedPaymentOption ?? deriveSelectedPaymentOption(service),
|
|
99
|
+
totalParticipants: params.totalParticipants || 1,
|
|
100
|
+
bookedEntity: {
|
|
101
|
+
slot: {
|
|
102
|
+
serviceId: slot.serviceId,
|
|
103
|
+
// APPOINTMENT carries scheduleId; CLASS carries eventId (Wix derives the rest).
|
|
104
|
+
scheduleId: slot.scheduleId ?? undefined,
|
|
105
|
+
startDate: slot.localStartDate,
|
|
106
|
+
endDate: slot.localEndDate,
|
|
107
|
+
timezone,
|
|
108
|
+
eventId: slot.eventId ?? undefined,
|
|
109
|
+
// Resource: use the chosen one, else the ANY_RESOURCE fallback so Wix
|
|
110
|
+
// auto-assigns a bookable staff member. Appointment availability slots
|
|
111
|
+
// return availableResources:[] yet book fine via ANY_RESOURCE.
|
|
112
|
+
...(resource
|
|
113
|
+
? { resource: { _id: resource._id, name: resource.name } }
|
|
114
|
+
: {
|
|
115
|
+
resourceSelections: [
|
|
116
|
+
{
|
|
117
|
+
resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID,
|
|
118
|
+
selectionMethod: "ANY_RESOURCE",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
location:
|
|
123
|
+
slot.locationId || slot.locationType
|
|
124
|
+
? {
|
|
125
|
+
...(slot.locationId ? { _id: slot.locationId } : {}),
|
|
126
|
+
locationType: mapLocationType(slot.locationType),
|
|
127
|
+
}
|
|
128
|
+
: { locationType: "OWNER_BUSINESS" },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
participantNotification: {
|
|
133
|
+
metadata: { channels: "EMAIL,SMS" },
|
|
134
|
+
notifyParticipants: true,
|
|
135
|
+
},
|
|
136
|
+
sendSmsReminder: true,
|
|
137
|
+
formSubmission,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── buildCartRequest ──────────────────────────────────────────────────────────
|
|
142
|
+
function buildCartRequest(args: {
|
|
143
|
+
bookingIds: string[];
|
|
144
|
+
contactDetails?: any;
|
|
145
|
+
businessLocationId?: string | null;
|
|
146
|
+
}) {
|
|
147
|
+
const cart: any = { source: { channelType: "WEB" } };
|
|
148
|
+
if (args.businessLocationId)
|
|
149
|
+
cart.businessInfo = { locationId: args.businessLocationId };
|
|
150
|
+
if (args.contactDetails) {
|
|
151
|
+
cart.customerInfo = args.contactDetails.email
|
|
152
|
+
? { email: args.contactDetails.email }
|
|
153
|
+
: {};
|
|
154
|
+
if (args.contactDetails.fullAddress?.country) {
|
|
155
|
+
cart.deliveryInfo = { address: { ...args.contactDetails.fullAddress } };
|
|
156
|
+
cart.paymentInfo = {
|
|
157
|
+
billingAddress: { ...args.contactDetails.fullAddress },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
catalogItems: args.bookingIds.map((id) => ({
|
|
163
|
+
quantity: 1,
|
|
164
|
+
catalogReference: { catalogItemId: id, appId: BOOKING_APP_ID },
|
|
165
|
+
})),
|
|
166
|
+
cart,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── isCheckoutRequired ────────────────────────────────────────────────────────
|
|
171
|
+
function isCheckoutRequired(cart: any, summary: any, service: any): boolean {
|
|
172
|
+
if (service?.bookingPolicy?.cancellationFeePolicy?.enabled) return true;
|
|
173
|
+
const total = Number(summary?.priceSummary?.total?.amount ?? 0);
|
|
174
|
+
if (total === 0) return false;
|
|
175
|
+
if (cart?.lineItems?.[0]?.paymentConfig?.paymentOption === "FULL_PAYMENT_OFFLINE")
|
|
176
|
+
return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── canBook ───────────────────────────────────────────────────────────────────
|
|
181
|
+
function canBook(params: BookParams): boolean {
|
|
182
|
+
const slotOk =
|
|
183
|
+
!!params.slot &&
|
|
184
|
+
!!params.slot.localStartDate &&
|
|
185
|
+
(params.slot.serviceType === "CLASS"
|
|
186
|
+
? !!params.slot.eventId
|
|
187
|
+
: !!params.slot.scheduleId);
|
|
188
|
+
return slotOk && params.formSubmission != null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── book — the full sequence ──────────────────────────────────────────────────
|
|
192
|
+
export async function book(params: BookParams): Promise<BookResult> {
|
|
193
|
+
if (!canBook(params))
|
|
194
|
+
throw new Error("Cannot book: missing slot or form submission");
|
|
195
|
+
|
|
196
|
+
// 1. createBooking → CREATED (the cart holds the seat)
|
|
197
|
+
const req = buildBookingRequest(params);
|
|
198
|
+
const created = await bookings.createBooking(req.booking as any, {
|
|
199
|
+
participantNotification: req.participantNotification,
|
|
200
|
+
sendSmsReminder: req.sendSmsReminder,
|
|
201
|
+
formSubmission: req.formSubmission as any,
|
|
202
|
+
});
|
|
203
|
+
const bookingId = created?.booking?._id;
|
|
204
|
+
if (!bookingId) throw new Error("Failed to create booking");
|
|
205
|
+
const contactDetails = created?.booking?.contactDetails;
|
|
206
|
+
|
|
207
|
+
// 2. createCart — one catalog item per booking id (channel WEB, bookings appId)
|
|
208
|
+
const businessLocationId = params.slot.locationId ?? undefined;
|
|
209
|
+
const cart = await createCart(
|
|
210
|
+
buildCartRequest({ bookingIds: [bookingId], contactDetails, businessLocationId }),
|
|
211
|
+
);
|
|
212
|
+
const cartId = cart?._id;
|
|
213
|
+
if (!cartId) throw new Error("Failed to create cart");
|
|
214
|
+
|
|
215
|
+
// 3. calculateCart → totals (not stored on the Cart V2 entity)
|
|
216
|
+
const { cart: calculatedCart, summary } = await calculateCart(cartId);
|
|
217
|
+
if (!calculatedCart || !summary) throw new Error("Failed to calculate cart");
|
|
218
|
+
|
|
219
|
+
// 4. checkout required? → redirect (paid) ; else placeOrder (free/offline)
|
|
220
|
+
if (isCheckoutRequired(calculatedCart, summary, params.service)) {
|
|
221
|
+
return { type: BookResultType.CheckoutRequired, cartId };
|
|
222
|
+
}
|
|
223
|
+
const order = await placeOrder(cartId);
|
|
224
|
+
const orderId = order?.orderId;
|
|
225
|
+
if (!orderId) throw new Error("Failed to place order");
|
|
226
|
+
return { type: BookResultType.CheckoutSkipped, orderId };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── navigateToCheckout ────────────────────────────────────────────────────────
|
|
230
|
+
// Paid services: hand the cart to the Wix-hosted ecom checkout. Same shape on
|
|
231
|
+
// every path — ecomCheckout.checkoutId = the cartId.
|
|
232
|
+
export async function navigateToCheckout(
|
|
233
|
+
cartId: string,
|
|
234
|
+
postFlowUrl: string,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const { redirectSession } = await redirects.createRedirectSession({
|
|
237
|
+
ecomCheckout: { checkoutId: cartId },
|
|
238
|
+
callbacks: { postFlowUrl },
|
|
239
|
+
});
|
|
240
|
+
if (redirectSession?.fullUrl) {
|
|
241
|
+
window.location.href = redirectSession.fullUrl;
|
|
242
|
+
} else {
|
|
243
|
+
throw new Error("Failed to create redirect session");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,39 +1,23 @@
|
|
|
1
1
|
/* src/styles/components-bookings.css
|
|
2
2
|
*
|
|
3
|
-
* Bookings-pack component CSS. Pre-copied by the orchestrator
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Bookings-pack component CSS. Pre-copied by the orchestrator before the build
|
|
4
|
+
* wave (it is brand-agnostic var(--token) CSS). The designer's global.css does
|
|
5
|
+
* NOT publish these classes — they live here. See references/shared/STYLING.md
|
|
6
|
+
* § "Component-specific CSS is owned by the component, not the designer".
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* .availability
|
|
10
|
-
*
|
|
11
|
-
* owned by the component, not the designer".
|
|
8
|
+
* Class groups:
|
|
9
|
+
* - .service-card / .service-grid / .service-card-* → ServiceCard.astro, services/index.astro
|
|
10
|
+
* - .availability-* / .time-slot* → AvailabilityCalendar.tsx (week calendar + slots)
|
|
11
|
+
* - .booking-* → BookingForm.tsx (schema-driven form)
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* services/index.astro, index.astro home teaser
|
|
16
|
-
* - .availability-calendar / .time-slot / .time-slot-* / .availability-*
|
|
17
|
-
* → AvailabilityCalendar.tsx date picker + slot grid
|
|
18
|
-
* - .booking-form / .booking-field / .booking-* → BookingForm.tsx
|
|
19
|
-
* details form + submit button
|
|
13
|
+
* Primary actions (a selected slot, the confirm button) use var(--color-accent)
|
|
14
|
+
* so the flow matches the site's primary-action styling.
|
|
20
15
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the designer's `@theme` block declares. Direct `var(--color-ink)` /
|
|
24
|
-
* `var(--spacing-sm)` calls work against any designer-published token set as
|
|
25
|
-
* long as the core scales exist (they always do). Use fallback chains
|
|
26
|
-
* (`var(--color-paper-warm, var(--color-paper, var(--color-cream)))`) for
|
|
27
|
-
* sometimes-published tokens.
|
|
16
|
+
* Direct var(--token) only (no @apply). Fallback chains for sometimes-published
|
|
17
|
+
* tokens, e.g. var(--color-paper-warm, var(--color-paper, var(--color-cream))).
|
|
28
18
|
*/
|
|
29
19
|
|
|
30
|
-
/*
|
|
31
|
-
Service grid + card — owned here, NOT in global.css.
|
|
32
|
-
.service-card uses overflow: hidden + border-radius, so inner padding
|
|
33
|
-
is the responsibility of THIS file (whoever sets the radius owns the
|
|
34
|
-
inset). The image is clipped by the card's overflow; content needs
|
|
35
|
-
padding-inline so text doesn't sit flush against the rounded edge.
|
|
36
|
-
========================================================================= */
|
|
20
|
+
/* ── Service grid + card ────────────────────────────────────────────────────── */
|
|
37
21
|
.service-grid {
|
|
38
22
|
display: grid;
|
|
39
23
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
@@ -75,9 +59,6 @@ body[data-navigating="true"] .service-grid {
|
|
|
75
59
|
object-fit: cover;
|
|
76
60
|
transition: transform 600ms ease;
|
|
77
61
|
}
|
|
78
|
-
.service-card-image--placeholder {
|
|
79
|
-
/* fallback when no image is provided */
|
|
80
|
-
}
|
|
81
62
|
|
|
82
63
|
.service-card-meta {
|
|
83
64
|
display: flex;
|
|
@@ -86,7 +67,6 @@ body[data-navigating="true"] .service-grid {
|
|
|
86
67
|
padding: var(--spacing-md) var(--spacing-md) var(--spacing-lg);
|
|
87
68
|
flex: 1;
|
|
88
69
|
}
|
|
89
|
-
|
|
90
70
|
.service-card-name {
|
|
91
71
|
margin: 0;
|
|
92
72
|
font-family: var(--font-display);
|
|
@@ -94,7 +74,6 @@ body[data-navigating="true"] .service-grid {
|
|
|
94
74
|
line-height: 1.25;
|
|
95
75
|
color: var(--color-ink);
|
|
96
76
|
}
|
|
97
|
-
|
|
98
77
|
.service-card-tagline {
|
|
99
78
|
margin: 0;
|
|
100
79
|
font-family: var(--font-body);
|
|
@@ -102,7 +81,6 @@ body[data-navigating="true"] .service-grid {
|
|
|
102
81
|
color: var(--color-ink-soft, var(--color-mute));
|
|
103
82
|
line-height: 1.45;
|
|
104
83
|
}
|
|
105
|
-
|
|
106
84
|
.service-card-price {
|
|
107
85
|
display: flex;
|
|
108
86
|
align-items: center;
|
|
@@ -119,17 +97,14 @@ body[data-navigating="true"] .service-grid {
|
|
|
119
97
|
color: var(--color-rule);
|
|
120
98
|
}
|
|
121
99
|
|
|
122
|
-
/*
|
|
123
|
-
Availability calendar — date navigator + slot grid.
|
|
124
|
-
Owned here, NOT in global.css.
|
|
125
|
-
========================================================================= */
|
|
100
|
+
/* ── Availability — week calendar ───────────────────────────────────────────── */
|
|
126
101
|
.availability-calendar {
|
|
127
102
|
display: flex;
|
|
128
103
|
flex-direction: column;
|
|
129
104
|
gap: var(--spacing-lg);
|
|
130
105
|
}
|
|
131
106
|
|
|
132
|
-
.availability-
|
|
107
|
+
.availability-week-nav {
|
|
133
108
|
display: flex;
|
|
134
109
|
align-items: center;
|
|
135
110
|
justify-content: space-between;
|
|
@@ -137,8 +112,7 @@ body[data-navigating="true"] .service-grid {
|
|
|
137
112
|
padding: var(--spacing-sm) 0;
|
|
138
113
|
border-bottom: 1px solid var(--color-rule);
|
|
139
114
|
}
|
|
140
|
-
|
|
141
|
-
.availability-date-label {
|
|
115
|
+
.availability-week-label {
|
|
142
116
|
font-family: var(--font-display);
|
|
143
117
|
font-size: 1rem;
|
|
144
118
|
color: var(--color-ink);
|
|
@@ -166,12 +140,57 @@ body[data-navigating="true"] .service-grid {
|
|
|
166
140
|
cursor: not-allowed;
|
|
167
141
|
}
|
|
168
142
|
|
|
169
|
-
|
|
143
|
+
/* The 7-day strip. */
|
|
144
|
+
.availability-day-strip {
|
|
170
145
|
display: grid;
|
|
171
|
-
grid-template-columns: repeat(
|
|
146
|
+
grid-template-columns: repeat(7, 1fr);
|
|
172
147
|
gap: var(--spacing-sm);
|
|
173
148
|
}
|
|
149
|
+
.availability-day {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 2px;
|
|
154
|
+
padding: var(--spacing-sm) 0;
|
|
155
|
+
border: 1px solid var(--color-rule);
|
|
156
|
+
border-radius: var(--radius-sm);
|
|
157
|
+
background: var(--color-paper, var(--color-cream));
|
|
158
|
+
color: var(--color-ink);
|
|
159
|
+
font-family: var(--font-body);
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
|
|
162
|
+
}
|
|
163
|
+
.availability-day-name {
|
|
164
|
+
font-size: 0.7rem;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.04em;
|
|
167
|
+
color: var(--color-mute);
|
|
168
|
+
}
|
|
169
|
+
.availability-day-num {
|
|
170
|
+
font-size: 1rem;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
}
|
|
173
|
+
.availability-day--has-slots:hover {
|
|
174
|
+
border-color: var(--color-accent);
|
|
175
|
+
}
|
|
176
|
+
.availability-day--empty {
|
|
177
|
+
opacity: 0.4;
|
|
178
|
+
cursor: not-allowed;
|
|
179
|
+
}
|
|
180
|
+
.availability-day--selected {
|
|
181
|
+
background: var(--color-accent);
|
|
182
|
+
border-color: var(--color-accent);
|
|
183
|
+
color: var(--color-paper);
|
|
184
|
+
}
|
|
185
|
+
.availability-day--selected .availability-day-name {
|
|
186
|
+
color: var(--color-paper);
|
|
187
|
+
}
|
|
174
188
|
|
|
189
|
+
.availability-slots {
|
|
190
|
+
display: grid;
|
|
191
|
+
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
|
192
|
+
gap: var(--spacing-sm);
|
|
193
|
+
}
|
|
175
194
|
.availability-loading,
|
|
176
195
|
.availability-empty,
|
|
177
196
|
.availability-error {
|
|
@@ -181,14 +200,17 @@ body[data-navigating="true"] .service-grid {
|
|
|
181
200
|
padding: var(--spacing-lg) 0;
|
|
182
201
|
text-align: center;
|
|
183
202
|
}
|
|
203
|
+
.availability-empty {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: var(--spacing-md);
|
|
208
|
+
}
|
|
184
209
|
.availability-error {
|
|
185
210
|
color: var(--color-error, var(--color-accent));
|
|
186
211
|
}
|
|
187
212
|
|
|
188
|
-
/*
|
|
189
|
-
Time slot buttons — individual slot items in the grid.
|
|
190
|
-
Three states: available (default), selected, unavailable.
|
|
191
|
-
========================================================================= */
|
|
213
|
+
/* ── Time-slot buttons ──────────────────────────────────────────────────────── */
|
|
192
214
|
.time-slot {
|
|
193
215
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
194
216
|
border: 1px solid var(--color-rule);
|
|
@@ -201,29 +223,25 @@ body[data-navigating="true"] .service-grid {
|
|
|
201
223
|
cursor: pointer;
|
|
202
224
|
transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
|
|
203
225
|
}
|
|
226
|
+
.time-slot-time {
|
|
227
|
+
font-weight: 500;
|
|
228
|
+
}
|
|
204
229
|
.time-slot--available {
|
|
205
230
|
border-color: var(--color-ink);
|
|
206
231
|
}
|
|
207
232
|
.time-slot--available:hover {
|
|
208
|
-
background: var(--color-
|
|
233
|
+
background: var(--color-accent);
|
|
234
|
+
border-color: var(--color-accent);
|
|
209
235
|
color: var(--color-paper);
|
|
210
236
|
}
|
|
211
237
|
.time-slot--selected {
|
|
212
|
-
background: var(--color-
|
|
238
|
+
background: var(--color-accent);
|
|
239
|
+
border-color: var(--color-accent);
|
|
213
240
|
color: var(--color-paper);
|
|
214
|
-
border-color: var(--color-ink);
|
|
215
241
|
font-weight: 600;
|
|
216
242
|
}
|
|
217
|
-
.time-slot--unavailable {
|
|
218
|
-
opacity: 0.3;
|
|
219
|
-
cursor: not-allowed;
|
|
220
|
-
border-color: var(--color-rule);
|
|
221
|
-
}
|
|
222
243
|
|
|
223
|
-
/*
|
|
224
|
-
Booking form — rendered after a slot is selected.
|
|
225
|
-
Owned here, NOT in global.css.
|
|
226
|
-
========================================================================= */
|
|
244
|
+
/* ── Booking form (schema-driven) ───────────────────────────────────────────── */
|
|
227
245
|
.booking-form {
|
|
228
246
|
display: flex;
|
|
229
247
|
flex-direction: column;
|
|
@@ -234,22 +252,37 @@ body[data-navigating="true"] .service-grid {
|
|
|
234
252
|
background: var(--color-paper-warm, var(--color-paper, var(--color-cream)));
|
|
235
253
|
}
|
|
236
254
|
|
|
237
|
-
.booking-form-
|
|
255
|
+
.booking-form-summary {
|
|
238
256
|
display: flex;
|
|
239
257
|
flex-direction: column;
|
|
240
258
|
gap: var(--spacing-xs);
|
|
259
|
+
padding-bottom: var(--spacing-md);
|
|
260
|
+
border-bottom: 1px solid var(--color-rule);
|
|
241
261
|
}
|
|
242
|
-
.booking-form-
|
|
262
|
+
.booking-form-summary strong {
|
|
243
263
|
font-family: var(--font-display);
|
|
244
|
-
font-size: 1.
|
|
264
|
+
font-size: 1.125rem;
|
|
245
265
|
color: var(--color-ink);
|
|
246
|
-
margin: 0;
|
|
247
266
|
}
|
|
248
|
-
.booking-form-
|
|
267
|
+
.booking-form-summary span {
|
|
249
268
|
font-family: var(--font-body);
|
|
250
|
-
font-size: 0.
|
|
269
|
+
font-size: 0.9375rem;
|
|
251
270
|
color: var(--color-ink-soft, var(--color-mute));
|
|
252
|
-
|
|
271
|
+
}
|
|
272
|
+
.booking-form-change {
|
|
273
|
+
align-self: flex-start;
|
|
274
|
+
margin-top: var(--spacing-xs);
|
|
275
|
+
background: none;
|
|
276
|
+
border: none;
|
|
277
|
+
padding: 0;
|
|
278
|
+
font-family: var(--font-body);
|
|
279
|
+
font-size: 0.8125rem;
|
|
280
|
+
color: var(--color-mute);
|
|
281
|
+
text-decoration: underline;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
.booking-form-change:hover {
|
|
285
|
+
color: var(--color-ink);
|
|
253
286
|
}
|
|
254
287
|
|
|
255
288
|
.booking-field {
|
|
@@ -257,17 +290,6 @@ body[data-navigating="true"] .service-grid {
|
|
|
257
290
|
flex-direction: column;
|
|
258
291
|
gap: var(--spacing-xs);
|
|
259
292
|
}
|
|
260
|
-
.booking-field-row {
|
|
261
|
-
display: grid;
|
|
262
|
-
grid-template-columns: 1fr 1fr;
|
|
263
|
-
gap: var(--spacing-md);
|
|
264
|
-
}
|
|
265
|
-
@media (max-width: 480px) {
|
|
266
|
-
.booking-field-row {
|
|
267
|
-
grid-template-columns: 1fr;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
293
|
.booking-label {
|
|
272
294
|
font-family: var(--font-display);
|
|
273
295
|
font-size: 0.75rem;
|
|
@@ -275,13 +297,11 @@ body[data-navigating="true"] .service-grid {
|
|
|
275
297
|
letter-spacing: 0.05em;
|
|
276
298
|
color: var(--color-ink-soft, var(--color-mute));
|
|
277
299
|
}
|
|
278
|
-
.booking-
|
|
279
|
-
content: " *";
|
|
300
|
+
.booking-required {
|
|
280
301
|
color: var(--color-accent);
|
|
281
302
|
}
|
|
282
303
|
|
|
283
|
-
.booking-input
|
|
284
|
-
.booking-textarea {
|
|
304
|
+
.booking-input {
|
|
285
305
|
width: 100%;
|
|
286
306
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
287
307
|
font-family: var(--font-body);
|
|
@@ -293,121 +313,44 @@ body[data-navigating="true"] .service-grid {
|
|
|
293
313
|
transition: border-color 150ms ease;
|
|
294
314
|
box-sizing: border-box;
|
|
295
315
|
}
|
|
296
|
-
.booking-input:focus
|
|
297
|
-
.booking-textarea:focus {
|
|
316
|
+
.booking-input:focus {
|
|
298
317
|
outline: none;
|
|
299
|
-
border-color: var(--color-
|
|
318
|
+
border-color: var(--color-accent);
|
|
300
319
|
}
|
|
301
|
-
.booking-
|
|
320
|
+
textarea.booking-input {
|
|
302
321
|
resize: vertical;
|
|
303
322
|
min-height: 6rem;
|
|
304
323
|
}
|
|
305
324
|
|
|
306
|
-
.booking-
|
|
325
|
+
.booking-error {
|
|
326
|
+
font-family: var(--font-body);
|
|
327
|
+
font-size: 0.875rem;
|
|
328
|
+
color: var(--color-error, var(--color-accent));
|
|
329
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
330
|
+
border: 1px solid var(--color-error, var(--color-accent));
|
|
331
|
+
border-radius: var(--radius-sm);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.booking-cta {
|
|
307
335
|
display: inline-flex;
|
|
308
336
|
align-items: center;
|
|
309
337
|
justify-content: center;
|
|
338
|
+
width: 100%;
|
|
310
339
|
padding: var(--spacing-md) var(--spacing-xl);
|
|
311
340
|
font-family: var(--font-display);
|
|
312
341
|
font-size: 0.9375rem;
|
|
313
342
|
font-weight: 500;
|
|
314
|
-
background: var(--color-
|
|
343
|
+
background: var(--color-accent);
|
|
315
344
|
color: var(--color-paper);
|
|
316
|
-
border: 1px solid var(--color-
|
|
345
|
+
border: 1px solid var(--color-accent);
|
|
317
346
|
border-radius: var(--radius-sm);
|
|
318
347
|
cursor: pointer;
|
|
319
|
-
|
|
320
|
-
margin-top: var(--spacing-sm);
|
|
321
|
-
transition: background-color 150ms ease, box-shadow 150ms ease;
|
|
348
|
+
transition: opacity 150ms ease;
|
|
322
349
|
}
|
|
323
|
-
.booking-
|
|
324
|
-
|
|
350
|
+
.booking-cta:hover:not(:disabled) {
|
|
351
|
+
opacity: 0.9;
|
|
325
352
|
}
|
|
326
|
-
.booking-
|
|
353
|
+
.booking-cta:disabled {
|
|
327
354
|
opacity: 0.5;
|
|
328
355
|
cursor: not-allowed;
|
|
329
356
|
}
|
|
330
|
-
|
|
331
|
-
.booking-cancel {
|
|
332
|
-
display: block;
|
|
333
|
-
text-align: center;
|
|
334
|
-
font-family: var(--font-body);
|
|
335
|
-
font-size: 0.875rem;
|
|
336
|
-
color: var(--color-mute);
|
|
337
|
-
cursor: pointer;
|
|
338
|
-
background: none;
|
|
339
|
-
border: none;
|
|
340
|
-
text-decoration: underline;
|
|
341
|
-
padding: 0;
|
|
342
|
-
margin-top: var(--spacing-xs);
|
|
343
|
-
}
|
|
344
|
-
.booking-cancel:hover {
|
|
345
|
-
color: var(--color-ink);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.booking-error {
|
|
349
|
-
font-family: var(--font-body);
|
|
350
|
-
font-size: 0.875rem;
|
|
351
|
-
color: var(--color-error, var(--color-accent));
|
|
352
|
-
padding: var(--spacing-sm) var(--spacing-md);
|
|
353
|
-
border: 1px solid var(--color-error, var(--color-accent));
|
|
354
|
-
border-radius: var(--radius-sm);
|
|
355
|
-
background: rgba(255, 0, 0, 0.04);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/* ── Slot capacity (CLASS) + full→waitlist ─────────────────────────────────── */
|
|
359
|
-
.time-slot {
|
|
360
|
-
display: flex;
|
|
361
|
-
flex-direction: column;
|
|
362
|
-
align-items: center;
|
|
363
|
-
gap: 2px;
|
|
364
|
-
line-height: 1.2;
|
|
365
|
-
}
|
|
366
|
-
.time-slot-time {
|
|
367
|
-
font-weight: 500;
|
|
368
|
-
}
|
|
369
|
-
.time-slot-capacity {
|
|
370
|
-
font-size: 0.7rem;
|
|
371
|
-
letter-spacing: 0.04em;
|
|
372
|
-
text-transform: uppercase;
|
|
373
|
-
color: var(--color-mute, var(--color-secondary));
|
|
374
|
-
}
|
|
375
|
-
.time-slot--full {
|
|
376
|
-
opacity: 0.85;
|
|
377
|
-
border-style: dashed;
|
|
378
|
-
}
|
|
379
|
-
.time-slot--full .time-slot-capacity {
|
|
380
|
-
color: var(--color-accent);
|
|
381
|
-
}
|
|
382
|
-
.booking-form-note {
|
|
383
|
-
font-family: var(--font-body);
|
|
384
|
-
font-size: 0.875rem;
|
|
385
|
-
color: var(--color-mute, var(--color-secondary));
|
|
386
|
-
margin-top: var(--spacing-xs);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/* ── Manage booking page ─────────────────────────────────────────────────────── */
|
|
390
|
-
.manage-booking {
|
|
391
|
-
display: flex;
|
|
392
|
-
flex-direction: column;
|
|
393
|
-
gap: var(--spacing-lg);
|
|
394
|
-
align-items: flex-start;
|
|
395
|
-
}
|
|
396
|
-
.manage-booking-summary,
|
|
397
|
-
.manage-booking-status {
|
|
398
|
-
font-family: var(--font-body);
|
|
399
|
-
font-size: 1.05rem;
|
|
400
|
-
color: var(--color-ink, var(--color-primary));
|
|
401
|
-
}
|
|
402
|
-
.manage-booking-note {
|
|
403
|
-
font-family: var(--font-body);
|
|
404
|
-
font-size: 0.875rem;
|
|
405
|
-
color: var(--color-mute, var(--color-secondary));
|
|
406
|
-
}
|
|
407
|
-
.manage-booking-link {
|
|
408
|
-
color: var(--color-ink, var(--color-primary));
|
|
409
|
-
text-decoration: underline;
|
|
410
|
-
}
|
|
411
|
-
.manage-booking-link:hover {
|
|
412
|
-
color: var(--color-accent);
|
|
413
|
-
}
|