@sankhyatronics/sankhya-cms 0.2.0 → 0.3.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 (209) hide show
  1. package/custom-elements.json +10968 -3080
  2. package/dist/attention-panel/index.js +2 -2
  3. package/dist/bar-chart/index.js +2 -2
  4. package/dist/bento-grid/index.d.ts +25 -0
  5. package/dist/bento-grid/index.js +4 -4
  6. package/dist/booking/index.d.ts +309 -0
  7. package/dist/booking/index.js +20 -0
  8. package/dist/booking/index.js.map +1 -0
  9. package/dist/card/index.js +2 -2
  10. package/dist/carousel/index.d.ts +20 -6
  11. package/dist/carousel/index.js +3 -3
  12. package/dist/chip/index.js +2 -2
  13. package/dist/{chunk-OLA35A2O.js → chunk-2GTR37PU.js} +2 -2
  14. package/dist/{chunk-7NIWQWTA.js → chunk-2HMOX253.js} +128 -25
  15. package/dist/chunk-2HMOX253.js.map +1 -0
  16. package/dist/{chunk-DH53HZPB.js → chunk-37KFBKPD.js} +32 -4
  17. package/dist/chunk-37KFBKPD.js.map +1 -0
  18. package/dist/{chunk-EE6PAN3B.js → chunk-4XPKKYVZ.js} +23 -15
  19. package/dist/chunk-4XPKKYVZ.js.map +1 -0
  20. package/dist/chunk-535QAZ6A.js +236 -0
  21. package/dist/chunk-535QAZ6A.js.map +1 -0
  22. package/dist/{chunk-AKJQ345O.js → chunk-5AWGZQWU.js} +2 -2
  23. package/dist/chunk-6OZKZG5J.js +300 -0
  24. package/dist/chunk-6OZKZG5J.js.map +1 -0
  25. package/dist/chunk-AYNT7G3J.js +805 -0
  26. package/dist/chunk-AYNT7G3J.js.map +1 -0
  27. package/dist/chunk-BEWX3E6P.js +32 -0
  28. package/dist/chunk-BEWX3E6P.js.map +1 -0
  29. package/dist/chunk-CZUGPKXV.js +275 -0
  30. package/dist/chunk-CZUGPKXV.js.map +1 -0
  31. package/dist/{chunk-2DXHPEB5.js → chunk-DJFWZOZM.js} +107 -11
  32. package/dist/chunk-DJFWZOZM.js.map +1 -0
  33. package/dist/chunk-DPANTC3O.js +1714 -0
  34. package/dist/chunk-DPANTC3O.js.map +1 -0
  35. package/dist/chunk-EKZFFY45.js +426 -0
  36. package/dist/chunk-EKZFFY45.js.map +1 -0
  37. package/dist/chunk-ER7MCPDV.js +7 -0
  38. package/dist/chunk-ER7MCPDV.js.map +1 -0
  39. package/dist/chunk-EW2RK5Y6.js +383 -0
  40. package/dist/chunk-EW2RK5Y6.js.map +1 -0
  41. package/dist/{chunk-BLQV2YMO.js → chunk-EZOKT7NJ.js} +2 -2
  42. package/dist/chunk-FF2LWEF3.js +386 -0
  43. package/dist/chunk-FF2LWEF3.js.map +1 -0
  44. package/dist/chunk-FOUK3NHR.js +7 -0
  45. package/dist/chunk-FOUK3NHR.js.map +1 -0
  46. package/dist/{chunk-O4FCRKDX.js → chunk-GB4NGCZI.js} +6 -3
  47. package/dist/chunk-GB4NGCZI.js.map +1 -0
  48. package/dist/{chunk-5F5PTVWR.js → chunk-GOFSMD6W.js} +9 -10
  49. package/dist/chunk-GOFSMD6W.js.map +1 -0
  50. package/dist/{chunk-6D6ROU7X.js → chunk-IGMVF3KK.js} +2 -2
  51. package/dist/{chunk-INMSEYT6.js → chunk-IVPVYVWG.js} +2 -2
  52. package/dist/{chunk-ZRC5BVDX.js → chunk-JURCF4OD.js} +174 -12
  53. package/dist/chunk-JURCF4OD.js.map +1 -0
  54. package/dist/{chunk-FZ36KL73.js → chunk-KZ7PMFWS.js} +11 -1
  55. package/dist/chunk-KZ7PMFWS.js.map +1 -0
  56. package/dist/{chunk-EMCQBT3S.js → chunk-LO6TZBGD.js} +129 -35
  57. package/dist/chunk-LO6TZBGD.js.map +1 -0
  58. package/dist/chunk-MFRGAT6D.js +860 -0
  59. package/dist/chunk-MFRGAT6D.js.map +1 -0
  60. package/dist/chunk-MIBVQXRU.js +1098 -0
  61. package/dist/chunk-MIBVQXRU.js.map +1 -0
  62. package/dist/{chunk-35TX3AOA.js → chunk-NABSRUXG.js} +2 -2
  63. package/dist/{chunk-2FJDNJ7R.js → chunk-NXKDZWS7.js} +44 -6
  64. package/dist/chunk-NXKDZWS7.js.map +1 -0
  65. package/dist/{chunk-KMU6ABQF.js → chunk-NXUS2CH7.js} +14 -1
  66. package/dist/chunk-NXUS2CH7.js.map +1 -0
  67. package/dist/{chunk-GJO3RGOB.js → chunk-OCPGOLVF.js} +56 -11
  68. package/dist/chunk-OCPGOLVF.js.map +1 -0
  69. package/dist/chunk-OEOE6IFY.js +628 -0
  70. package/dist/chunk-OEOE6IFY.js.map +1 -0
  71. package/dist/{chunk-OELI3IWC.js → chunk-PV6OCXXN.js} +2 -2
  72. package/dist/{chunk-5C6C6K5U.js → chunk-Q66CJO3P.js} +200 -12
  73. package/dist/chunk-Q66CJO3P.js.map +1 -0
  74. package/dist/{chunk-MG2VTZA4.js → chunk-QS3ANOWN.js} +2 -2
  75. package/dist/{chunk-3OEEOBHB.js → chunk-QSJ543V2.js} +2 -2
  76. package/dist/{chunk-FML6AG2V.js → chunk-RANC3OI6.js} +2 -2
  77. package/dist/chunk-T4BIN37Q.js +32 -0
  78. package/dist/chunk-T4BIN37Q.js.map +1 -0
  79. package/dist/{chunk-YVTFQHVY.js → chunk-TEAZZPDQ.js} +2 -2
  80. package/dist/chunk-TJK55CJ2.js +49 -0
  81. package/dist/chunk-TJK55CJ2.js.map +1 -0
  82. package/dist/chunk-UKIVLF7S.js +32 -0
  83. package/dist/chunk-UKIVLF7S.js.map +1 -0
  84. package/dist/chunk-V4R7PYP5.js +905 -0
  85. package/dist/chunk-V4R7PYP5.js.map +1 -0
  86. package/dist/{chunk-AG2COC64.js → chunk-VVYQ2DDS.js} +2 -2
  87. package/dist/chunk-WAMOAAMN.js +16 -0
  88. package/dist/chunk-WAMOAAMN.js.map +1 -0
  89. package/dist/chunk-WAYHHL4J.js +1234 -0
  90. package/dist/chunk-WAYHHL4J.js.map +1 -0
  91. package/dist/{chunk-PHEGSCOM.js → chunk-WH6HE6SY.js} +10 -4
  92. package/dist/chunk-WH6HE6SY.js.map +1 -0
  93. package/dist/{chunk-5MY6OWMK.js → chunk-XP6DI3VY.js} +59 -4
  94. package/dist/chunk-XP6DI3VY.js.map +1 -0
  95. package/dist/{chunk-QLA4BH2B.js → chunk-XS6LNTSH.js} +73 -11
  96. package/dist/chunk-XS6LNTSH.js.map +1 -0
  97. package/dist/{chunk-WT4OATGP.js → chunk-ZKFCDVB4.js} +16 -7
  98. package/dist/chunk-ZKFCDVB4.js.map +1 -0
  99. package/dist/cms-theme.css +22 -0
  100. package/dist/contact-form/index.d.ts +220 -0
  101. package/dist/contact-form/index.js +16 -0
  102. package/dist/contact-form/index.js.map +1 -0
  103. package/dist/content-block/index.d.ts +39 -3
  104. package/dist/content-block/index.js +8 -5
  105. package/dist/cookie-consent/index.d.ts +60 -12
  106. package/dist/cookie-consent/index.js +3 -2
  107. package/dist/date-tile/index.js +2 -2
  108. package/dist/dropdown/index.d.ts +22 -1
  109. package/dist/dropdown/index.js +4 -2
  110. package/dist/feature-split/index.d.ts +6 -1
  111. package/dist/feature-split/index.js +5 -5
  112. package/dist/features-section/index.d.ts +49 -5
  113. package/dist/features-section/index.js +6 -6
  114. package/dist/flow/index.js +2 -2
  115. package/dist/footer/index.d.ts +13 -0
  116. package/dist/footer/index.js +2 -2
  117. package/dist/header/index.d.ts +127 -1
  118. package/dist/header/index.js +3 -2
  119. package/dist/hero/index.d.ts +75 -6
  120. package/dist/hero/index.js +6 -3
  121. package/dist/icon-button/index.d.ts +5 -3
  122. package/dist/icon-button/index.js +2 -2
  123. package/dist/index.d.ts +9 -3
  124. package/dist/index.js +118 -57
  125. package/dist/items-accordion/index.d.ts +5 -1
  126. package/dist/items-accordion/index.js +4 -4
  127. package/dist/kpi-card/index.js +2 -2
  128. package/dist/markdown/index.d.ts +18 -0
  129. package/dist/markdown/index.js +3 -3
  130. package/dist/mega-menu/index.d.ts +95 -7
  131. package/dist/mega-menu/index.js +7 -2
  132. package/dist/menu-grid/index.js +2 -2
  133. package/dist/menu-grid-item/index.js +2 -2
  134. package/dist/menu-item/index.d.ts +6 -0
  135. package/dist/menu-item/index.js +3 -2
  136. package/dist/plan-cards/index.d.ts +69 -0
  137. package/dist/plan-cards/index.js +13 -0
  138. package/dist/plan-cards/index.js.map +1 -0
  139. package/dist/product-nav/index.d.ts +127 -0
  140. package/dist/product-nav/index.js +11 -0
  141. package/dist/product-nav/index.js.map +1 -0
  142. package/dist/promo-banner/index.d.ts +34 -7
  143. package/dist/promo-banner/index.js +6 -3
  144. package/dist/quote-dialog/index.d.ts +174 -0
  145. package/dist/quote-dialog/index.js +22 -0
  146. package/dist/quote-dialog/index.js.map +1 -0
  147. package/dist/schema/index.d.ts +8 -0
  148. package/dist/schema/index.js +6 -0
  149. package/dist/schema/index.js.map +1 -1
  150. package/dist/screenshot-tour/index.d.ts +54 -0
  151. package/dist/screenshot-tour/index.js +9 -0
  152. package/dist/screenshot-tour/index.js.map +1 -0
  153. package/dist/section/index.js +3 -3
  154. package/dist/segmented-control/index.js +2 -2
  155. package/dist/select/index.js +2 -2
  156. package/dist/shared/styles.d.ts +7 -1
  157. package/dist/shared/styles.js +3 -1
  158. package/dist/stats/index.js +2 -2
  159. package/dist/steps/index.d.ts +12 -0
  160. package/dist/steps/index.js +4 -3
  161. package/dist/tag-strip/index.d.ts +7 -0
  162. package/dist/tag-strip/index.js +2 -2
  163. package/dist/testimonials/index.d.ts +9 -1
  164. package/dist/testimonials/index.js +3 -3
  165. package/dist/tooltip/index.js +2 -2
  166. package/package.json +26 -2
  167. package/dist/chunk-2DXHPEB5.js.map +0 -1
  168. package/dist/chunk-2FJDNJ7R.js.map +0 -1
  169. package/dist/chunk-4AFOKKSU.js +0 -158
  170. package/dist/chunk-4AFOKKSU.js.map +0 -1
  171. package/dist/chunk-5C6C6K5U.js.map +0 -1
  172. package/dist/chunk-5F5PTVWR.js.map +0 -1
  173. package/dist/chunk-5MY6OWMK.js.map +0 -1
  174. package/dist/chunk-7NIWQWTA.js.map +0 -1
  175. package/dist/chunk-DH53HZPB.js.map +0 -1
  176. package/dist/chunk-EE6PAN3B.js.map +0 -1
  177. package/dist/chunk-EMCQBT3S.js.map +0 -1
  178. package/dist/chunk-FZ36KL73.js.map +0 -1
  179. package/dist/chunk-GJO3RGOB.js.map +0 -1
  180. package/dist/chunk-HMUZON6I.js +0 -202
  181. package/dist/chunk-HMUZON6I.js.map +0 -1
  182. package/dist/chunk-J6EO4R73.js +0 -633
  183. package/dist/chunk-J6EO4R73.js.map +0 -1
  184. package/dist/chunk-KMU6ABQF.js.map +0 -1
  185. package/dist/chunk-MSSH3HFK.js +0 -205
  186. package/dist/chunk-MSSH3HFK.js.map +0 -1
  187. package/dist/chunk-O4FCRKDX.js.map +0 -1
  188. package/dist/chunk-PHEGSCOM.js.map +0 -1
  189. package/dist/chunk-QLA4BH2B.js.map +0 -1
  190. package/dist/chunk-SQ3OZO32.js +0 -486
  191. package/dist/chunk-SQ3OZO32.js.map +0 -1
  192. package/dist/chunk-UAIUBPDN.js +0 -11
  193. package/dist/chunk-UAIUBPDN.js.map +0 -1
  194. package/dist/chunk-WT4OATGP.js.map +0 -1
  195. package/dist/chunk-XUGEXERO.js +0 -708
  196. package/dist/chunk-XUGEXERO.js.map +0 -1
  197. package/dist/chunk-ZRC5BVDX.js.map +0 -1
  198. /package/dist/{chunk-OLA35A2O.js.map → chunk-2GTR37PU.js.map} +0 -0
  199. /package/dist/{chunk-AKJQ345O.js.map → chunk-5AWGZQWU.js.map} +0 -0
  200. /package/dist/{chunk-BLQV2YMO.js.map → chunk-EZOKT7NJ.js.map} +0 -0
  201. /package/dist/{chunk-6D6ROU7X.js.map → chunk-IGMVF3KK.js.map} +0 -0
  202. /package/dist/{chunk-INMSEYT6.js.map → chunk-IVPVYVWG.js.map} +0 -0
  203. /package/dist/{chunk-35TX3AOA.js.map → chunk-NABSRUXG.js.map} +0 -0
  204. /package/dist/{chunk-OELI3IWC.js.map → chunk-PV6OCXXN.js.map} +0 -0
  205. /package/dist/{chunk-MG2VTZA4.js.map → chunk-QS3ANOWN.js.map} +0 -0
  206. /package/dist/{chunk-3OEEOBHB.js.map → chunk-QSJ543V2.js.map} +0 -0
  207. /package/dist/{chunk-FML6AG2V.js.map → chunk-RANC3OI6.js.map} +0 -0
  208. /package/dist/{chunk-YVTFQHVY.js.map → chunk-TEAZZPDQ.js.map} +0 -0
  209. /package/dist/{chunk-AG2COC64.js.map → chunk-VVYQ2DDS.js.map} +0 -0
@@ -0,0 +1,1714 @@
1
+ import {
2
+ codePointLength,
3
+ fillTemplate,
4
+ parseRetryAfter
5
+ } from "./chunk-CZUGPKXV.js";
6
+ import {
7
+ controlBaseStyles,
8
+ fontFamilyStyles
9
+ } from "./chunk-NXUS2CH7.js";
10
+ import {
11
+ __decorateClass
12
+ } from "./chunk-EUXUH3YW.js";
13
+
14
+ // src/booking/Booking.ts
15
+ import { LitElement, css, html, nothing, svg } from "lit";
16
+ import { customElement, property, state } from "lit/decorators.js";
17
+
18
+ // src/booking/BookingEvents.ts
19
+ var BOOKING_CREATED_EVENT = "st-booking-created";
20
+ var BOOKING_RESCHEDULED_EVENT = "st-booking-rescheduled";
21
+ var BOOKING_CANCELLED_EVENT = "st-booking-cancelled";
22
+ var BOOKING_ERROR_EVENT = "st-booking-error";
23
+
24
+ // src/booking/booking-client.ts
25
+ var BOOKING_FIELD_NAMES = ["name", "email", "phone", "notes", "consent"];
26
+ var BOOKING_LIMITS = { nameMax: 100, emailMax: 254, phoneMax: 30, notesMax: 1e3 };
27
+ var BOOKING_TIMEOUT_MS = 15e3;
28
+ var MAX_AVAILABILITY_MS = 31 * 24 * 36e5 - 36e5;
29
+ var MAX_MONTH_LOOKAHEAD = 3;
30
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
31
+ var PHONE_PATTERN = /^[0-9 +\-().]+$/;
32
+ var REFERENCE_MAX_LENGTH = 2048;
33
+ function isPlainObject(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+ function str(value) {
37
+ return typeof value === "string" ? value : "";
38
+ }
39
+ function validInstant(value) {
40
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
41
+ }
42
+ function parseServices(input) {
43
+ if (!Array.isArray(input)) return [];
44
+ const services = [];
45
+ for (const item of input) {
46
+ if (!isPlainObject(item)) continue;
47
+ const id = str(item.id).trim();
48
+ const name = str(item.name).trim();
49
+ if (id === "" || name === "") continue;
50
+ const staff = [];
51
+ if (Array.isArray(item.staff)) {
52
+ for (const member of item.staff) {
53
+ if (isPlainObject(member) && str(member.id).trim() !== "" && str(member.name).trim() !== "") {
54
+ staff.push({ id: str(member.id).trim(), name: str(member.name).trim() });
55
+ }
56
+ }
57
+ }
58
+ const minutes = typeof item.durationMinutes === "number" && item.durationMinutes > 0 ? Math.round(item.durationMinutes) : 0;
59
+ services.push({ id, name, description: str(item.description).trim(), durationMinutes: minutes, online: item.online === true, staff });
60
+ }
61
+ return services;
62
+ }
63
+ function parseSlots(input) {
64
+ if (!Array.isArray(input)) return [];
65
+ const slots = [];
66
+ for (const item of input) {
67
+ if (!isPlainObject(item) || !validInstant(item.start) || !validInstant(item.end)) continue;
68
+ const staffIds = Array.isArray(item.staffIds) ? item.staffIds.filter((id) => typeof id === "string") : [];
69
+ slots.push({ start: item.start, end: item.end, staffIds });
70
+ }
71
+ return slots.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
72
+ }
73
+ function parseBooking(input) {
74
+ if (!isPlainObject(input)) return void 0;
75
+ const reference = str(input.reference).trim();
76
+ if (reference === "" || reference.length > REFERENCE_MAX_LENGTH || !validInstant(input.start) || !validInstant(input.end)) return void 0;
77
+ return { reference, serviceId: str(input.serviceId), serviceName: str(input.serviceName), start: input.start, end: input.end };
78
+ }
79
+ var DEFAULT_BOOKING_ERROR_MESSAGES = {
80
+ required: "{field} is required.",
81
+ tooLong: "{field} must be {max} characters or fewer.",
82
+ invalidEmail: "Enter a valid email address.",
83
+ invalidPhone: "Use only digits, spaces and + - ( ) . in the phone number.",
84
+ consentRequired: "Please accept to continue."
85
+ };
86
+ function validateBooking(values, labels, messages) {
87
+ const errors = {};
88
+ const name = values.name.trim();
89
+ const email = values.email.trim();
90
+ const phone = values.phone.trim();
91
+ const notes = values.notes.trim();
92
+ const required = (field) => fillTemplate(messages.required, { field: labels[field] });
93
+ const tooLong = (field, max) => fillTemplate(messages.tooLong, { field: labels[field], max });
94
+ if (name === "") errors.name = required("name");
95
+ else if (codePointLength(name) > BOOKING_LIMITS.nameMax) errors.name = tooLong("name", BOOKING_LIMITS.nameMax);
96
+ if (email === "") errors.email = required("email");
97
+ else if (codePointLength(email) > BOOKING_LIMITS.emailMax) errors.email = tooLong("email", BOOKING_LIMITS.emailMax);
98
+ else if (!EMAIL_PATTERN.test(email)) errors.email = messages.invalidEmail;
99
+ if (phone !== "") {
100
+ if (codePointLength(phone) > BOOKING_LIMITS.phoneMax) errors.phone = tooLong("phone", BOOKING_LIMITS.phoneMax);
101
+ else if (!PHONE_PATTERN.test(phone)) errors.phone = messages.invalidPhone;
102
+ }
103
+ if (codePointLength(notes) > BOOKING_LIMITS.notesMax) errors.notes = tooLong("notes", BOOKING_LIMITS.notesMax);
104
+ if (!values.consent) errors.consent = messages.consentRequired;
105
+ return errors;
106
+ }
107
+ function firstInvalidBookingField(errors) {
108
+ return BOOKING_FIELD_NAMES.find((name) => errors[name] !== void 0);
109
+ }
110
+ function buildBookingPayload(values, choice) {
111
+ if (!values.consent) throw new Error("A booking needs consent.");
112
+ const payload = {
113
+ serviceId: choice.serviceId,
114
+ start: choice.start,
115
+ name: values.name.trim(),
116
+ email: values.email.trim(),
117
+ phone: values.phone.trim(),
118
+ notes: values.notes.trim(),
119
+ consent: true,
120
+ website: values.website ?? ""
121
+ };
122
+ if (choice.staffId) payload.staffId = choice.staffId;
123
+ return payload;
124
+ }
125
+ function parseJsonObject(text) {
126
+ try {
127
+ const value = JSON.parse(text);
128
+ return isPlainObject(value) ? value : void 0;
129
+ } catch {
130
+ return void 0;
131
+ }
132
+ }
133
+ function interpretBookingResponse(status, retryAfter, bodyText, readOk) {
134
+ if (status === 429) {
135
+ const retryAfterSeconds = parseRetryAfter(retryAfter);
136
+ return retryAfterSeconds === void 0 ? { kind: "rate_limited" } : { kind: "rate_limited", retryAfterSeconds };
137
+ }
138
+ const body = parseJsonObject(bodyText);
139
+ if (!body) return { kind: "unavailable" };
140
+ if (status >= 200 && status < 300) {
141
+ if (body.ok !== true) return { kind: "unavailable" };
142
+ const data = readOk(body);
143
+ return data === void 0 ? { kind: "unavailable" } : { kind: "ok", data };
144
+ }
145
+ if (status === 400 && body.error === "validation" && isPlainObject(body.errors)) {
146
+ const errors = {};
147
+ for (const [key, message] of Object.entries(body.errors)) {
148
+ if (typeof message === "string" && message.trim() !== "") errors[key] = message;
149
+ }
150
+ return Object.keys(errors).length > 0 ? { kind: "validation", errors } : { kind: "unavailable" };
151
+ }
152
+ if (status === 409 && body.error === "slot_taken") return { kind: "slot_taken" };
153
+ if (status === 409 && body.error === "too_late") return { kind: "too_late" };
154
+ if (status === 404 && body.error === "not_found") return { kind: "not_found" };
155
+ return { kind: "unavailable" };
156
+ }
157
+ function isCallableBase(base) {
158
+ if (base.trim() === "") return false;
159
+ try {
160
+ const url = new URL(base);
161
+ return url.protocol === "https:" || url.protocol === "http:";
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+ function bookingUrl(base, path) {
167
+ return `${base.trim().replace(/\/+$/, "")}/public/v1/bookings${path}`;
168
+ }
169
+ function appointmentPath(reference, suffix = "") {
170
+ return `/appointments/${encodeURIComponent(reference)}${suffix}`;
171
+ }
172
+ async function callBookings(call) {
173
+ const { base, method, path, body, readOk, signal, timeoutMs = BOOKING_TIMEOUT_MS, fetchImpl = globalThis.fetch } = call;
174
+ if (!isCallableBase(base) || signal?.aborted) return { kind: "unavailable" };
175
+ const controller = new AbortController();
176
+ const abort = () => controller.abort();
177
+ const timer = setTimeout(abort, timeoutMs);
178
+ signal?.addEventListener("abort", abort, { once: true });
179
+ try {
180
+ const headers = { Accept: "application/json" };
181
+ if (body !== void 0) headers["Content-Type"] = "application/json";
182
+ const response = await fetchImpl(bookingUrl(base, path), {
183
+ method,
184
+ mode: "cors",
185
+ credentials: "omit",
186
+ cache: "no-store",
187
+ referrerPolicy: "no-referrer",
188
+ headers,
189
+ body: body === void 0 ? void 0 : JSON.stringify(body),
190
+ signal: controller.signal
191
+ });
192
+ const text = await response.text();
193
+ return interpretBookingResponse(response.status, response.headers.get("Retry-After"), text, readOk);
194
+ } catch {
195
+ return { kind: "unavailable" };
196
+ } finally {
197
+ clearTimeout(timer);
198
+ signal?.removeEventListener("abort", abort);
199
+ }
200
+ }
201
+ var readServices = (body) => parseServices(body.services);
202
+ var readSlots = (body) => parseSlots(body.slots);
203
+ var readBooking = (body) => parseBooking(body.booking);
204
+ var readNothing = () => true;
205
+ function readReferenceParam(search, hash) {
206
+ const fromQuery = new URLSearchParams(search).get("ref");
207
+ if (fromQuery?.trim()) return fromQuery.trim();
208
+ const fragment = hash.replace(/^#/, "");
209
+ const queryStart = fragment.indexOf("?");
210
+ const params = new URLSearchParams(queryStart >= 0 ? fragment.slice(queryStart + 1) : fragment);
211
+ const fromHash = params.get("ref");
212
+ return fromHash?.trim() ? fromHash.trim() : null;
213
+ }
214
+ function buildManageUrl(pageUrl, reference) {
215
+ const url = new URL(pageUrl);
216
+ url.searchParams.delete("ref");
217
+ url.hash = `ref=${encodeURIComponent(reference)}`;
218
+ return url.toString();
219
+ }
220
+ function dayKey(date) {
221
+ const month = String(date.getMonth() + 1).padStart(2, "0");
222
+ const day = String(date.getDate()).padStart(2, "0");
223
+ return `${date.getFullYear()}-${month}-${day}`;
224
+ }
225
+ function parseDayKey(key) {
226
+ const [year, month, day] = key.split("-").map(Number);
227
+ return new Date(year ?? 1970, (month ?? 1) - 1, day ?? 1);
228
+ }
229
+ function startOfMonth(date) {
230
+ return new Date(date.getFullYear(), date.getMonth(), 1);
231
+ }
232
+ function addMonths(month, count) {
233
+ return new Date(month.getFullYear(), month.getMonth() + count, 1);
234
+ }
235
+ function addDays(date, count) {
236
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + count);
237
+ }
238
+ function monthWindow(month, now) {
239
+ const first = startOfMonth(month);
240
+ const from = first.getTime() > now.getTime() ? first : now;
241
+ const end = addMonths(first, 1);
242
+ const to = new Date(Math.min(end.getTime(), from.getTime() + MAX_AVAILABILITY_MS));
243
+ return { from, to };
244
+ }
245
+ function groupSlotsByDay(slots) {
246
+ const days = /* @__PURE__ */ new Map();
247
+ for (const slot of slots) {
248
+ const key = dayKey(new Date(slot.start));
249
+ const list = days.get(key);
250
+ if (list) list.push(slot);
251
+ else days.set(key, [slot]);
252
+ }
253
+ return days;
254
+ }
255
+ function monthCells(month, weekStart) {
256
+ const first = startOfMonth(month);
257
+ const lead = (first.getDay() - weekStart + 7) % 7;
258
+ const count = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
259
+ const cells = Array.from({ length: lead }, () => null);
260
+ for (let day = 1; day <= count; day += 1) cells.push(new Date(first.getFullYear(), first.getMonth(), day));
261
+ while (cells.length % 7 !== 0) cells.push(null);
262
+ return cells;
263
+ }
264
+ function staffIdToSend(chosen) {
265
+ return chosen === "" ? void 0 : chosen;
266
+ }
267
+
268
+ // src/booking/booking-labels.ts
269
+ var DEFAULT_BOOKING_LABELS = {
270
+ stepService: "Service",
271
+ stepTime: "Date and time",
272
+ stepDetails: "Your details",
273
+ stepDone: "Confirmation",
274
+ stepsLabel: "Booking steps",
275
+ serviceHeading: "Choose a service",
276
+ staffLegend: "Who would you like to see?",
277
+ anyStaff: "Any available",
278
+ duration: "{minutes} min",
279
+ online: "Online meeting",
280
+ noServices: "No services can be booked online right now. Please try again later.",
281
+ timeHeading: "Pick a date and time",
282
+ rescheduleHeading: "Pick a new date and time",
283
+ previousMonth: "Previous month",
284
+ nextMonth: "Next month",
285
+ timesHeading: "Times on {date}",
286
+ timeZoneNote: "Times are shown in your time zone: {zone}.",
287
+ chooseDay: "Choose a date to see the available times.",
288
+ slotsAvailable: "{count} times available on {date}.",
289
+ noSlots: "No times are available in {month}.",
290
+ loading: "Loading\u2026",
291
+ back: "Back",
292
+ continue: "Continue",
293
+ confirmReschedule: "Confirm new time",
294
+ detailsHeading: "Your details",
295
+ nameLabel: "Full name",
296
+ namePlaceholder: "Your name",
297
+ emailLabel: "Email",
298
+ emailPlaceholder: "you@example.com",
299
+ phoneLabel: "Phone (optional)",
300
+ notesLabel: "Notes (optional)",
301
+ notesPlaceholder: "Anything we should know beforehand",
302
+ notesCounter: "{count} / {max}",
303
+ consentLabel: "I agree that these details may be used to arrange and confirm my appointment.",
304
+ requiredLabel: "required",
305
+ requiredNote: "* Required",
306
+ submit: "Book appointment",
307
+ sending: "Booking\u2026",
308
+ bookedHeading: "Your appointment is booked",
309
+ bookingHeading: "Your appointment",
310
+ bookedNote: "Your appointment is booked.",
311
+ rescheduledNote: "Your appointment has been moved to the new time.",
312
+ serviceLabel: "Service",
313
+ whenLabel: "When",
314
+ manageLinkLabel: "Link to manage this appointment",
315
+ manageLinkHint: "Keep this link private. Anyone who has it can change or cancel your appointment.",
316
+ copyLink: "Copy link",
317
+ linkCopied: "Link copied.",
318
+ reschedule: "Reschedule",
319
+ cancel: "Cancel appointment",
320
+ cancelConfirm: "Cancel this appointment?",
321
+ cancelYes: "Yes, cancel it",
322
+ cancelNo: "Keep it",
323
+ cancelledHeading: "Your appointment is cancelled",
324
+ cancelledNote: "The appointment has been cancelled.",
325
+ bookAnother: "Book another appointment",
326
+ errorNote: "Sorry, the booking service could not be reached. Please try again later.",
327
+ retry: "Try again",
328
+ slotTakenNote: "That time has just been taken. Please choose another.",
329
+ tooLateNote: "This appointment can no longer be changed online. Please contact us.",
330
+ notFoundNote: "We could not find this appointment. It may have been cancelled, or the link is incomplete.",
331
+ rateLimitedNote: "Too many requests in a short time. Please wait {seconds} seconds and try again.",
332
+ rateLimitedGenericNote: "Too many requests in a short time. Please wait a little and try again.",
333
+ validationNote: "Please check the highlighted fields."
334
+ };
335
+
336
+ // src/booking/Booking.ts
337
+ var CHECK_ICON = svg`<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12l5 5L20 7" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;
338
+ var MAX_MONTHS_AHEAD = 12;
339
+ var Booking = class extends LitElement {
340
+ constructor() {
341
+ super(...arguments);
342
+ this.endpoint = "";
343
+ this.reference = "";
344
+ this.serviceId = "";
345
+ this.weekStart = 1;
346
+ this.locale = "";
347
+ this.manageUrl = "";
348
+ this.labels = {};
349
+ this.errorMessages = {};
350
+ this.view = "loading";
351
+ this.mode = "new";
352
+ this.services = [];
353
+ this.staffId = "";
354
+ this.month = startOfMonth(/* @__PURE__ */ new Date());
355
+ this.slots = [];
356
+ this.slotsLoading = false;
357
+ this.selectedDay = "";
358
+ this.focusDay = "";
359
+ this.banner = "";
360
+ this.confirmingCancel = false;
361
+ this.busy = false;
362
+ this.errors = {};
363
+ this.alertText = "";
364
+ this.notice = "";
365
+ this.notesLength = 0;
366
+ /** The reference in use: the property, the page URL, or the one the API just gave. Never rendered or sent anywhere but the API. */
367
+ this.activeReference = "";
368
+ this.inflight = null;
369
+ this.slotsRun = 0;
370
+ /** What the details form held when the visitor went back to change the time, so nothing typed is lost. */
371
+ this.draft = {};
372
+ }
373
+ disconnectedCallback() {
374
+ this.inflight?.abort();
375
+ this.inflight = null;
376
+ this.slotsRun += 1;
377
+ super.disconnectedCallback();
378
+ }
379
+ updated(changed) {
380
+ if (changed.has("endpoint") || changed.has("reference") || changed.has("serviceId")) void this.start();
381
+ }
382
+ /* ------------------------------------------------------------ text and formatting */
383
+ text(key, values = {}) {
384
+ const label = this.labels[key];
385
+ return fillTemplate(typeof label === "string" ? label : DEFAULT_BOOKING_LABELS[key], values);
386
+ }
387
+ messages() {
388
+ const merged = { ...DEFAULT_BOOKING_ERROR_MESSAGES };
389
+ for (const key of Object.keys(merged)) {
390
+ const override = this.errorMessages[key];
391
+ if (typeof override === "string") merged[key] = override;
392
+ }
393
+ return merged;
394
+ }
395
+ fieldLabels() {
396
+ return { name: this.text("nameLabel"), email: this.text("emailLabel"), phone: this.text("phoneLabel"), notes: this.text("notesLabel") };
397
+ }
398
+ format(options) {
399
+ const tag = this.locale || document.documentElement.lang || void 0;
400
+ try {
401
+ return new Intl.DateTimeFormat(tag, options);
402
+ } catch {
403
+ return new Intl.DateTimeFormat(void 0, options);
404
+ }
405
+ }
406
+ fullDate(date) {
407
+ return this.format({ weekday: "long", year: "numeric", month: "long", day: "numeric" }).format(date);
408
+ }
409
+ timeOfDay(iso) {
410
+ return this.format({ hour: "numeric", minute: "2-digit" }).format(new Date(iso));
411
+ }
412
+ whenText(iso) {
413
+ return this.format({ weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }).format(new Date(iso));
414
+ }
415
+ monthName(month) {
416
+ return this.format({ month: "long", year: "numeric" }).format(month);
417
+ }
418
+ zoneName() {
419
+ return new Intl.DateTimeFormat().resolvedOptions().timeZone;
420
+ }
421
+ /* ------------------------------------------------------------ flow */
422
+ resetState() {
423
+ this.service = void 0;
424
+ this.staffId = "";
425
+ this.slots = [];
426
+ this.selectedDay = "";
427
+ this.focusDay = "";
428
+ this.selectedSlot = void 0;
429
+ this.booking = void 0;
430
+ this.banner = "";
431
+ this.confirmingCancel = false;
432
+ this.busy = false;
433
+ this.errors = {};
434
+ this.alertText = "";
435
+ this.notice = "";
436
+ this.notesLength = 0;
437
+ this.draft = {};
438
+ this.mode = "new";
439
+ }
440
+ /** Opens an existing booking when there is a reference, else starts at the services. */
441
+ async start() {
442
+ this.inflight?.abort();
443
+ this.slotsRun += 1;
444
+ const controller = new AbortController();
445
+ this.inflight = controller;
446
+ this.resetState();
447
+ this.view = "loading";
448
+ this.activeReference = this.reference.trim() || readReferenceParam(window.location.search, window.location.hash) || "";
449
+ if (this.activeReference !== "") await this.loadBooking(controller);
450
+ else await this.loadServices(controller);
451
+ }
452
+ async loadServices(controller) {
453
+ const outcome = await callBookings({ base: this.endpoint, method: "GET", path: "/services", readOk: readServices, signal: controller.signal });
454
+ if (this.inflight !== controller) return;
455
+ if (outcome.kind !== "ok") {
456
+ this.fail(outcome.kind, outcome, "fatal");
457
+ return;
458
+ }
459
+ this.services = outcome.data;
460
+ const preset = outcome.data.find((item) => item.id === this.serviceId.trim());
461
+ if (preset) {
462
+ await this.chooseService(preset);
463
+ return;
464
+ }
465
+ this.go("services");
466
+ }
467
+ async loadBooking(controller) {
468
+ const outcome = await callBookings({
469
+ base: this.endpoint,
470
+ method: "GET",
471
+ path: appointmentPath(this.activeReference),
472
+ readOk: readBooking,
473
+ signal: controller.signal
474
+ });
475
+ if (this.inflight !== controller) return;
476
+ if (outcome.kind !== "ok") {
477
+ this.fail(outcome.kind, outcome, outcome.kind === "not_found" ? "notfound" : "fatal");
478
+ return;
479
+ }
480
+ this.booking = outcome.data;
481
+ this.go("booking");
482
+ }
483
+ /** Switches the step, clears the last problem and moves focus to the new step's heading. */
484
+ go(view) {
485
+ this.view = view;
486
+ this.alertText = "";
487
+ void this.updateComplete.then(() => this.renderRoot.querySelector("#bk-heading")?.focus());
488
+ }
489
+ /** Shows the note for a failed call and reports it. `next` is the step to show (when it should change). */
490
+ fail(kind, outcome, next) {
491
+ this.busy = false;
492
+ if (next) {
493
+ this.view = next;
494
+ void this.updateComplete.then(() => this.renderRoot.querySelector("#bk-heading")?.focus());
495
+ }
496
+ switch (kind) {
497
+ case "slot_taken":
498
+ this.alertText = this.text("slotTakenNote");
499
+ break;
500
+ case "too_late":
501
+ this.alertText = this.text("tooLateNote");
502
+ break;
503
+ case "not_found":
504
+ this.alertText = this.text("notFoundNote");
505
+ break;
506
+ case "rate_limited": {
507
+ const seconds = outcome.kind === "rate_limited" ? outcome.retryAfterSeconds : void 0;
508
+ this.alertText = seconds === void 0 ? this.text("rateLimitedGenericNote") : this.text("rateLimitedNote", { seconds });
509
+ break;
510
+ }
511
+ case "validation":
512
+ this.alertText = this.text("validationNote");
513
+ break;
514
+ case "unavailable":
515
+ this.alertText = this.text("errorNote");
516
+ break;
517
+ }
518
+ this.dispatchEvent(
519
+ new CustomEvent(BOOKING_ERROR_EVENT, { detail: { kind }, bubbles: true, composed: true })
520
+ );
521
+ }
522
+ async chooseService(service) {
523
+ this.service = service;
524
+ this.staffId = "";
525
+ this.mode = "new";
526
+ this.selectedSlot = void 0;
527
+ this.selectedDay = "";
528
+ this.month = startOfMonth(/* @__PURE__ */ new Date());
529
+ this.go("time");
530
+ await this.loadSlots(true);
531
+ }
532
+ /** Loads the current month's times. With `advance`, an empty month moves on to the next, up to a few months. */
533
+ async loadSlots(advance) {
534
+ const service = this.service;
535
+ if (!service) return;
536
+ const run = this.slotsRun += 1;
537
+ this.slotsLoading = true;
538
+ this.slots = [];
539
+ let lookahead = 0;
540
+ for (; ; ) {
541
+ const { from, to } = monthWindow(this.month, /* @__PURE__ */ new Date());
542
+ const query = new URLSearchParams({ serviceId: service.id, from: from.toISOString(), to: to.toISOString() });
543
+ const staff = staffIdToSend(this.staffId);
544
+ if (staff) query.set("staffId", staff);
545
+ const outcome = await callBookings({
546
+ base: this.endpoint,
547
+ method: "GET",
548
+ path: `/availability?${query.toString()}`,
549
+ readOk: readSlots,
550
+ signal: this.inflight?.signal
551
+ });
552
+ if (run !== this.slotsRun || !this.isConnected) return;
553
+ if (outcome.kind !== "ok") {
554
+ this.slotsLoading = false;
555
+ this.fail(outcome.kind, outcome);
556
+ return;
557
+ }
558
+ if (outcome.data.length === 0 && advance && lookahead < MAX_MONTH_LOOKAHEAD) {
559
+ lookahead += 1;
560
+ this.month = addMonths(this.month, 1);
561
+ continue;
562
+ }
563
+ this.slots = outcome.data;
564
+ break;
565
+ }
566
+ this.slotsLoading = false;
567
+ const days = groupSlotsByDay(this.slots);
568
+ const first = [...days.keys()][0];
569
+ this.focusDay = first ?? dayKey(this.month);
570
+ this.notice = this.slots.length === 0 ? this.text("noSlots", { month: this.monthName(this.month) }) : "";
571
+ }
572
+ async shiftMonth(count) {
573
+ const next = addMonths(this.month, count);
574
+ const nowMonth = startOfMonth(/* @__PURE__ */ new Date());
575
+ if (next.getTime() < nowMonth.getTime() || next.getTime() > addMonths(nowMonth, MAX_MONTHS_AHEAD).getTime()) return;
576
+ this.month = next;
577
+ this.selectedDay = "";
578
+ this.selectedSlot = void 0;
579
+ await this.loadSlots(false);
580
+ }
581
+ onStaffChange(event) {
582
+ this.staffId = event.target.value;
583
+ this.selectedDay = "";
584
+ this.selectedSlot = void 0;
585
+ this.alertText = "";
586
+ void this.loadSlots(false);
587
+ }
588
+ pickDay(key) {
589
+ const days = groupSlotsByDay(this.slots);
590
+ this.focusDay = key;
591
+ if (!days.has(key)) return;
592
+ this.selectedDay = key;
593
+ this.selectedSlot = void 0;
594
+ this.alertText = "";
595
+ const count = days.get(key)?.length ?? 0;
596
+ this.notice = this.text("slotsAvailable", { count, date: this.fullDate(parseDayKey(key)) });
597
+ }
598
+ pickSlot(slot) {
599
+ this.selectedSlot = slot;
600
+ this.alertText = "";
601
+ }
602
+ onGridKeydown(event) {
603
+ const key = event.target.dataset.day;
604
+ if (!key) return;
605
+ const current = parseDayKey(key);
606
+ const moves = {
607
+ ArrowLeft: addDays(current, -1),
608
+ ArrowRight: addDays(current, 1),
609
+ ArrowUp: addDays(current, -7),
610
+ ArrowDown: addDays(current, 7),
611
+ Home: addDays(current, -((current.getDay() - this.weekStart + 7) % 7)),
612
+ End: addDays(current, 6 - (current.getDay() - this.weekStart + 7) % 7),
613
+ PageUp: new Date(current.getFullYear(), current.getMonth() - 1, current.getDate()),
614
+ PageDown: new Date(current.getFullYear(), current.getMonth() + 1, current.getDate())
615
+ };
616
+ const target = moves[event.key];
617
+ if (!target) return;
618
+ event.preventDefault();
619
+ const targetKey = dayKey(target);
620
+ const sameMonth = target.getMonth() === this.month.getMonth() && target.getFullYear() === this.month.getFullYear();
621
+ this.focusDay = targetKey;
622
+ if (!sameMonth) void this.shiftMonth(this.monthDelta(target)).then(() => this.focusDayButton(targetKey));
623
+ else this.focusDayButton(targetKey);
624
+ }
625
+ monthDelta(target) {
626
+ return (target.getFullYear() - this.month.getFullYear()) * 12 + target.getMonth() - this.month.getMonth();
627
+ }
628
+ focusDayButton(key) {
629
+ this.focusDay = key;
630
+ void this.updateComplete.then(() => this.renderRoot.querySelector(`[data-day="${key}"]`)?.focus());
631
+ }
632
+ continueFromTime() {
633
+ if (!this.selectedSlot) return;
634
+ if (this.mode === "reschedule") void this.reschedule(this.selectedSlot);
635
+ else this.go("details");
636
+ }
637
+ backFromTime() {
638
+ if (this.mode === "reschedule") this.go("booking");
639
+ else if (this.services.length > 0) this.go("services");
640
+ }
641
+ /* ------------------------------------------------------------ calls */
642
+ async book(event) {
643
+ event.preventDefault();
644
+ const slot = this.selectedSlot;
645
+ const service = this.service;
646
+ if (this.busy || !slot || !service) return;
647
+ const form = event.currentTarget;
648
+ const values = this.readValues(form);
649
+ this.draft = { name: values.name, email: values.email, phone: values.phone, notes: values.notes, consent: values.consent };
650
+ this.alertText = "";
651
+ const errors = validateBooking(values, this.fieldLabels(), this.messages());
652
+ this.errors = errors;
653
+ const invalid = firstInvalidBookingField(errors);
654
+ if (invalid) {
655
+ this.focusField(invalid);
656
+ return;
657
+ }
658
+ const payload = buildBookingPayload(values, { serviceId: service.id, staffId: staffIdToSend(this.staffId), start: slot.start });
659
+ this.busy = true;
660
+ const controller = this.freshController();
661
+ const outcome = await callBookings({ base: this.endpoint, method: "POST", path: "/appointments", body: payload, readOk: readBooking, signal: controller.signal });
662
+ if (this.inflight !== controller) return;
663
+ this.busy = false;
664
+ if (outcome.kind === "ok") {
665
+ this.booking = outcome.data;
666
+ this.activeReference = outcome.data.reference;
667
+ this.banner = "booked";
668
+ this.draft = {};
669
+ this.errors = {};
670
+ this.go("booking");
671
+ this.dispatchEvent(
672
+ new CustomEvent(BOOKING_CREATED_EVENT, { detail: { serviceId: outcome.data.serviceId || service.id, start: outcome.data.start }, bubbles: true, composed: true })
673
+ );
674
+ return;
675
+ }
676
+ if (outcome.kind === "validation") {
677
+ this.applyServerErrors(outcome.errors);
678
+ this.fail("validation", outcome);
679
+ return;
680
+ }
681
+ if (outcome.kind === "slot_taken") {
682
+ this.selectedSlot = void 0;
683
+ this.fail("slot_taken", outcome, "time");
684
+ void this.loadSlots(false).then(() => {
685
+ this.alertText = this.text("slotTakenNote");
686
+ });
687
+ return;
688
+ }
689
+ this.fail(outcome.kind, outcome);
690
+ }
691
+ /** Server messages go on the fields that exist; a rejected start or staff means the time must be picked again. */
692
+ applyServerErrors(errors) {
693
+ const shown = {};
694
+ const rest = [];
695
+ const names = BOOKING_FIELD_NAMES;
696
+ for (const [key, message] of Object.entries(errors)) {
697
+ if (names.includes(key)) shown[key] = message;
698
+ else rest.push(message);
699
+ }
700
+ this.errors = shown;
701
+ const invalid = firstInvalidBookingField(shown);
702
+ if (invalid) this.focusField(invalid);
703
+ if (rest.length > 0) {
704
+ this.selectedSlot = void 0;
705
+ void this.loadSlots(false);
706
+ this.go("time");
707
+ this.alertText = rest.join(" ");
708
+ }
709
+ }
710
+ async reschedule(slot) {
711
+ const booking = this.booking;
712
+ if (this.busy || !booking) return;
713
+ this.busy = true;
714
+ const controller = this.freshController();
715
+ const outcome = await callBookings({
716
+ base: this.endpoint,
717
+ method: "POST",
718
+ path: appointmentPath(this.activeReference, "/reschedule"),
719
+ body: { start: slot.start },
720
+ readOk: readBooking,
721
+ signal: controller.signal
722
+ });
723
+ if (this.inflight !== controller) return;
724
+ this.busy = false;
725
+ if (outcome.kind === "ok") {
726
+ this.booking = outcome.data;
727
+ this.activeReference = outcome.data.reference;
728
+ this.banner = "rescheduled";
729
+ this.mode = "new";
730
+ this.go("booking");
731
+ this.dispatchEvent(
732
+ new CustomEvent(BOOKING_RESCHEDULED_EVENT, { detail: { serviceId: outcome.data.serviceId || booking.serviceId, start: outcome.data.start }, bubbles: true, composed: true })
733
+ );
734
+ return;
735
+ }
736
+ if (outcome.kind === "slot_taken") {
737
+ this.selectedSlot = void 0;
738
+ this.fail("slot_taken", outcome);
739
+ void this.loadSlots(false).then(() => {
740
+ this.alertText = this.text("slotTakenNote");
741
+ });
742
+ return;
743
+ }
744
+ if (outcome.kind === "too_late") {
745
+ this.mode = "new";
746
+ this.fail("too_late", outcome, "booking");
747
+ return;
748
+ }
749
+ this.fail(outcome.kind, outcome, outcome.kind === "not_found" ? "notfound" : void 0);
750
+ }
751
+ startReschedule() {
752
+ const booking = this.booking;
753
+ if (!booking) return;
754
+ this.service = { id: booking.serviceId, name: booking.serviceName, description: "", durationMinutes: 0, online: false, staff: [] };
755
+ this.staffId = "";
756
+ this.mode = "reschedule";
757
+ this.selectedSlot = void 0;
758
+ this.selectedDay = "";
759
+ this.banner = "";
760
+ this.confirmingCancel = false;
761
+ this.month = startOfMonth(/* @__PURE__ */ new Date());
762
+ this.go("time");
763
+ void this.loadSlots(true);
764
+ }
765
+ async cancelBooking() {
766
+ const booking = this.booking;
767
+ if (this.busy || !booking) return;
768
+ this.busy = true;
769
+ const controller = this.freshController();
770
+ const outcome = await callBookings({ base: this.endpoint, method: "DELETE", path: appointmentPath(this.activeReference), readOk: readNothing, signal: controller.signal });
771
+ if (this.inflight !== controller) return;
772
+ this.busy = false;
773
+ this.confirmingCancel = false;
774
+ if (outcome.kind === "ok") {
775
+ this.activeReference = "";
776
+ this.banner = "";
777
+ this.go("cancelled");
778
+ this.dispatchEvent(
779
+ new CustomEvent(BOOKING_CANCELLED_EVENT, { detail: { serviceId: booking.serviceId }, bubbles: true, composed: true })
780
+ );
781
+ return;
782
+ }
783
+ this.fail(outcome.kind, outcome, outcome.kind === "not_found" ? "notfound" : void 0);
784
+ }
785
+ freshController() {
786
+ this.inflight?.abort();
787
+ const controller = new AbortController();
788
+ this.inflight = controller;
789
+ return controller;
790
+ }
791
+ bookAnother() {
792
+ this.reference = "";
793
+ this.activeReference = "";
794
+ this.inflight?.abort();
795
+ const controller = new AbortController();
796
+ this.inflight = controller;
797
+ this.resetState();
798
+ this.view = "loading";
799
+ void this.loadServices(controller);
800
+ }
801
+ async copyLink() {
802
+ const link = this.manageLink();
803
+ try {
804
+ await navigator.clipboard.writeText(link);
805
+ this.notice = this.text("linkCopied");
806
+ } catch {
807
+ const input = this.renderRoot.querySelector("#bk-link");
808
+ input?.focus();
809
+ input?.select();
810
+ }
811
+ }
812
+ manageLink() {
813
+ return buildManageUrl(this.manageUrl.trim() || window.location.href, this.activeReference);
814
+ }
815
+ /* ------------------------------------------------------------ details form */
816
+ readText(form, name) {
817
+ const field = form.elements.namedItem(name);
818
+ return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field.value : "";
819
+ }
820
+ readValues(form) {
821
+ const consent = form.elements.namedItem("consent");
822
+ return {
823
+ name: this.readText(form, "name"),
824
+ email: this.readText(form, "email"),
825
+ phone: this.readText(form, "phone"),
826
+ notes: this.readText(form, "notes"),
827
+ consent: consent instanceof HTMLInputElement && consent.checked,
828
+ website: this.readText(form, "website")
829
+ };
830
+ }
831
+ focusField(name) {
832
+ void this.updateComplete.then(() => this.renderRoot.querySelector(`#bk-${name}`)?.focus());
833
+ }
834
+ /* ------------------------------------------------------------ rendering */
835
+ heading(text) {
836
+ return html`<h2 id="bk-heading" part="heading" tabindex="-1">${text}</h2>`;
837
+ }
838
+ renderSteps() {
839
+ if (this.mode === "reschedule" || !["services", "time", "details"].includes(this.view)) return nothing;
840
+ const steps = [
841
+ { view: "services", label: this.text("stepService") },
842
+ { view: "time", label: this.text("stepTime") },
843
+ { view: "details", label: this.text("stepDetails") }
844
+ ];
845
+ return html`<ol class="steps" part="steps" aria-label=${this.text("stepsLabel")}>
846
+ ${steps.map((step) => html`<li aria-current=${step.view === this.view ? "step" : nothing}>${step.label}</li>`)}
847
+ </ol>`;
848
+ }
849
+ renderServices() {
850
+ return html`
851
+ ${this.heading(this.text("serviceHeading"))}
852
+ ${this.services.length === 0 ? html`<p class="note">${this.text("noServices")}</p>` : html`<ul class="options">
853
+ ${this.services.map(
854
+ (service) => html`<li>
855
+ <button class="service" part="service" type="button" @click=${() => void this.chooseService(service)}>
856
+ <span class="service-name">${service.name}</span>
857
+ ${service.description ? html`<span>${service.description}</span>` : nothing}
858
+ <span class="meta">
859
+ ${service.durationMinutes > 0 ? this.text("duration", { minutes: service.durationMinutes }) : nothing}${service.online ? html`${service.durationMinutes > 0 ? " \xB7 " : ""}${this.text("online")}` : nothing}
860
+ </span>
861
+ </button>
862
+ </li>`
863
+ )}
864
+ </ul>`}
865
+ `;
866
+ }
867
+ renderStaff() {
868
+ const staff = this.service?.staff ?? [];
869
+ if (this.mode === "reschedule" || staff.length < 2) return nothing;
870
+ return html`<fieldset>
871
+ <legend>${this.text("staffLegend")}</legend>
872
+ <label class="choice">
873
+ <input type="radio" name="staff" value="" .checked=${this.staffId === ""} @change=${this.onStaffChange} />
874
+ ${this.text("anyStaff")}
875
+ </label>
876
+ ${staff.map(
877
+ (member) => html`<label class="choice">
878
+ <input type="radio" name="staff" value=${member.id} .checked=${this.staffId === member.id} @change=${this.onStaffChange} />
879
+ ${member.name}
880
+ </label>`
881
+ )}
882
+ </fieldset>`;
883
+ }
884
+ renderCalendar(days) {
885
+ const first = startOfMonth(/* @__PURE__ */ new Date());
886
+ const canGoBack = this.month.getTime() > first.getTime();
887
+ const canGoForward = this.month.getTime() < addMonths(first, MAX_MONTHS_AHEAD).getTime();
888
+ const cells = monthCells(this.month, this.weekStart === 0 ? 0 : 1);
889
+ const weekdays = Array.from({ length: 7 }, (_, index) => {
890
+ const date = new Date(2021, 0, 3 + (index + this.weekStart) % 7);
891
+ return { long: this.format({ weekday: "long" }).format(date), short: this.format({ weekday: "short" }).format(date) };
892
+ });
893
+ const todayKey = dayKey(/* @__PURE__ */ new Date());
894
+ const monthKeys = cells.filter((cell) => cell !== null).map(dayKey);
895
+ const rovingKey = monthKeys.includes(this.focusDay) ? this.focusDay : monthKeys.find((key) => days.has(key)) ?? monthKeys[0];
896
+ const rows = [];
897
+ for (let index = 0; index < cells.length; index += 7) rows.push(cells.slice(index, index + 7));
898
+ return html`<div>
899
+ <div class="month-bar">
900
+ <button class="btn icon-btn" part="button" type="button" ?disabled=${!canGoBack || this.slotsLoading} aria-label=${this.text("previousMonth")} @click=${() => void this.shiftMonth(-1)}>‹</button>
901
+ <span class="month-name" id="bk-month" aria-live="polite">${this.monthName(this.month)}</span>
902
+ <button class="btn icon-btn" part="button" type="button" ?disabled=${!canGoForward || this.slotsLoading} aria-label=${this.text("nextMonth")} @click=${() => void this.shiftMonth(1)}>›</button>
903
+ </div>
904
+ <table part="calendar" role="grid" aria-labelledby="bk-month" aria-busy=${this.slotsLoading ? "true" : "false"} @keydown=${this.onGridKeydown}>
905
+ <thead>
906
+ <tr>${weekdays.map((day) => html`<th scope="col" abbr=${day.long}>${day.short}</th>`)}</tr>
907
+ </thead>
908
+ <tbody>
909
+ ${rows.map(
910
+ (row) => html`<tr>
911
+ ${row.map((cell) => {
912
+ if (!cell) return html`<td></td>`;
913
+ const key = dayKey(cell);
914
+ const available = days.has(key) && key >= todayKey;
915
+ return html`<td role="gridcell">
916
+ <button
917
+ class="day ${available ? "has-slots" : ""}"
918
+ part="day"
919
+ type="button"
920
+ data-day=${key}
921
+ tabindex=${key === rovingKey ? "0" : "-1"}
922
+ aria-disabled=${available ? "false" : "true"}
923
+ aria-pressed=${available ? key === this.selectedDay ? "true" : "false" : nothing}
924
+ aria-label=${this.fullDate(cell)}
925
+ @click=${() => this.pickDay(key)}
926
+ >
927
+ ${cell.getDate()}
928
+ </button>
929
+ </td>`;
930
+ })}
931
+ </tr>`
932
+ )}
933
+ </tbody>
934
+ </table>
935
+ </div>`;
936
+ }
937
+ renderTime() {
938
+ const days = groupSlotsByDay(this.slots);
939
+ const daySlots = days.get(this.selectedDay) ?? [];
940
+ const heading = this.mode === "reschedule" ? this.text("rescheduleHeading") : this.text("timeHeading");
941
+ const dayDate = this.selectedDay ? parseDayKey(this.selectedDay) : void 0;
942
+ return html`
943
+ ${this.heading(heading)}
944
+ ${this.service ? html`<p class="meta">${this.service.name}</p>` : nothing}
945
+ ${this.renderStaff()}
946
+ <div class="time-layout">
947
+ ${this.renderCalendar(days)}
948
+ <div>
949
+ ${this.slotsLoading ? html`<p class="loading" role="status">${this.text("loading")}</p>` : this.slots.length === 0 ? html`<p class="note">${this.text("noSlots", { month: this.monthName(this.month) })}</p>` : dayDate ? html`<h3>${this.text("timesHeading", { date: this.fullDate(dayDate) })}</h3>
950
+ <ul class="slots" aria-label=${this.text("timesHeading", { date: this.fullDate(dayDate) })}>
951
+ ${daySlots.map(
952
+ (slot) => html`<li>
953
+ <button class="slot" part="slot" type="button" aria-pressed=${slot === this.selectedSlot ? "true" : "false"} @click=${() => this.pickSlot(slot)}>
954
+ ${this.timeOfDay(slot.start)}
955
+ </button>
956
+ </li>`
957
+ )}
958
+ </ul>` : html`<p class="note">${this.text("chooseDay")}</p>`}
959
+ <p class="note">${this.text("timeZoneNote", { zone: this.zoneName() })}</p>
960
+ </div>
961
+ </div>
962
+ <div class="actions">
963
+ ${this.mode === "reschedule" || this.services.length > 0 ? html`<button class="btn" part="button" type="button" @click=${this.backFromTime}>${this.text("back")}</button>` : nothing}
964
+ <button class="btn primary" part="primary" type="button" ?disabled=${!this.selectedSlot || this.busy} @click=${this.continueFromTime}>
965
+ ${this.mode === "reschedule" ? this.text("confirmReschedule") : this.text("continue")}
966
+ </button>
967
+ </div>
968
+ `;
969
+ }
970
+ requiredMark() {
971
+ return html`<span aria-hidden="true"> *</span><span class="sr-only"> (${this.text("requiredLabel")})</span>`;
972
+ }
973
+ renderInput(name, label, options) {
974
+ const error = this.errors[name];
975
+ return html`<div class="field" part="field">
976
+ <label class="label" part="label" for="bk-${name}">${label}${options.required ? this.requiredMark() : nothing}</label>
977
+ <input
978
+ class="input"
979
+ part="input"
980
+ id="bk-${name}"
981
+ name=${name}
982
+ type=${options.type}
983
+ autocomplete=${options.autocomplete}
984
+ inputmode=${name === "phone" ? "tel" : nothing}
985
+ placeholder=${options.placeholder || nothing}
986
+ .value=${this.draft[name] ?? ""}
987
+ ?required=${options.required}
988
+ aria-invalid=${error ? "true" : "false"}
989
+ aria-describedby=${error ? `bk-${name}-error` : nothing}
990
+ />
991
+ ${error ? html`<p class="error" part="error" id="bk-${name}-error">${error}</p>` : nothing}
992
+ </div>`;
993
+ }
994
+ renderDetails() {
995
+ const slot = this.selectedSlot;
996
+ const notesError = this.errors.notes;
997
+ const consentError = this.errors.consent;
998
+ const over = this.notesLength > BOOKING_LIMITS.notesMax;
999
+ return html`
1000
+ ${this.heading(this.text("detailsHeading"))}
1001
+ ${this.service && slot ? html`<p class="meta" part="summary">${this.service.name} · ${this.whenText(slot.start)}</p>` : nothing}
1002
+ <form novalidate aria-busy=${this.busy ? "true" : "false"} @submit=${this.book} class="root">
1003
+ <p class="note">${this.text("requiredNote")}</p>
1004
+ ${this.renderInput("name", this.text("nameLabel"), { type: "text", autocomplete: "name", placeholder: this.text("namePlaceholder"), required: true })}
1005
+ ${this.renderInput("email", this.text("emailLabel"), { type: "email", autocomplete: "email", placeholder: this.text("emailPlaceholder"), required: true })}
1006
+ ${this.renderInput("phone", this.text("phoneLabel"), { type: "tel", autocomplete: "tel", required: false })}
1007
+ <div class="field" part="field">
1008
+ <label class="label" part="label" for="bk-notes">${this.text("notesLabel")}</label>
1009
+ <textarea
1010
+ class="input"
1011
+ part="input"
1012
+ id="bk-notes"
1013
+ name="notes"
1014
+ rows="4"
1015
+ placeholder=${this.text("notesPlaceholder") || nothing}
1016
+ .value=${this.draft.notes ?? ""}
1017
+ aria-invalid=${notesError ? "true" : "false"}
1018
+ aria-describedby=${notesError ? "bk-notes-error" : nothing}
1019
+ @input=${(event) => this.notesLength = codePointLength(event.target.value.trim())}
1020
+ ></textarea>
1021
+ ${this.notesLength >= BOOKING_LIMITS.notesMax * 0.8 ? html`<p class="counter ${over ? "over" : ""}">${this.text("notesCounter", { count: this.notesLength, max: BOOKING_LIMITS.notesMax })}</p>` : nothing}
1022
+ ${notesError ? html`<p class="error" part="error" id="bk-notes-error">${notesError}</p>` : nothing}
1023
+ </div>
1024
+ <div class="field" part="field">
1025
+ <label class="consent">
1026
+ <input
1027
+ type="checkbox"
1028
+ id="bk-consent"
1029
+ name="consent"
1030
+ required
1031
+ .checked=${this.draft.consent === true}
1032
+ aria-invalid=${consentError ? "true" : "false"}
1033
+ aria-describedby=${consentError ? "bk-consent-error" : nothing}
1034
+ />
1035
+ <span>${this.text("consentLabel")}${this.requiredMark()}</span>
1036
+ </label>
1037
+ ${consentError ? html`<p class="error" part="error" id="bk-consent-error">${consentError}</p>` : nothing}
1038
+ </div>
1039
+ <div class="hp" aria-hidden="true" inert>
1040
+ <input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" />
1041
+ </div>
1042
+ <div class="actions">
1043
+ <button class="btn" part="button" type="button" ?disabled=${this.busy} @click=${() => this.go("time")}>${this.text("back")}</button>
1044
+ <button class="btn primary" part="primary" type="submit" ?disabled=${this.busy}>${this.busy ? this.text("sending") : this.text("submit")}</button>
1045
+ </div>
1046
+ </form>
1047
+ `;
1048
+ }
1049
+ renderBooking() {
1050
+ const booking = this.booking;
1051
+ if (!booking) return nothing;
1052
+ const heading = this.banner === "booked" ? this.text("bookedHeading") : this.text("bookingHeading");
1053
+ return html`
1054
+ ${this.heading(heading)}
1055
+ ${this.banner ? html`<div class="success" part="success">
1056
+ <span class="success-icon" aria-hidden="true">${CHECK_ICON}</span>
1057
+ <p>${this.banner === "booked" ? this.text("bookedNote") : this.text("rescheduledNote")}</p>
1058
+ </div>` : nothing}
1059
+ <dl class="summary" part="summary">
1060
+ <dt>${this.text("serviceLabel")}</dt>
1061
+ <dd>${booking.serviceName}</dd>
1062
+ <dt>${this.text("whenLabel")}</dt>
1063
+ <dd>${this.whenText(booking.start)}</dd>
1064
+ </dl>
1065
+ ${this.banner === "booked" ? html`<div class="field">
1066
+ <label class="label" for="bk-link">${this.text("manageLinkLabel")}</label>
1067
+ <div class="link-row">
1068
+ <input class="input" id="bk-link" type="text" readonly aria-describedby="bk-link-hint" .value=${this.manageLink()} @focus=${(event) => event.target.select()} />
1069
+ <button class="btn" part="button" type="button" @click=${() => void this.copyLink()}>${this.text("copyLink")}</button>
1070
+ </div>
1071
+ <p class="note" id="bk-link-hint">${this.text("manageLinkHint")}</p>
1072
+ </div>` : nothing}
1073
+ ${this.confirmingCancel ? html`<div role="group" aria-labelledby="bk-cancel-q" class="field">
1074
+ <p id="bk-cancel-q" class="label">${this.text("cancelConfirm")}</p>
1075
+ <div class="actions">
1076
+ <button class="btn danger" part="button" type="button" ?disabled=${this.busy} @click=${() => void this.cancelBooking()}>${this.text("cancelYes")}</button>
1077
+ <button class="btn" part="button" type="button" ?disabled=${this.busy} @click=${() => this.confirmingCancel = false}>${this.text("cancelNo")}</button>
1078
+ </div>
1079
+ </div>` : html`<div class="actions">
1080
+ <button class="btn primary" part="primary" type="button" @click=${this.startReschedule}>${this.text("reschedule")}</button>
1081
+ <button class="btn danger" part="button" type="button" @click=${() => this.confirmingCancel = true}>${this.text("cancel")}</button>
1082
+ </div>`}
1083
+ `;
1084
+ }
1085
+ renderCancelled() {
1086
+ return html`
1087
+ ${this.heading(this.text("cancelledHeading"))}
1088
+ <p>${this.text("cancelledNote")}</p>
1089
+ <div class="actions"><button class="btn primary" part="primary" type="button" @click=${this.bookAnother}>${this.text("bookAnother")}</button></div>
1090
+ `;
1091
+ }
1092
+ renderRetry(label) {
1093
+ return html`<div class="actions"><button class="btn primary" part="primary" type="button" @click=${() => void this.start()}>${label}</button></div>`;
1094
+ }
1095
+ renderView() {
1096
+ switch (this.view) {
1097
+ case "loading":
1098
+ return html`<p class="loading" role="status">${this.text("loading")}</p>`;
1099
+ case "services":
1100
+ return this.renderServices();
1101
+ case "time":
1102
+ return this.renderTime();
1103
+ case "details":
1104
+ return this.renderDetails();
1105
+ case "booking":
1106
+ return this.renderBooking();
1107
+ case "cancelled":
1108
+ return this.renderCancelled();
1109
+ case "notfound":
1110
+ return html`${this.heading(this.text("bookingHeading"))}
1111
+ <div class="actions"><button class="btn primary" part="primary" type="button" @click=${this.bookAnother}>${this.text("bookAnother")}</button></div>`;
1112
+ case "fatal":
1113
+ return html`${this.heading(this.text("bookingHeading"))}${this.renderRetry(this.text("retry"))}`;
1114
+ }
1115
+ }
1116
+ render() {
1117
+ return html`
1118
+ <div class="root" aria-busy=${this.view === "loading" || this.slotsLoading ? "true" : "false"}>
1119
+ ${this.renderSteps()}
1120
+ <div class="alert ${this.alertText ? "has-text" : ""}" part="alert" role="alert">${this.alertText}</div>
1121
+ <p class="sr-only" role="status" aria-live="polite">${this.notice}</p>
1122
+ ${this.renderView()}
1123
+ </div>
1124
+ `;
1125
+ }
1126
+ };
1127
+ Booking.styles = css`
1128
+ ${controlBaseStyles}
1129
+
1130
+ :host {
1131
+ display: block;
1132
+ box-sizing: border-box;
1133
+ ${fontFamilyStyles}
1134
+ }
1135
+
1136
+ *,
1137
+ *::before,
1138
+ *::after {
1139
+ box-sizing: border-box;
1140
+ }
1141
+
1142
+ .root {
1143
+ display: flex;
1144
+ flex-direction: column;
1145
+ gap: 16px;
1146
+ min-width: 0;
1147
+ color: var(--st-color-text);
1148
+ }
1149
+
1150
+ /* Text uses the theme's own colour tokens (they swap in dark themes), as st-contact-form does. */
1151
+ h2 {
1152
+ margin: 0;
1153
+ font-size: 22px;
1154
+ line-height: 1.3;
1155
+ color: var(--st-color-text-strong);
1156
+ }
1157
+
1158
+ h2:focus-visible {
1159
+ outline: 2px solid var(--st-cms-brand-azure);
1160
+ outline-offset: 4px;
1161
+ }
1162
+
1163
+ h3 {
1164
+ margin: 0;
1165
+ font-size: 16px;
1166
+ color: var(--st-color-text-strong);
1167
+ }
1168
+
1169
+ p {
1170
+ margin: 0;
1171
+ }
1172
+
1173
+ .steps {
1174
+ display: flex;
1175
+ flex-wrap: wrap;
1176
+ gap: 8px;
1177
+ margin: 0;
1178
+ padding: 0;
1179
+ list-style: none;
1180
+ font-size: 13px;
1181
+ }
1182
+
1183
+ .steps li {
1184
+ padding: 4px 12px;
1185
+ border-radius: 9999px;
1186
+ background: var(--st-color-subtle, var(--st-cms-surface-soft));
1187
+ color: var(--st-color-text);
1188
+ }
1189
+
1190
+ .steps li[aria-current='step'] {
1191
+ background: var(--st-cms-brand-navy);
1192
+ color: var(--st-cms-on-dark);
1193
+ font-weight: var(--st-font-weight-semibold);
1194
+ }
1195
+
1196
+ .options {
1197
+ display: grid;
1198
+ gap: 12px;
1199
+ margin: 0;
1200
+ padding: 0;
1201
+ list-style: none;
1202
+ }
1203
+
1204
+ .service {
1205
+ display: flex;
1206
+ flex-direction: column;
1207
+ gap: 6px;
1208
+ width: 100%;
1209
+ min-height: 44px;
1210
+ padding: 16px 18px;
1211
+ border: 1px solid var(--st-color-border, var(--st-cms-primary-200));
1212
+ border-radius: 16px;
1213
+ background: var(--st-color-surface);
1214
+ color: var(--st-color-text);
1215
+ font: inherit;
1216
+ text-align: left;
1217
+ cursor: pointer;
1218
+ }
1219
+
1220
+ .service:hover {
1221
+ border-color: var(--st-cms-brand-navy);
1222
+ }
1223
+
1224
+ .service-name {
1225
+ font-size: 17px;
1226
+ font-weight: var(--st-font-weight-semibold);
1227
+ color: var(--st-color-text-strong);
1228
+ }
1229
+
1230
+ .meta {
1231
+ font-size: 14px;
1232
+ color: var(--st-color-text-muted);
1233
+ }
1234
+
1235
+ fieldset {
1236
+ min-width: 0;
1237
+ margin: 0;
1238
+ padding: 0;
1239
+ border: 0;
1240
+ }
1241
+
1242
+ legend {
1243
+ padding: 0;
1244
+ margin-bottom: 8px;
1245
+ font-size: 14px;
1246
+ font-weight: var(--st-font-weight-semibold);
1247
+ color: var(--st-color-text-strong);
1248
+ }
1249
+
1250
+ .choice {
1251
+ display: inline-flex;
1252
+ align-items: center;
1253
+ gap: 8px;
1254
+ min-height: 44px;
1255
+ margin-right: 16px;
1256
+ font-size: 15px;
1257
+ cursor: pointer;
1258
+ }
1259
+
1260
+ .choice input {
1261
+ width: 20px;
1262
+ height: 20px;
1263
+ margin: 0;
1264
+ accent-color: var(--st-cms-brand-navy);
1265
+ }
1266
+
1267
+ .time-layout {
1268
+ display: grid;
1269
+ grid-template-columns: minmax(0, 1fr);
1270
+ gap: 20px;
1271
+ }
1272
+
1273
+ @media (min-width: 720px) {
1274
+ .time-layout {
1275
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1276
+ }
1277
+ }
1278
+
1279
+ .month-bar {
1280
+ display: flex;
1281
+ align-items: center;
1282
+ justify-content: space-between;
1283
+ gap: 8px;
1284
+ margin-bottom: 8px;
1285
+ }
1286
+
1287
+ .month-name {
1288
+ font-size: 16px;
1289
+ font-weight: var(--st-font-weight-semibold);
1290
+ color: var(--st-color-text-strong);
1291
+ }
1292
+
1293
+ table {
1294
+ width: 100%;
1295
+ border-collapse: separate;
1296
+ border-spacing: 4px;
1297
+ table-layout: fixed;
1298
+ }
1299
+
1300
+ th {
1301
+ padding: 4px 0;
1302
+ font-size: 12px;
1303
+ font-weight: var(--st-font-weight-semibold);
1304
+ color: var(--st-color-text-muted);
1305
+ }
1306
+
1307
+ td {
1308
+ padding: 0;
1309
+ text-align: center;
1310
+ }
1311
+
1312
+ .day {
1313
+ width: 100%;
1314
+ min-height: 44px;
1315
+ padding: 0;
1316
+ border: 1px solid transparent;
1317
+ border-radius: 12px;
1318
+ background: transparent;
1319
+ color: var(--st-color-text-muted);
1320
+ font: inherit;
1321
+ font-size: 15px;
1322
+ cursor: default;
1323
+ }
1324
+
1325
+ .day.has-slots {
1326
+ border-color: var(--st-color-border, var(--st-cms-primary-200));
1327
+ background: var(--st-color-surface);
1328
+ color: var(--st-color-text-strong);
1329
+ font-weight: var(--st-font-weight-semibold);
1330
+ cursor: pointer;
1331
+ }
1332
+
1333
+ .day.has-slots:hover {
1334
+ border-color: var(--st-cms-brand-navy);
1335
+ }
1336
+
1337
+ .day[aria-pressed='true'] {
1338
+ background: var(--st-cms-brand-navy);
1339
+ border-color: var(--st-cms-brand-navy);
1340
+ color: var(--st-cms-on-dark);
1341
+ }
1342
+
1343
+ .slots {
1344
+ display: grid;
1345
+ grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
1346
+ gap: 8px;
1347
+ margin: 0;
1348
+ padding: 0;
1349
+ list-style: none;
1350
+ }
1351
+
1352
+ .slot {
1353
+ width: 100%;
1354
+ min-height: 44px;
1355
+ padding: 0 8px;
1356
+ border: 1px solid var(--st-color-border, var(--st-cms-primary-200));
1357
+ border-radius: 12px;
1358
+ background: var(--st-color-surface);
1359
+ color: var(--st-color-text-strong);
1360
+ font: inherit;
1361
+ font-size: 15px;
1362
+ cursor: pointer;
1363
+ }
1364
+
1365
+ .slot:hover {
1366
+ border-color: var(--st-cms-brand-navy);
1367
+ }
1368
+
1369
+ .slot[aria-pressed='true'] {
1370
+ background: var(--st-cms-brand-navy);
1371
+ border-color: var(--st-cms-brand-navy);
1372
+ color: var(--st-cms-on-dark);
1373
+ }
1374
+
1375
+ .day:focus-visible,
1376
+ .slot:focus-visible,
1377
+ .service:focus-visible,
1378
+ .btn:focus-visible,
1379
+ .input:focus-visible,
1380
+ .choice input:focus-visible,
1381
+ .consent input:focus-visible {
1382
+ outline: 2px solid var(--st-cms-brand-azure);
1383
+ outline-offset: 2px;
1384
+ }
1385
+
1386
+ .note {
1387
+ font-size: 14px;
1388
+ color: var(--st-color-text-muted);
1389
+ }
1390
+
1391
+ .field {
1392
+ display: flex;
1393
+ flex-direction: column;
1394
+ gap: 6px;
1395
+ min-width: 0;
1396
+ }
1397
+
1398
+ .label {
1399
+ font-size: 14px;
1400
+ font-weight: var(--st-font-weight-semibold);
1401
+ color: var(--st-color-text-strong);
1402
+ }
1403
+
1404
+ .input {
1405
+ width: 100%;
1406
+ min-height: 44px;
1407
+ padding: 10px 14px;
1408
+ border: 1px solid var(--st-color-border, var(--st-cms-primary-200));
1409
+ border-radius: 12px;
1410
+ background: var(--st-color-surface);
1411
+ color: var(--st-color-text-strong);
1412
+ /* 16px stops iOS zooming into the field. */
1413
+ font: inherit;
1414
+ font-size: 16px;
1415
+ }
1416
+
1417
+ .input::placeholder {
1418
+ color: var(--st-color-text-muted);
1419
+ opacity: 1;
1420
+ }
1421
+
1422
+ textarea.input {
1423
+ min-height: 104px;
1424
+ resize: vertical;
1425
+ }
1426
+
1427
+ .input[aria-invalid='true'] {
1428
+ border-color: var(--st-color-danger);
1429
+ box-shadow: inset 0 0 0 1px var(--st-color-danger);
1430
+ }
1431
+
1432
+ .error {
1433
+ font-size: 13px;
1434
+ line-height: 1.4;
1435
+ color: var(--st-color-danger-text);
1436
+ }
1437
+
1438
+ .counter {
1439
+ align-self: flex-end;
1440
+ font-size: 12px;
1441
+ color: var(--st-color-text-muted);
1442
+ }
1443
+
1444
+ .counter.over {
1445
+ color: var(--st-color-danger-text);
1446
+ font-weight: var(--st-font-weight-semibold);
1447
+ }
1448
+
1449
+ .consent {
1450
+ display: flex;
1451
+ align-items: flex-start;
1452
+ gap: 10px;
1453
+ min-height: 44px;
1454
+ font-size: 14px;
1455
+ line-height: 1.5;
1456
+ cursor: pointer;
1457
+ }
1458
+
1459
+ .consent input {
1460
+ flex: none;
1461
+ width: 20px;
1462
+ height: 20px;
1463
+ margin: 2px 0 0;
1464
+ accent-color: var(--st-cms-brand-navy);
1465
+ }
1466
+
1467
+ .sr-only {
1468
+ position: absolute;
1469
+ width: 1px;
1470
+ height: 1px;
1471
+ margin: -1px;
1472
+ padding: 0;
1473
+ overflow: hidden;
1474
+ clip: rect(0 0 0 0);
1475
+ white-space: nowrap;
1476
+ border: 0;
1477
+ }
1478
+
1479
+ /* Off-screen, not display:none: the browser still fills it in, a person and a screen reader do not see it. */
1480
+ .hp {
1481
+ position: absolute;
1482
+ left: -10000px;
1483
+ top: auto;
1484
+ width: 1px;
1485
+ height: 1px;
1486
+ overflow: hidden;
1487
+ }
1488
+
1489
+ .actions {
1490
+ display: flex;
1491
+ flex-wrap: wrap;
1492
+ gap: 12px;
1493
+ }
1494
+
1495
+ .btn {
1496
+ display: inline-flex;
1497
+ align-items: center;
1498
+ justify-content: center;
1499
+ min-height: 48px;
1500
+ padding: 0 24px;
1501
+ border: 1px solid var(--st-cms-brand-navy);
1502
+ border-radius: 9999px;
1503
+ background: transparent;
1504
+ color: var(--st-color-text-strong);
1505
+ font: inherit;
1506
+ font-size: 16px;
1507
+ font-weight: var(--st-font-weight-semibold);
1508
+ cursor: pointer;
1509
+ transition: opacity 150ms ease;
1510
+ }
1511
+
1512
+ .btn.primary {
1513
+ background: var(--st-cms-brand-navy);
1514
+ color: var(--st-cms-on-dark);
1515
+ }
1516
+
1517
+ .btn.danger {
1518
+ border-color: var(--st-color-danger);
1519
+ color: var(--st-color-danger-text);
1520
+ }
1521
+
1522
+ .btn:hover:not(:disabled) {
1523
+ opacity: 0.92;
1524
+ }
1525
+
1526
+ .btn:disabled {
1527
+ opacity: 0.6;
1528
+ cursor: progress;
1529
+ }
1530
+
1531
+ .icon-btn {
1532
+ min-width: 44px;
1533
+ padding: 0;
1534
+ }
1535
+
1536
+ @media (prefers-reduced-motion: reduce) {
1537
+ .btn {
1538
+ transition: none;
1539
+ }
1540
+ }
1541
+
1542
+ .alert {
1543
+ margin: 0;
1544
+ }
1545
+
1546
+ .alert.has-text {
1547
+ padding: 12px 14px;
1548
+ border-radius: 12px;
1549
+ background: var(--st-color-danger-subtle);
1550
+ color: var(--st-color-danger-text);
1551
+ font-size: 14px;
1552
+ line-height: 1.5;
1553
+ }
1554
+
1555
+ .success {
1556
+ display: flex;
1557
+ align-items: flex-start;
1558
+ gap: 12px;
1559
+ padding: 16px 18px;
1560
+ border-radius: 16px;
1561
+ background: var(--st-color-subtle, var(--st-cms-surface-soft));
1562
+ color: var(--st-color-text-strong);
1563
+ font-size: 15px;
1564
+ line-height: 1.6;
1565
+ }
1566
+
1567
+ .success-icon {
1568
+ flex: none;
1569
+ display: inline-flex;
1570
+ align-items: center;
1571
+ justify-content: center;
1572
+ width: 32px;
1573
+ height: 32px;
1574
+ border-radius: 9999px;
1575
+ background: var(--st-cms-highlight);
1576
+ color: var(--st-cms-navy);
1577
+ }
1578
+
1579
+ .success-icon svg {
1580
+ width: 18px;
1581
+ height: 18px;
1582
+ }
1583
+
1584
+ dl.summary {
1585
+ display: grid;
1586
+ grid-template-columns: max-content minmax(0, 1fr);
1587
+ gap: 8px 16px;
1588
+ margin: 0;
1589
+ padding: 16px 18px;
1590
+ border: 1px solid var(--st-color-border, var(--st-cms-primary-200));
1591
+ border-radius: 16px;
1592
+ background: var(--st-color-surface);
1593
+ }
1594
+
1595
+ dl.summary dt {
1596
+ font-weight: var(--st-font-weight-semibold);
1597
+ color: var(--st-color-text-strong);
1598
+ }
1599
+
1600
+ dl.summary dd {
1601
+ margin: 0;
1602
+ overflow-wrap: anywhere;
1603
+ }
1604
+
1605
+ .link-row {
1606
+ display: flex;
1607
+ flex-wrap: wrap;
1608
+ gap: 8px;
1609
+ }
1610
+
1611
+ .link-row .input {
1612
+ flex: 1 1 220px;
1613
+ width: auto;
1614
+ }
1615
+
1616
+ .loading {
1617
+ padding: 24px 0;
1618
+ color: var(--st-color-text-muted);
1619
+ }
1620
+ `;
1621
+ __decorateClass([
1622
+ property({ type: String })
1623
+ ], Booking.prototype, "endpoint", 2);
1624
+ __decorateClass([
1625
+ property({ type: String })
1626
+ ], Booking.prototype, "reference", 2);
1627
+ __decorateClass([
1628
+ property({ type: String, attribute: "service-id" })
1629
+ ], Booking.prototype, "serviceId", 2);
1630
+ __decorateClass([
1631
+ property({ type: Number, attribute: "week-start" })
1632
+ ], Booking.prototype, "weekStart", 2);
1633
+ __decorateClass([
1634
+ property({ type: String })
1635
+ ], Booking.prototype, "locale", 2);
1636
+ __decorateClass([
1637
+ property({ type: String, attribute: "manage-url" })
1638
+ ], Booking.prototype, "manageUrl", 2);
1639
+ __decorateClass([
1640
+ property({ attribute: false })
1641
+ ], Booking.prototype, "labels", 2);
1642
+ __decorateClass([
1643
+ property({ attribute: false })
1644
+ ], Booking.prototype, "errorMessages", 2);
1645
+ __decorateClass([
1646
+ state()
1647
+ ], Booking.prototype, "view", 2);
1648
+ __decorateClass([
1649
+ state()
1650
+ ], Booking.prototype, "mode", 2);
1651
+ __decorateClass([
1652
+ state()
1653
+ ], Booking.prototype, "services", 2);
1654
+ __decorateClass([
1655
+ state()
1656
+ ], Booking.prototype, "service", 2);
1657
+ __decorateClass([
1658
+ state()
1659
+ ], Booking.prototype, "staffId", 2);
1660
+ __decorateClass([
1661
+ state()
1662
+ ], Booking.prototype, "month", 2);
1663
+ __decorateClass([
1664
+ state()
1665
+ ], Booking.prototype, "slots", 2);
1666
+ __decorateClass([
1667
+ state()
1668
+ ], Booking.prototype, "slotsLoading", 2);
1669
+ __decorateClass([
1670
+ state()
1671
+ ], Booking.prototype, "selectedDay", 2);
1672
+ __decorateClass([
1673
+ state()
1674
+ ], Booking.prototype, "focusDay", 2);
1675
+ __decorateClass([
1676
+ state()
1677
+ ], Booking.prototype, "selectedSlot", 2);
1678
+ __decorateClass([
1679
+ state()
1680
+ ], Booking.prototype, "booking", 2);
1681
+ __decorateClass([
1682
+ state()
1683
+ ], Booking.prototype, "banner", 2);
1684
+ __decorateClass([
1685
+ state()
1686
+ ], Booking.prototype, "confirmingCancel", 2);
1687
+ __decorateClass([
1688
+ state()
1689
+ ], Booking.prototype, "busy", 2);
1690
+ __decorateClass([
1691
+ state()
1692
+ ], Booking.prototype, "errors", 2);
1693
+ __decorateClass([
1694
+ state()
1695
+ ], Booking.prototype, "alertText", 2);
1696
+ __decorateClass([
1697
+ state()
1698
+ ], Booking.prototype, "notice", 2);
1699
+ __decorateClass([
1700
+ state()
1701
+ ], Booking.prototype, "notesLength", 2);
1702
+ Booking = __decorateClass([
1703
+ customElement("st-booking")
1704
+ ], Booking);
1705
+
1706
+ export {
1707
+ BOOKING_CREATED_EVENT,
1708
+ BOOKING_RESCHEDULED_EVENT,
1709
+ BOOKING_CANCELLED_EVENT,
1710
+ BOOKING_ERROR_EVENT,
1711
+ DEFAULT_BOOKING_LABELS,
1712
+ Booking
1713
+ };
1714
+ //# sourceMappingURL=chunk-DPANTC3O.js.map