@wix/agent-skills 1.8.0 → 1.10.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 (66) 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/DESIGN_SYSTEM.md +6 -16
  10. package/skills/wix-headless/references/SETUP.md +1 -1
  11. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  12. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +77 -428
  13. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +2 -2
  14. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +7 -10
  15. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +2 -2
  16. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
  17. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
  18. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +7 -7
  19. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +4 -4
  20. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +2 -2
  21. package/skills/wix-headless/references/astro/templates/Footer.astro +4 -4
  22. package/skills/wix-headless/references/astro/templates/Navigation.astro +4 -4
  23. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +298 -142
  24. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  25. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +35 -14
  26. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +11 -2
  27. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +23 -85
  28. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  29. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +209 -198
  30. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +114 -19
  31. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +131 -15
  32. package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +3 -3
  33. package/skills/wix-headless/references/astro/templates/ecom/cart.astro +3 -3
  34. package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +17 -17
  35. package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +3 -3
  36. package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +4 -4
  37. package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +19 -18
  38. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +3 -3
  39. package/skills/wix-headless/references/astro/templates/global.css +17 -15
  40. package/skills/wix-headless/references/astro/templates/index.astro +4 -4
  41. package/skills/wix-headless/references/astro/templates/stores/components-stores.css +31 -31
  42. package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +4 -4
  43. package/skills/wix-headless/references/bookings/FLOW.md +246 -0
  44. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +65 -76
  45. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +60 -0
  46. package/skills/wix-headless/references/custom/bookings/WIRING.md +169 -60
  47. package/skills/wix-headless/references/shared/DESIGN_MD.md +2 -3
  48. package/skills/wix-headless/references/shared/IMPLEMENTER.md +4 -4
  49. package/skills/wix-headless/references/shared/STYLING.md +15 -18
  50. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +2 -2
  51. package/skills/wix-headless/references/verticals/bookings.md +9 -9
  52. package/skills/wix-headless/scripts/compose.mjs +4 -19
  53. package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -3
  54. package/skills/wix-manage/SKILL.md +12 -0
  55. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  56. package/skills/wix-manage/references/bookings/create-appointment-service.md +254 -0
  57. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  58. package/skills/wix-manage/references/bookings/create-class-service.md +233 -0
  59. package/skills/wix-manage/references/bookings/create-course-service.md +237 -0
  60. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  61. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  62. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  63. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  64. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  65. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  66. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -5,26 +5,83 @@ import Layout from "../../layouts/Layout.astro";
5
5
  import SeoTags from "../../components/SeoTags.astro";
6
6
  import ServiceBookingFlow from "../../components/ServiceBookingFlow";
7
7
  import { services } from "@wix/bookings";
8
+ import { forms } from "@wix/forms";
8
9
  import { auth } from "@wix/essentials";
9
10
 
11
+ const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
10
12
  const { slug } = Astro.params;
11
13
 
14
+ // Carried from the catalog's location selector (when the site has >1 location):
15
+ // the default location the availability picker scopes to. Absent on single-location
16
+ // sites (and on deep links — the picker then defaults to the service's first location).
17
+ const reqUrl = new URL(Astro.request.url);
18
+ const locationId = reqUrl.searchParams.get("locationId") ?? undefined;
19
+
12
20
  let service: any = null;
13
21
  try {
14
- // Ambient SSR via @wix/essentials; query BUILDER (object form returns 0).
15
- // Fetch the small catalog and match the slug in JS (robust without a
16
- // nested-field `mainSlug.name` builder filter).
17
- const result = await auth.elevate(services.queryServices)().limit(100).find();
18
- service =
19
- (result.items ?? []).find(
20
- (s: any) => (s.mainSlug?.name ?? s.supportedSlugs?.[0]?.name) === slug,
21
- ) ?? null;
22
+ // Ambient SSR via @wix/essentials. Single service by slug: the .eq() builder
23
+ // chain on mainSlug.name + appId (the form the @wix/bookings SDK expects).
24
+ const result = await auth
25
+ .elevate(services.queryServices)({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
26
+ .eq("mainSlug.name", slug)
27
+ .eq("appId", BOOKING_APP_ID)
28
+ .limit(1)
29
+ .find();
30
+ service = result.items?.[0] ?? null;
22
31
  } catch (err) {
23
32
  console.error(`[services/${slug}] detail query failed:`, err);
24
33
  }
25
34
 
26
35
  if (!service) return Astro.redirect("/services", 302);
27
36
 
37
+ // Booking-form SCHEMA — the form belongs to the service (service.form._id) in the
38
+ // bookings namespace. Read it server-side and pass the field list to the island.
39
+ // Same renderer the forms vertical uses (references/astro/forms/CONTACT_FORM.md):
40
+ // keep the field's `target` — that is the key createBooking expects in formSubmission.
41
+ let formFields: any[] = [];
42
+ try {
43
+ const formId = service.form?._id;
44
+ if (formId) {
45
+ const res: any = await auth.elevate(forms.getForm)(formId);
46
+ const form = res?.form ?? res;
47
+ // Only render field types we can submit as a simple value. Skip complex,
48
+ // object-valued fields (e.g. a multi-line ADDRESS) — they carry no string
49
+ // `componentType`, and sending a string for them fails createBooking with
50
+ // "must be object". The booking enforces only the contact basics
51
+ // (first_name/last_name/email), so skipping optional complex fields is safe.
52
+ const RENDERABLE = ["TEXT_INPUT", "PHONE_INPUT", "DROPDOWN"];
53
+ formFields = (form?.formFields ?? [])
54
+ .filter(
55
+ (f: any) =>
56
+ f.fieldType === "INPUT" &&
57
+ !f.hidden &&
58
+ RENDERABLE.includes(f.inputOptions?.stringOptions?.componentType),
59
+ )
60
+ .map((f: any) => {
61
+ const opts = f.inputOptions;
62
+ const str = opts?.stringOptions;
63
+ const label =
64
+ str?.textInputOptions?.label ??
65
+ str?.dropdownOptions?.label ??
66
+ str?.phoneInputOptions?.label ??
67
+ "";
68
+ return {
69
+ label,
70
+ target: opts?.target ?? "",
71
+ required: opts?.required ?? false,
72
+ componentType: str?.componentType ?? "TEXT_INPUT",
73
+ identifier: f.identifier ?? "",
74
+ options: str?.dropdownOptions?.options?.map((o: any) => ({
75
+ value: o.value ?? o.label ?? "",
76
+ label: o.label ?? o.value ?? "",
77
+ })),
78
+ };
79
+ });
80
+ }
81
+ } catch (err) {
82
+ console.error(`[services/${slug}] booking form schema failed:`, err);
83
+ }
84
+
28
85
  const formatPrice = (p: any): string | undefined => {
29
86
  if (!p || p.rateType === "NO_FEE") return undefined;
30
87
  if (p.rateType === "FIXED" && p.fixed?.price) {
@@ -39,41 +96,79 @@ const duration = service.schedule?.availabilityConstraints?.sessionDurations?.[0
39
96
  const imageUrl = service.media?.mainMedia?.image?.url ?? service.media?.items?.[0]?.image?.url;
40
97
  const serviceName = service.name ?? "Service";
41
98
  const seoTags = service.seoData?.tags ?? [];
99
+
100
+ // Staff for the optional staff picker — carried on the service via the
101
+ // STAFF_MEMBER_DETAILS conditional field (already requested in the query above).
102
+ // Each entry is { staffMemberId, name } — `staffMemberId` is, despite the name,
103
+ // the resource id (the calendar filters availability + books by it). The calendar
104
+ // shows the picker only when there is more than one; empty/absent → "any staff
105
+ // member" (the ANY_RESOURCE fallback).
106
+ const staffMembers = service.staffMemberDetails?.staffMembers ?? [];
107
+
108
+ // Business locations the service is offered at — sourced from `queryLocations()`
109
+ // (the site's real business locations, whose ids the availability engine
110
+ // recognizes), intersected with the service's own location ids. Availability is
111
+ // scoped to one of these so a multi-location service doesn't return duplicate
112
+ // per-location time slots (listAvailabilityTimeSlots returns one slot per location
113
+ // otherwise). Source from `queryLocations()`, NOT from `service.locations` alone:
114
+ // a service's location entry can carry an id the availability engine doesn't
115
+ // recognize, and scoping to it returns zero slots. When the site has no business
116
+ // locations this resolves to [] and the calendar fetches unscoped.
117
+ let serviceLocations: { _id: string; name?: string }[] = [];
118
+ try {
119
+ const locResult: any = await auth.elevate(services.queryLocations)();
120
+ const businessLocations = locResult?.businessLocations?.locations ?? [];
121
+ const serviceLocationIds = new Set(
122
+ (service.locations ?? [])
123
+ .filter((l: any) => (l.type ?? "BUSINESS") === "BUSINESS")
124
+ .map((l: any) => l.business?._id ?? l._id)
125
+ .filter(Boolean),
126
+ );
127
+ serviceLocations = businessLocations
128
+ .filter((l: any) => serviceLocationIds.has(l._id))
129
+ .map((l: any) => ({ _id: l._id, name: l.business?.name ?? "Location" }));
130
+ } catch (err) {
131
+ console.error(`[services/${slug}] queryLocations failed:`, err);
132
+ }
42
133
  ---
43
134
 
44
135
  <!--
45
- Width: use `container-reading` (max-width: var(--container-prose) + auto margins),
46
- NOT a bare `max-w-<size>` — in this Tailwind v4 setup an un-emitted max-w-<size>
47
- key falls back to the spacing scale (e.g. max-w-4xl → ~7rem, one word per line).
136
+ Width: use `container-reading` (max-width: var(--container-prose) + auto margins)
137
+ for a deliberate prose measure. Bare `max-w-<size>` keys resolve to Tailwind's
138
+ --container-* scale, which is wider than ideal for reading text.
48
139
  -->
49
140
  <Layout title={serviceName} description={service.tagLine ?? service.description}>
50
141
  <SeoTags tags={seoTags} slot="head" />
51
- <section class="container-reading px-lg py-4xl">
52
- <a href="/services" class="inline-block text-sm text-mute hover:text-ink mb-xl">← All services</a>
142
+ <section class="container-reading px-6 py-24">
143
+ <a href="/services" class="inline-block text-sm text-mute hover:text-ink mb-8">← All services</a>
53
144
 
54
145
  <!-- Optional banner image — only when the service has media (image-less is the
55
146
  common seed case; rendering a fixed placeholder collapses the layout). -->
56
147
  {imageUrl && (
57
- <img src={imageUrl} alt={serviceName} class="w-full h-64 object-cover rounded-lg mb-xl" />
148
+ <img src={imageUrl} alt={serviceName} class="w-full h-64 object-cover rounded-lg mb-8" />
58
149
  )}
59
150
 
60
- <div class="flex flex-col gap-md mb-3xl">
151
+ <div class="flex flex-col gap-4 mb-16">
61
152
  <hr class="editorial-rule w-16 ml-0" />
62
153
  <h1 class="font-display text-4xl md:text-5xl leading-tight">{serviceName}</h1>
63
154
  {service.tagLine && <p class="text-lg text-mute leading-relaxed max-w-prose">{service.tagLine}</p>}
64
- <div class="flex items-center gap-md text-sm text-mute">
155
+ <div class="flex items-center gap-4 text-sm text-mute">
65
156
  <span>{duration} min</span>
66
157
  {price && <span class="text-ink font-medium">{price}</span>}
67
158
  </div>
68
159
  {service.description && <p class="text-ink leading-relaxed max-w-prose">{service.description}</p>}
69
160
  </div>
70
161
 
71
- <section class="border-t border-rule pt-2xl">
72
- <h2 class="font-display text-3xl leading-tight mb-xl">Book this service</h2>
162
+ <section class="border-t border-rule pt-12">
163
+ <h2 class="font-display text-3xl leading-tight mb-8">Book this service</h2>
73
164
  <ServiceBookingFlow
74
- serviceId={service._id}
165
+ service={service}
75
166
  serviceName={serviceName}
76
167
  serviceType={service.type ?? "APPOINTMENT"}
168
+ fields={formFields}
169
+ staffMembers={staffMembers}
170
+ locations={serviceLocations}
171
+ locationId={locationId}
77
172
  client:only="react"
78
173
  />
79
174
  </section>
@@ -2,22 +2,93 @@
2
2
  // src/pages/services/index.astro — SSR services listing grid (APPOINTMENT or CLASS).
3
3
  // One dir deep inside src/pages → ../../ reaches src/. Adapt copy/headings to the
4
4
  // brand; keep the SSR data wiring.
5
+ //
6
+ // Optional catalog filters (both auto-skip when there's nothing to filter):
7
+ // location — a location selector, shown ONLY when the site has >1 location
8
+ // (business + custom + customer). Mirrors the SoT's location-first
9
+ // flow without forcing a separate step: single-location sites never
10
+ // see it. The choice is carried as ?locationId (+?locationType) and
11
+ // forwarded into both the services query and the detail page's
12
+ // availability.
13
+ // category — a category filter bar, shown ONLY when >1 category exists. The
14
+ // choice is carried as ?category and merged into the services query.
15
+ // Both are plain links that re-SSR with query params (re-query, like the SoT) —
16
+ // no client JS on the catalog.
5
17
  import Layout from "../../layouts/Layout.astro";
6
18
  import ServiceCard from "../../components/ServiceCard.astro";
7
- import { services } from "@wix/bookings";
19
+ import { services, categoriesV2 } from "@wix/bookings";
8
20
  import { auth } from "@wix/essentials";
9
21
 
10
- let serviceList: any[] = [];
11
- try {
12
- // Ambient @wix/astro SSR client via @wix/essentials (same pattern as CMS pages).
13
- // Do NOT build createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) }) —
14
- // that env var is client-only and undefined at server render → 500.
15
- // queryServices uses the query BUILDER; the object form returns 0 items.
16
- const result = await auth.elevate(services.queryServices)().limit(100).find();
17
- serviceList = (result.items ?? []).filter((s: any) => !s.hidden);
18
- } catch (err) {
19
- console.error("[services] listing query failed:", err);
20
- }
22
+ // Wix Bookings app id — the base filter must scope queryServices to it.
23
+ const BOOKING_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
24
+
25
+ const reqUrl = new URL(Astro.request.url);
26
+ const selectedCategory = reqUrl.searchParams.get("category") ?? "";
27
+ const selectedLocation = reqUrl.searchParams.get("locationId") ?? "";
28
+ const selectedLocationType = reqUrl.searchParams.get("locationType") ?? "";
29
+
30
+ // ── Services filter (appId + optional category + optional location) ─────────
31
+ // A real business-location id filters by locations.business.id; the synthetic
32
+ // "custom"/"customer" ids map to a location-TYPE filter (the SoT mapping). Built
33
+ // from URL params only, so it doesn't depend on the locations/categories reads.
34
+ const filter: Record<string, any> = { appId: BOOKING_APP_ID };
35
+ if (selectedCategory) filter["category.id"] = { $eq: selectedCategory };
36
+ if (selectedLocation === "custom") filter["locations.type"] = { $hasSome: ["CUSTOM"] };
37
+ else if (selectedLocation === "customer") filter["locations.type"] = { $hasSome: ["CUSTOMER"] };
38
+ else if (selectedLocation) filter["locations.business.id"] = { $hasSome: [selectedLocation] };
39
+
40
+ // ── Catalog reads — three INDEPENDENT queries, fetched concurrently ─────────
41
+ // queryServices (the filtered list) + queryLocations (selector) + queryCategories
42
+ // (bar) don't depend on each other, so run them in parallel (the SoT fetches
43
+ // services + categories with Promise.allSettled likewise) — one round-trip's
44
+ // latency, not three. Each is individually non-fatal: a failed read degrades only
45
+ // its own piece (an empty list / a hidden selector or bar), never the whole page.
46
+ // Ambient @wix/astro SSR client via @wix/essentials — do NOT build
47
+ // createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) }); that env
48
+ // var is client-only and undefined at server render → 500.
49
+ const [servicesResult, locResult, catResult] = await Promise.all([
50
+ auth
51
+ .elevate(services.queryServices)({ query: { filter, paging: { limit: 100 } }, conditionalFields: ["STAFF_MEMBER_DETAILS"] })
52
+ .find()
53
+ .catch((err: any) => { console.error("[services] listing query failed:", err); return null; }),
54
+ auth.elevate(services.queryLocations)().catch((err: any) => { console.error("[services] queryLocations failed:", err); return null; }),
55
+ auth.elevate(categoriesV2.queryCategories)().find().catch((err: any) => { console.error("[services] queryCategories failed:", err); return null; }),
56
+ ]);
57
+
58
+ const serviceList: any[] = (servicesResult?.items ?? []).filter((s: any) => !s.hidden);
59
+
60
+ // Locations — auto-skip the selector when ≤1 (business + custom + customer).
61
+ const businessLocations: any[] = locResult?.businessLocations?.locations ?? [];
62
+ const hasCustomLocations: boolean = locResult?.customLocations?.exists ?? false;
63
+ const hasCustomerLocations: boolean = locResult?.customerLocations?.exists ?? false;
64
+ const locationCount =
65
+ businessLocations.length + (hasCustomLocations ? 1 : 0) + (hasCustomerLocations ? 1 : 0);
66
+ const showLocationSelector = locationCount > 1;
67
+
68
+ // Categories — non-fatal; show the bar only when >1 (a fresh site has one default).
69
+ const categories: any[] = catResult?.items ?? [];
70
+ const showCategoryBar = categories.length > 1;
71
+
72
+ // Build a /services href preserving the current selection, applying overrides.
73
+ // Pass null to clear a param. locationType is always paired with locationId.
74
+ const hrefWith = (patch: Record<string, string | null>): string => {
75
+ const p = new URLSearchParams();
76
+ if (selectedCategory) p.set("category", selectedCategory);
77
+ if (selectedLocation) {
78
+ p.set("locationId", selectedLocation);
79
+ if (selectedLocationType) p.set("locationType", selectedLocationType);
80
+ }
81
+ for (const [k, v] of Object.entries(patch)) {
82
+ if (v === null) p.delete(k);
83
+ else p.set(k, v);
84
+ }
85
+ if (patch.locationId === null) p.delete("locationType");
86
+ const qs = p.toString();
87
+ return qs ? `/services?${qs}` : "/services";
88
+ };
89
+
90
+ // Is a filter narrowing the list right now? Drives the empty-state copy.
91
+ const filterActive = Boolean(selectedCategory || selectedLocation);
21
92
 
22
93
  const getSlug = (s: any): string => s.mainSlug?.name ?? s.supportedSlugs?.[0]?.name ?? s._id;
23
94
  const formatPrice = (s: any): string | undefined => {
@@ -31,19 +102,62 @@ const formatPrice = (s: any): string | undefined => {
31
102
  return undefined;
32
103
  };
33
104
  const getDuration = (s: any): number => s.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? 60;
105
+ const locationName = (loc: any): string => loc?.business?.name ?? loc?.calculatedAddress?.city ?? "Location";
34
106
  ---
35
107
 
36
108
  <Layout title="Services">
37
- <section class="max-w-6xl mx-auto px-lg py-4xl">
38
- <header class="flex flex-col gap-md max-w-prose mb-3xl">
109
+ <section class="max-w-6xl mx-auto px-6 py-24">
110
+ <header class="flex flex-col gap-4 max-w-prose mb-16">
39
111
  <hr class="editorial-rule w-16 ml-0" />
40
112
  <h1 class="font-display text-4xl md:text-5xl leading-tight">Our Services</h1>
41
113
  <!-- Optional brand tagline -->
42
114
  </header>
43
115
 
116
+ {
117
+ showLocationSelector && (
118
+ <nav class="booking-filter-bar" aria-label="Filter by location">
119
+ <a href={hrefWith({ locationId: null })} class={`booking-filter-chip${selectedLocation ? "" : " booking-filter-chip--active"}`}>All locations</a>
120
+ {businessLocations.map((loc: any) => (
121
+ <a
122
+ href={hrefWith({ locationId: loc._id, locationType: "BUSINESS" })}
123
+ class={`booking-filter-chip${selectedLocation === loc._id ? " booking-filter-chip--active" : ""}`}
124
+ >
125
+ {locationName(loc)}
126
+ </a>
127
+ ))}
128
+ {hasCustomLocations && (
129
+ <a href={hrefWith({ locationId: "custom", locationType: "CUSTOM" })} class={`booking-filter-chip${selectedLocation === "custom" ? " booking-filter-chip--active" : ""}`}>Custom location</a>
130
+ )}
131
+ {hasCustomerLocations && (
132
+ <a href={hrefWith({ locationId: "customer", locationType: "CUSTOMER" })} class={`booking-filter-chip${selectedLocation === "customer" ? " booking-filter-chip--active" : ""}`}>My location</a>
133
+ )}
134
+ </nav>
135
+ )
136
+ }
137
+
138
+ {
139
+ showCategoryBar && (
140
+ <nav class="booking-filter-bar" aria-label="Filter by category">
141
+ <a href={hrefWith({ category: null })} class={`booking-filter-chip${selectedCategory ? "" : " booking-filter-chip--active"}`}>All</a>
142
+ {categories.map((cat: any) => (
143
+ <a
144
+ href={hrefWith({ category: cat._id })}
145
+ class={`booking-filter-chip${selectedCategory === cat._id ? " booking-filter-chip--active" : ""}`}
146
+ >
147
+ {cat.name ?? "Category"}
148
+ </a>
149
+ ))}
150
+ </nav>
151
+ )
152
+ }
153
+
44
154
  {
45
155
  serviceList.length === 0 ? (
46
- <p class="text-mute">Services coming soon — check back shortly.</p>
156
+ <p class="text-mute">
157
+ {filterActive
158
+ ? "No services match your selection — try a different filter."
159
+ : "Services coming soon — check back shortly."}
160
+ </p>
47
161
  ) : (
48
162
  <div class="service-grid">
49
163
  {serviceList.map((s) => (
@@ -56,6 +170,8 @@ const getDuration = (s: any): number => s.schedule?.availabilityConstraints?.ses
56
170
  price={formatPrice(s)}
57
171
  imageUrl={s.media?.mainMedia?.image?.url ?? s.media?.items?.[0]?.image?.url}
58
172
  type={s.type ?? "APPOINTMENT"}
173
+ locationId={selectedLocation || undefined}
174
+ locationType={selectedLocationType || undefined}
59
175
  />
60
176
  ))}
61
177
  </div>
@@ -354,9 +354,9 @@ export default function CartView() {
354
354
  const hasUnavailable = items.some(isItemUnavailable);
355
355
 
356
356
  return (
357
- <div className="grid grid-cols-1 md:grid-cols-[1fr_22rem] gap-2xl md:gap-3xl md:items-start">
357
+ <div className="grid grid-cols-1 md:grid-cols-[1fr_22rem] gap-12 md:gap-16 md:items-start">
358
358
  {/* ── Left column: line items ── */}
359
- <div className="flex flex-col gap-lg">
359
+ <div className="flex flex-col gap-6">
360
360
  {items.map((item) => {
361
361
  const unavailable = isItemUnavailable(item);
362
362
  const maxQty = item.availability?.quantityAvailable ?? 99;
@@ -367,7 +367,7 @@ export default function CartView() {
367
367
  return (
368
368
  <div
369
369
  key={item._id}
370
- className={`flex gap-lg pb-lg border-b border-rule last:border-0${unavailable ? " unavailable" : ""}`}
370
+ className={`flex gap-6 pb-6 border-b border-rule last:border-0${unavailable ? " unavailable" : ""}`}
371
371
  >
372
372
  {/* Product image + name link back to the product detail page
373
373
  for available items. Unavailable lines render as plain text. */}
@@ -4,9 +4,9 @@ import CartView from "../components/CartView.tsx";
4
4
  ---
5
5
 
6
6
  <Layout title="Cart">
7
- <main class="py-3xl">
8
- <div class="max-w-wide mx-auto px-xl">
9
- <h1 class="font-display text-4xl mb-2xl">Your Cart</h1>
7
+ <main class="py-16">
8
+ <div class="max-w-wide mx-auto px-8">
9
+ <h1 class="font-display text-4xl mb-12">Your Cart</h1>
10
10
  <CartView client:load />
11
11
  </div>
12
12
  </main>
@@ -20,7 +20,7 @@
20
20
  * used `@apply text-bark/70 bg-tobacco …` (stale token vocabulary) and
21
21
  * `@apply size-6` (built-in utility that needs `@reference` to be visible
22
22
  * here), and both classes built-failed against current designer output.
23
- * Direct `var(--color-ink)` / `var(--spacing-sm)` calls work against any
23
+ * Direct `var(--color-ink)` / `0.75rem` calls work against any
24
24
  * designer-published token set as long as the core scales exist (they always
25
25
  * do). Use fallback chains (`var(--color-paper-warm, var(--color-paper, var(--color-cream)))`)
26
26
  * for sometimes-published tokens.
@@ -33,22 +33,22 @@
33
33
  .cart-summary {
34
34
  background: var(--color-paper-warm, var(--color-paper, var(--color-cream)));
35
35
  border: 1px solid var(--color-rule);
36
- padding: var(--spacing-xl);
36
+ padding: 2rem;
37
37
  border-radius: var(--radius-md);
38
38
  display: flex;
39
39
  flex-direction: column;
40
- gap: var(--spacing-sm);
40
+ gap: 0.75rem;
41
41
  }
42
42
  @media (min-width: 768px) {
43
43
  .cart-summary {
44
44
  position: sticky;
45
- top: var(--spacing-xl);
45
+ top: 2rem;
46
46
  }
47
47
  }
48
48
 
49
49
  .cart-empty {
50
50
  text-align: center;
51
- padding-block: var(--spacing-3xl);
51
+ padding-block: 4rem;
52
52
  color: var(--color-mute);
53
53
  }
54
54
  .cart-empty .checkout-btn {
@@ -66,7 +66,7 @@
66
66
  display: inline-flex;
67
67
  align-items: center;
68
68
  justify-content: center;
69
- padding: var(--spacing-md) var(--spacing-lg);
69
+ padding: 1rem 1.5rem;
70
70
  font-family: var(--font-display);
71
71
  font-size: 0.9375rem;
72
72
  font-weight: 500;
@@ -76,7 +76,7 @@
76
76
  border-radius: var(--radius-sm);
77
77
  cursor: pointer;
78
78
  width: 100%;
79
- margin-top: var(--spacing-md);
79
+ margin-top: 1rem;
80
80
  transition: background-color 150ms ease, box-shadow 150ms ease;
81
81
  }
82
82
  .checkout-btn:hover:not(:disabled) {
@@ -123,7 +123,7 @@
123
123
  .cart-item {
124
124
  display: flex;
125
125
  flex-direction: row;
126
- gap: var(--spacing-md);
126
+ gap: 1rem;
127
127
  }
128
128
  .cart-item-image {
129
129
  flex-shrink: 0;
@@ -171,8 +171,8 @@
171
171
  .cart-item-qty {
172
172
  display: inline-flex;
173
173
  align-items: center;
174
- gap: var(--spacing-sm);
175
- margin-top: var(--spacing-sm);
174
+ gap: 0.75rem;
175
+ margin-top: 0.75rem;
176
176
  width: fit-content;
177
177
  }
178
178
  .qty-btn {
@@ -221,7 +221,7 @@
221
221
  .cart-item-prices {
222
222
  display: flex;
223
223
  align-items: center;
224
- gap: var(--spacing-sm);
224
+ gap: 0.75rem;
225
225
  }
226
226
  .cart-item-full-price {
227
227
  font-family: var(--font-body);
@@ -258,10 +258,10 @@
258
258
  display: flex;
259
259
  align-items: baseline;
260
260
  justify-content: space-between;
261
- gap: var(--spacing-sm);
261
+ gap: 0.75rem;
262
262
  font-family: var(--font-body);
263
263
  font-size: 0.875rem;
264
- margin-top: var(--spacing-sm);
264
+ margin-top: 0.75rem;
265
265
  }
266
266
  .cart-discount-name {
267
267
  font-size: 0.75rem;
@@ -272,8 +272,8 @@
272
272
  }
273
273
  .cart-total {
274
274
  border-top: 1px solid var(--color-rule);
275
- padding-top: var(--spacing-md);
276
- margin-top: var(--spacing-sm);
275
+ padding-top: 1rem;
276
+ margin-top: 0.75rem;
277
277
  font-family: var(--font-display);
278
278
  font-size: 1.25rem;
279
279
  font-weight: 500;
@@ -290,10 +290,10 @@
290
290
  display: flex;
291
291
  align-items: baseline;
292
292
  justify-content: space-between;
293
- gap: var(--spacing-sm);
293
+ gap: 0.75rem;
294
294
  font-family: var(--font-body);
295
295
  font-size: 0.875rem;
296
- margin-top: var(--spacing-sm);
296
+ margin-top: 0.75rem;
297
297
  }
298
298
  .cart-applied-discounts-name {
299
299
  text-align: right;
@@ -5,12 +5,12 @@ const orderId = Astro.url.searchParams.get("orderId");
5
5
  ---
6
6
 
7
7
  <Layout title="Thank You">
8
- <main class="py-4xl">
9
- <div class="max-w-prose mx-auto px-xl text-center flex flex-col gap-lg">
8
+ <main class="py-24">
9
+ <div class="max-w-prose mx-auto px-8 text-center flex flex-col gap-6">
10
10
  <h1 class="font-display text-5xl">Thank You!</h1>
11
11
  <p class="text-ink-soft">Your order has been placed successfully.</p>
12
12
  {orderId && <p class="text-mute text-sm tracking-wide">Order ID: {orderId}</p>}
13
- <a href="/products" class="btn-primary self-center mt-md">Continue Shopping</a>
13
+ <a href="/products" class="btn-primary self-center mt-4">Continue Shopping</a>
14
14
  </div>
15
15
  </main>
16
16
  </Layout>
@@ -18,8 +18,8 @@
18
18
  during adaptation; keep the structure as-is.
19
19
  */}
20
20
  {giftCardProduct && (
21
- <section class="py-3xl md:py-4xl">
22
- <a href="/gift-cards" class="max-w-wide mx-auto px-xl grid grid-cols-1 md:grid-cols-2 gap-xl md:gap-3xl items-center group">
21
+ <section class="py-16 md:py-24">
22
+ <a href="/gift-cards" class="max-w-wide mx-auto px-8 grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center group">
23
23
  <div class="aspect-[4/3] bg-cream rounded-sm overflow-hidden">
24
24
  {giftCardImage && (
25
25
  <img
@@ -31,11 +31,11 @@
31
31
  />
32
32
  )}
33
33
  </div>
34
- <div class="flex flex-col gap-md">
34
+ <div class="flex flex-col gap-4">
35
35
  <p class="eyebrow">A gift, considered</p>
36
36
  <h2 class="font-display text-3xl leading-tight">{giftCardProduct.name}</h2>
37
37
  <p class="text-ink-soft leading-relaxed">{giftCardProduct.description}</p>
38
- <span class="text-sm tracking-wide uppercase mt-sm">Send a gift card →</span>
38
+ <span class="text-sm tracking-wide uppercase mt-3">Send a gift card →</span>
39
39
  </div>
40
40
  </a>
41
41
  </section>