@flamingo-stack/openframe-frontend-core 0.0.594 → 0.0.595-snapshot.20260905173123

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 (161) hide show
  1. package/dist/{chunk-HQWZ4IXU.cjs → chunk-3CH6VRJX.cjs} +24 -24
  2. package/dist/{chunk-HQWZ4IXU.cjs.map → chunk-3CH6VRJX.cjs.map} +1 -1
  3. package/dist/{chunk-LKN2KKYZ.js → chunk-4K7TCJ35.js} +6 -59
  4. package/dist/{chunk-LKN2KKYZ.js.map → chunk-4K7TCJ35.js.map} +1 -1
  5. package/dist/{chunk-YLVPQNLO.cjs → chunk-5MSHWXJ5.cjs} +170 -223
  6. package/dist/chunk-5MSHWXJ5.cjs.map +1 -0
  7. package/dist/{chunk-CCLPJSY3.cjs → chunk-73BECFSU.cjs} +15 -15
  8. package/dist/{chunk-CCLPJSY3.cjs.map → chunk-73BECFSU.cjs.map} +1 -1
  9. package/dist/{chunk-F5Q3DTP5.js → chunk-7EUE3YTC.js} +8 -8
  10. package/dist/{chunk-GBIC3VNA.js → chunk-7KGN7DV6.js} +6 -6
  11. package/dist/{chunk-YZLUOZKR.js → chunk-7Q66A4CF.js} +2 -2
  12. package/dist/{chunk-DRZ224RU.js → chunk-ANSZVKI2.js} +8 -8
  13. package/dist/{chunk-N3RHX26N.js → chunk-AZZVSEP5.js} +3 -3
  14. package/dist/{chunk-66I2QUFE.cjs → chunk-B26UT36N.cjs} +6 -6
  15. package/dist/{chunk-66I2QUFE.cjs.map → chunk-B26UT36N.cjs.map} +1 -1
  16. package/dist/{chunk-UBRVIVY4.cjs → chunk-B2CSYMKW.cjs} +298 -285
  17. package/dist/chunk-B2CSYMKW.cjs.map +1 -0
  18. package/dist/{chunk-ZK3AZ5YF.cjs → chunk-BTOL222V.cjs} +11 -11
  19. package/dist/{chunk-ZK3AZ5YF.cjs.map → chunk-BTOL222V.cjs.map} +1 -1
  20. package/dist/{chunk-S7VW4HVL.cjs → chunk-DODE3PD4.cjs} +49 -49
  21. package/dist/{chunk-S7VW4HVL.cjs.map → chunk-DODE3PD4.cjs.map} +1 -1
  22. package/dist/{chunk-ZZHHKE2Q.js → chunk-ECXQX4EP.js} +2 -2
  23. package/dist/{chunk-NEFNARFF.cjs → chunk-EEV7SSBJ.cjs} +30 -30
  24. package/dist/{chunk-NEFNARFF.cjs.map → chunk-EEV7SSBJ.cjs.map} +1 -1
  25. package/dist/{chunk-3OQIZPUX.js → chunk-EO3NEGWB.js} +4 -4
  26. package/dist/{chunk-MOHSA65M.js → chunk-EZM5DNII.js} +2 -2
  27. package/dist/{chunk-IKQVXZB5.js → chunk-GSPS6Y5U.js} +6 -6
  28. package/dist/{chunk-CQ35OMPJ.cjs → chunk-I2BNLWC3.cjs} +3 -3
  29. package/dist/{chunk-CQ35OMPJ.cjs.map → chunk-I2BNLWC3.cjs.map} +1 -1
  30. package/dist/{chunk-HOLK4ZMT.js → chunk-II2GBWNW.js} +4 -4
  31. package/dist/{chunk-PETYVGEQ.cjs → chunk-JKGVXLTT.cjs} +5 -5
  32. package/dist/{chunk-PETYVGEQ.cjs.map → chunk-JKGVXLTT.cjs.map} +1 -1
  33. package/dist/{chunk-5EM6NGBK.cjs → chunk-KOQCA5X5.cjs} +17 -27
  34. package/dist/chunk-KOQCA5X5.cjs.map +1 -0
  35. package/dist/{chunk-ACU7J5BL.cjs → chunk-KPE7OQF6.cjs} +84 -84
  36. package/dist/{chunk-ACU7J5BL.cjs.map → chunk-KPE7OQF6.cjs.map} +1 -1
  37. package/dist/{chunk-C4YJC5X7.cjs → chunk-KRKW63LG.cjs} +9 -9
  38. package/dist/{chunk-C4YJC5X7.cjs.map → chunk-KRKW63LG.cjs.map} +1 -1
  39. package/dist/{chunk-ZEG3ZJPY.cjs → chunk-LE5SZPMN.cjs} +33 -33
  40. package/dist/{chunk-ZEG3ZJPY.cjs.map → chunk-LE5SZPMN.cjs.map} +1 -1
  41. package/dist/{chunk-A5AKGC4S.js → chunk-LPMDCQLL.js} +17 -27
  42. package/dist/chunk-LPMDCQLL.js.map +1 -0
  43. package/dist/{chunk-EWOFXJGH.cjs → chunk-M3OQLL7D.cjs} +63 -63
  44. package/dist/{chunk-EWOFXJGH.cjs.map → chunk-M3OQLL7D.cjs.map} +1 -1
  45. package/dist/{chunk-FKXDDJEQ.cjs → chunk-M4YQUHOR.cjs} +5 -5
  46. package/dist/{chunk-FKXDDJEQ.cjs.map → chunk-M4YQUHOR.cjs.map} +1 -1
  47. package/dist/{chunk-5XQMEDGE.cjs → chunk-OHJ4QOWN.cjs} +12 -12
  48. package/dist/{chunk-5XQMEDGE.cjs.map → chunk-OHJ4QOWN.cjs.map} +1 -1
  49. package/dist/{chunk-Y6I2DGKC.cjs → chunk-OMKI5T2G.cjs} +1 -1
  50. package/dist/chunk-OMKI5T2G.cjs.map +1 -0
  51. package/dist/{chunk-M5HGULIY.js → chunk-OQODJ26H.js} +5 -5
  52. package/dist/{chunk-MEWAFY6X.js → chunk-OT5IQMSI.js} +4 -4
  53. package/dist/{chunk-Y22VQDVW.js → chunk-QB244ZDI.js} +4 -4
  54. package/dist/{chunk-3T7ZP3TL.js → chunk-QL6ECU6Y.js} +3 -3
  55. package/dist/{chunk-3XDPXOMS.js → chunk-R2SZYRB2.js} +7 -7
  56. package/dist/{chunk-QAVT6P5F.cjs → chunk-RFA5T5MW.cjs} +28 -28
  57. package/dist/{chunk-QAVT6P5F.cjs.map → chunk-RFA5T5MW.cjs.map} +1 -1
  58. package/dist/{chunk-BTF3CNDP.js → chunk-ROQO355A.js} +1 -1
  59. package/dist/chunk-ROQO355A.js.map +1 -0
  60. package/dist/{chunk-43ADGEWL.cjs → chunk-UJAMXLA3.cjs} +40 -40
  61. package/dist/{chunk-43ADGEWL.cjs.map → chunk-UJAMXLA3.cjs.map} +1 -1
  62. package/dist/{chunk-KVNA3HU2.js → chunk-XANQTIHS.js} +3 -3
  63. package/dist/{chunk-PMN7WDXI.js → chunk-YUHOKPKS.js} +26 -13
  64. package/dist/chunk-YUHOKPKS.js.map +1 -0
  65. package/dist/components/case-studies/index.cjs +16 -16
  66. package/dist/components/case-studies/index.js +6 -6
  67. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  68. package/dist/components/chat/index.cjs +4 -4
  69. package/dist/components/chat/index.js +3 -3
  70. package/dist/components/contact/index.cjs +7 -7
  71. package/dist/components/contact/index.js +6 -6
  72. package/dist/components/docs/index.cjs +10 -10
  73. package/dist/components/docs/index.js +9 -9
  74. package/dist/components/embeds/index.cjs +7 -7
  75. package/dist/components/embeds/index.js +6 -6
  76. package/dist/components/faq/index.cjs +7 -7
  77. package/dist/components/faq/index.js +6 -6
  78. package/dist/components/features/index.cjs +6 -6
  79. package/dist/components/features/index.js +5 -5
  80. package/dist/components/help-center-pages/index.cjs +26 -26
  81. package/dist/components/help-center-pages/index.js +17 -17
  82. package/dist/components/icons/index.cjs +2 -2
  83. package/dist/components/icons/index.js +1 -1
  84. package/dist/components/icons/mlg-logo.d.ts.map +1 -1
  85. package/dist/components/index.cjs +56 -56
  86. package/dist/components/index.js +17 -17
  87. package/dist/components/meeting-scheduler/booking-form.d.ts +70 -6
  88. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  89. package/dist/components/meeting-scheduler/context-panel.d.ts +9 -1
  90. package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
  91. package/dist/components/meeting-scheduler/index.cjs +395 -188
  92. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  93. package/dist/components/meeting-scheduler/index.d.ts +55 -1
  94. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  95. package/dist/components/meeting-scheduler/index.js +381 -174
  96. package/dist/components/meeting-scheduler/index.js.map +1 -1
  97. package/dist/components/meeting-scheduler/slot-picker.d.ts +8 -1
  98. package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
  99. package/dist/components/navigation/index.cjs +9 -9
  100. package/dist/components/navigation/index.js +8 -8
  101. package/dist/components/onboarding-guides/index.cjs +9 -9
  102. package/dist/components/onboarding-guides/index.js +8 -8
  103. package/dist/components/related-content/index.cjs +3 -3
  104. package/dist/components/related-content/index.js +2 -2
  105. package/dist/components/tickets/index.cjs +10 -10
  106. package/dist/components/tickets/index.js +9 -9
  107. package/dist/components/ui/benefit-card.d.ts.map +1 -1
  108. package/dist/components/ui/index.cjs +6 -6
  109. package/dist/components/ui/index.js +5 -5
  110. package/dist/hooks/index.cjs +3 -3
  111. package/dist/hooks/index.js +2 -2
  112. package/dist/index.cjs +10 -10
  113. package/dist/index.js +9 -9
  114. package/dist/schemas/meeting-booking-schema.cjs +19 -4
  115. package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
  116. package/dist/schemas/meeting-booking-schema.d.ts +41 -16
  117. package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
  118. package/dist/schemas/meeting-booking-schema.js +19 -5
  119. package/dist/schemas/meeting-booking-schema.js.map +1 -1
  120. package/dist/types/index.cjs.map +1 -1
  121. package/dist/types/index.js.map +1 -1
  122. package/dist/utils/index.cjs +18 -40
  123. package/dist/utils/index.cjs.map +1 -1
  124. package/dist/utils/index.js +18 -40
  125. package/dist/utils/index.js.map +1 -1
  126. package/dist/utils/source-icons.d.ts.map +1 -1
  127. package/package.json +1 -1
  128. package/src/components/chat/entity-cards/dispatch.tsx +0 -57
  129. package/src/components/icons/mlg-logo.tsx +1 -7
  130. package/src/components/meeting-scheduler/booking-form.tsx +425 -154
  131. package/src/components/meeting-scheduler/context-panel.tsx +13 -3
  132. package/src/components/meeting-scheduler/index.tsx +317 -69
  133. package/src/components/meeting-scheduler/slot-picker.tsx +17 -1
  134. package/src/components/ui/benefit-card.tsx +30 -9
  135. package/src/schemas/meeting-booking-schema.ts +58 -4
  136. package/src/types/tmcg.ts +0 -2
  137. package/src/utils/format.ts +1 -1
  138. package/src/utils/source-icons.ts +0 -24
  139. package/dist/chunk-5EM6NGBK.cjs.map +0 -1
  140. package/dist/chunk-A5AKGC4S.js.map +0 -1
  141. package/dist/chunk-BTF3CNDP.js.map +0 -1
  142. package/dist/chunk-PMN7WDXI.js.map +0 -1
  143. package/dist/chunk-UBRVIVY4.cjs.map +0 -1
  144. package/dist/chunk-Y6I2DGKC.cjs.map +0 -1
  145. package/dist/chunk-YLVPQNLO.cjs.map +0 -1
  146. /package/dist/{chunk-F5Q3DTP5.js.map → chunk-7EUE3YTC.js.map} +0 -0
  147. /package/dist/{chunk-GBIC3VNA.js.map → chunk-7KGN7DV6.js.map} +0 -0
  148. /package/dist/{chunk-YZLUOZKR.js.map → chunk-7Q66A4CF.js.map} +0 -0
  149. /package/dist/{chunk-DRZ224RU.js.map → chunk-ANSZVKI2.js.map} +0 -0
  150. /package/dist/{chunk-N3RHX26N.js.map → chunk-AZZVSEP5.js.map} +0 -0
  151. /package/dist/{chunk-ZZHHKE2Q.js.map → chunk-ECXQX4EP.js.map} +0 -0
  152. /package/dist/{chunk-3OQIZPUX.js.map → chunk-EO3NEGWB.js.map} +0 -0
  153. /package/dist/{chunk-MOHSA65M.js.map → chunk-EZM5DNII.js.map} +0 -0
  154. /package/dist/{chunk-IKQVXZB5.js.map → chunk-GSPS6Y5U.js.map} +0 -0
  155. /package/dist/{chunk-HOLK4ZMT.js.map → chunk-II2GBWNW.js.map} +0 -0
  156. /package/dist/{chunk-M5HGULIY.js.map → chunk-OQODJ26H.js.map} +0 -0
  157. /package/dist/{chunk-MEWAFY6X.js.map → chunk-OT5IQMSI.js.map} +0 -0
  158. /package/dist/{chunk-Y22VQDVW.js.map → chunk-QB244ZDI.js.map} +0 -0
  159. /package/dist/{chunk-3T7ZP3TL.js.map → chunk-QL6ECU6Y.js.map} +0 -0
  160. /package/dist/{chunk-3XDPXOMS.js.map → chunk-R2SZYRB2.js.map} +0 -0
  161. /package/dist/{chunk-KVNA3HU2.js.map → chunk-XANQTIHS.js.map} +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  AlertDescription,
12
12
  Autocomplete,
13
13
  BackButton,
14
+ CheckboxBlock,
14
15
  DatePickerCalendar,
15
16
  HoneypotField,
16
17
  MAX_MONTH_OFFSET,
@@ -22,13 +23,13 @@ import {
22
23
  SelectTrigger,
23
24
  SelectValue,
24
25
  Textarea
25
- } from "../../chunk-PMN7WDXI.js";
26
+ } from "../../chunk-YUHOKPKS.js";
26
27
  import "../../chunk-L2SLFCXP.js";
27
28
  import "../../chunk-BJXDHKUX.js";
28
29
  import {
29
30
  useHumanitySignals,
30
31
  useToast
31
- } from "../../chunk-MOHSA65M.js";
32
+ } from "../../chunk-EZM5DNII.js";
32
33
  import "../../chunk-ZRIVCLPR.js";
33
34
  import "../../chunk-GBON2OCG.js";
34
35
  import {
@@ -42,10 +43,10 @@ import {
42
43
  } from "../../chunk-T2NQSKH4.js";
43
44
  import {
44
45
  useIsHydrated
45
- } from "../../chunk-LKN2KKYZ.js";
46
+ } from "../../chunk-4K7TCJ35.js";
46
47
  import "../../chunk-5LIDR7HL.js";
47
48
  import "../../chunk-XXMHLZ3Y.js";
48
- import "../../chunk-A5AKGC4S.js";
49
+ import "../../chunk-LPMDCQLL.js";
49
50
  import "../../chunk-IMDG23LW.js";
50
51
  import "../../chunk-W72U7OU7.js";
51
52
  import {
@@ -53,7 +54,7 @@ import {
53
54
  SquareAvatar,
54
55
  StatusBadge,
55
56
  formatDurationCompact
56
- } from "../../chunk-BTF3CNDP.js";
57
+ } from "../../chunk-ROQO355A.js";
57
58
  import "../../chunk-MDKCTSDS.js";
58
59
  import "../../chunk-MYBXCWEI.js";
59
60
  import "../../chunk-OTX6XNSA.js";
@@ -76,7 +77,7 @@ import "../../chunk-3DTTSBDF.js";
76
77
  import "../../chunk-DCHYNTHI.js";
77
78
 
78
79
  // src/components/meeting-scheduler/index.tsx
79
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef2, useState as useState3 } from "react";
80
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef2, useState as useState4 } from "react";
80
81
 
81
82
  // src/hooks/use-meeting-booking.ts
82
83
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -113,7 +114,7 @@ function isValidBcp47Locale(locale) {
113
114
  return false;
114
115
  }
115
116
  }
116
- function makeBookingSchema(formFields, legalConsent) {
117
+ function buildBookingSchema(formFields, legalConsent, slot) {
117
118
  const answers = {};
118
119
  for (const field of formFields) {
119
120
  if (!isSupportedFormField(field)) continue;
@@ -150,12 +151,12 @@ function makeBookingSchema(formFields, legalConsent) {
150
151
  const answersObject = z.object(answers);
151
152
  return z.object({
152
153
  meetingId: z.string().min(1),
153
- startTimeMs: z.number().int().positive(),
154
- durationMs: z.number().int().positive(),
154
+ startTimeMs: slot.startTimeMs,
155
+ durationMs: slot.durationMs,
155
156
  firstName: z.string().min(1, { message: "First name is required" }).max(255),
156
157
  lastName: z.string().min(1, { message: "Last name is required" }).max(255),
157
158
  email: z.string().email({ message: "Please enter a valid email address" }).max(255),
158
- timezone: z.string().refine(isValidIanaTimezone, { message: "Invalid timezone" }),
159
+ timezone: slot.timezone,
159
160
  locale: z.string().refine(isValidBcp47Locale, { message: "Invalid locale" }).optional(),
160
161
  // Plain `.optional()` (no `.default()`) so zod's input and output types
161
162
  // match — react-hook-form's zodResolver needs them identical, and the
@@ -177,6 +178,13 @@ function makeBookingSchema(formFields, legalConsent) {
177
178
  }
178
179
  });
179
180
  }
181
+ function makeDeferredBookingSchema(formFields, legalConsent) {
182
+ return buildBookingSchema(formFields, legalConsent, {
183
+ startTimeMs: z.number().int().positive().nullish(),
184
+ durationMs: z.number().int().positive().nullish(),
185
+ timezone: z.string().refine(isValidIanaTimezone, { message: "Invalid timezone" }).nullish()
186
+ });
187
+ }
180
188
 
181
189
  // src/hooks/use-meeting-booking.ts
182
190
  var AVAILABILITY_STALE_MS = 0;
@@ -268,15 +276,28 @@ function useMeetingBooking(options) {
268
276
 
269
277
  // src/components/meeting-scheduler/booking-form.tsx
270
278
  import { zodResolver } from "@hookform/resolvers/zod";
271
- import { useMemo } from "react";
279
+ import { Fragment, useEffect, useMemo, useState as useState2 } from "react";
272
280
  import { useForm, Controller } from "react-hook-form";
273
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
281
+ import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
282
+ var SPAN_CLASS = {
283
+ 1: "md:col-span-1",
284
+ 2: "md:col-span-2",
285
+ 3: "md:col-span-3",
286
+ 4: "md:col-span-4"
287
+ };
288
+ var evenSpan = (count) => Math.min(4, Math.max(1, Math.floor(4 / Math.max(1, count))));
274
289
  function BookingForm({
275
290
  availability,
276
291
  meetingId,
277
292
  startTimeMs,
278
293
  durationMs,
279
294
  timezone,
295
+ deferSlot = false,
296
+ initialValues,
297
+ submitLabel,
298
+ footerNote,
299
+ fieldRows,
300
+ consent,
280
301
  isSubmitting,
281
302
  onSubmit,
282
303
  honeypotInputProps,
@@ -284,42 +305,75 @@ function BookingForm({
284
305
  }) {
285
306
  const { formFields, legalConsent } = availability;
286
307
  const supportedFields = useMemo(() => formFields.filter(isSupportedFormField), [formFields]);
287
- const schema = useMemo(() => makeBookingSchema(supportedFields, legalConsent), [supportedFields, legalConsent]);
308
+ const schema = useMemo(
309
+ () => makeDeferredBookingSchema(supportedFields, legalConsent),
310
+ [supportedFields, legalConsent]
311
+ );
312
+ const consentDefaults = useMemo(
313
+ () => (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({
314
+ communicationTypeId: c.communicationTypeId,
315
+ consented: false
316
+ })),
317
+ [legalConsent]
318
+ );
288
319
  const {
289
320
  register,
290
321
  control,
291
322
  handleSubmit,
323
+ setValue,
292
324
  formState: { errors }
293
325
  } = useForm({
294
326
  resolver: zodResolver(schema),
295
327
  defaultValues: {
296
328
  meetingId,
297
- startTimeMs,
298
- durationMs,
329
+ // Genuinely OMITTED, not present-as-undefined, when the slot is deferred.
330
+ ...deferSlot ? {} : { startTimeMs, durationMs },
299
331
  timezone,
300
332
  locale: typeof navigator !== "undefined" ? navigator.language : void 0,
301
333
  firstName: "",
302
334
  lastName: "",
303
335
  email: "",
304
336
  formFields: {},
305
- legalConsentResponses: (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({
306
- communicationTypeId: c.communicationTypeId,
307
- consented: false
308
- }))
337
+ legalConsentResponses: consentDefaults,
338
+ // A remount (back edge, or an error return) would otherwise lose every
339
+ // answer — `defaultValues` is snapshotted once and never re-read.
340
+ ...initialValues ?? {}
309
341
  }
310
342
  });
311
- const submit = handleSubmit(async (data) => {
312
- await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() });
343
+ const priorConsents = initialValues?.legalConsentResponses;
344
+ const [consented, setConsented] = useState2(initialValues?.hostConsent === true);
345
+ const [consentError, setConsentError] = useState2(null);
346
+ const consentMissing = Boolean(consent) && !consented;
347
+ useEffect(() => {
348
+ setValue(
349
+ "legalConsentResponses",
350
+ consentDefaults.map((next) => ({
351
+ ...next,
352
+ consented: priorConsents?.find((r) => r.communicationTypeId === next.communicationTypeId)?.consented ?? false
353
+ })),
354
+ { shouldDirty: false }
355
+ );
356
+ }, [consentDefaults, priorConsents, setValue]);
357
+ const submitValid = handleSubmit(async (data) => {
358
+ if (consentMissing) return;
359
+ if (deferSlot) {
360
+ await onSubmit({ ...data, meetingId, hostConsent: consented });
361
+ return;
362
+ }
363
+ await onSubmit({ ...data, meetingId, hostConsent: consented, startTimeMs, durationMs, timezone, ...getSignals() });
313
364
  });
365
+ const submit = (event) => {
366
+ if (consentMissing) setConsentError(consent?.errorMessage ?? "Please agree to continue.");
367
+ return submitValid(event);
368
+ };
314
369
  const fieldError = (name) => {
315
370
  const err = errors.formFields?.[name];
316
371
  return err?.message;
317
372
  };
318
373
  const inputClass = "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12";
319
374
  const FORM_STACK = "flex flex-col gap-[var(--spacing-system-l)]";
320
- return /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: FORM_STACK, noValidate: true, children: [
321
- /* @__PURE__ */ jsx(HoneypotField, { ...honeypotInputProps }),
322
- /* @__PURE__ */ jsx(FieldWrapper, { label: "Email", htmlFor: "ms-email", error: errors.email?.message, children: /* @__PURE__ */ jsx(
375
+ const builtInFields = {
376
+ email: /* @__PURE__ */ jsx(FieldWrapper, { label: "Email", htmlFor: "ms-email", error: errors.email?.message, children: /* @__PURE__ */ jsx(
323
377
  Input,
324
378
  {
325
379
  id: "ms-email",
@@ -332,129 +386,210 @@ function BookingForm({
332
386
  ...register("email")
333
387
  }
334
388
  ) }),
335
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-[var(--spacing-system-m)]", children: [
336
- /* @__PURE__ */ jsx(FieldWrapper, { label: "First Name", htmlFor: "ms-first-name", error: errors.firstName?.message, children: /* @__PURE__ */ jsx(
337
- Input,
338
- {
339
- id: "ms-first-name",
340
- required: true,
341
- autoComplete: "given-name",
342
- placeholder: "Jane",
343
- "aria-invalid": !!errors.firstName,
344
- className: inputClass,
345
- ...register("firstName")
346
- }
347
- ) }),
348
- /* @__PURE__ */ jsx(FieldWrapper, { label: "Last Name", htmlFor: "ms-last-name", error: errors.lastName?.message, children: /* @__PURE__ */ jsx(
349
- Input,
350
- {
351
- id: "ms-last-name",
352
- required: true,
353
- autoComplete: "family-name",
354
- placeholder: "Doe",
355
- "aria-invalid": !!errors.lastName,
356
- className: inputClass,
357
- ...register("lastName")
358
- }
359
- ) })
360
- ] }),
361
- supportedFields.map((field) => /* @__PURE__ */ jsxs(
362
- FieldWrapper,
389
+ firstName: /* @__PURE__ */ jsx(FieldWrapper, { label: "First Name", htmlFor: "ms-first-name", error: errors.firstName?.message, children: /* @__PURE__ */ jsx(
390
+ Input,
363
391
  {
364
- label: field.label,
365
- htmlFor: `ms-q-${field.name}`,
366
- error: fieldError(field.name),
367
- children: [
368
- field.type === "textarea" && /* @__PURE__ */ jsx(
369
- Textarea,
370
- {
371
- id: `ms-q-${field.name}`,
372
- className: "border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary",
373
- ...register(`formFields.${field.name}`)
374
- }
375
- ),
376
- field.type === "text" && /* @__PURE__ */ jsx(
377
- Input,
392
+ id: "ms-first-name",
393
+ required: true,
394
+ autoComplete: "given-name",
395
+ placeholder: "Jane",
396
+ "aria-invalid": !!errors.firstName,
397
+ className: inputClass,
398
+ ...register("firstName")
399
+ }
400
+ ) }),
401
+ lastName: /* @__PURE__ */ jsx(FieldWrapper, { label: "Last Name", htmlFor: "ms-last-name", error: errors.lastName?.message, children: /* @__PURE__ */ jsx(
402
+ Input,
403
+ {
404
+ id: "ms-last-name",
405
+ required: true,
406
+ autoComplete: "family-name",
407
+ placeholder: "Doe",
408
+ "aria-invalid": !!errors.lastName,
409
+ className: inputClass,
410
+ ...register("lastName")
411
+ }
412
+ ) })
413
+ };
414
+ const renderDeclaredField = (field) => /* @__PURE__ */ jsxs(FieldWrapper, { label: field.label, htmlFor: `ms-q-${field.name}`, error: fieldError(field.name), children: [
415
+ field.type === "textarea" && /* @__PURE__ */ jsx(
416
+ Textarea,
417
+ {
418
+ id: `ms-q-${field.name}`,
419
+ className: "border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary",
420
+ ...register(`formFields.${field.name}`)
421
+ }
422
+ ),
423
+ field.type === "text" && /* @__PURE__ */ jsx(
424
+ Input,
425
+ {
426
+ id: `ms-q-${field.name}`,
427
+ className: "h-11 border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary md:h-12",
428
+ ...register(`formFields.${field.name}`)
429
+ }
430
+ ),
431
+ (field.type === "select" || field.type === "radio") && /* @__PURE__ */ jsx(
432
+ Controller,
433
+ {
434
+ control,
435
+ name: `formFields.${field.name}`,
436
+ render: ({ field: rhf }) => field.type === "select" ? /* @__PURE__ */ jsxs(Select, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: [
437
+ /* @__PURE__ */ jsx(SelectTrigger, { id: `ms-q-${field.name}`, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
438
+ /* @__PURE__ */ jsx(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt, children: opt }, opt)) })
439
+ ] }) : /* @__PURE__ */ jsx(RadioGroup, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
440
+ /* @__PURE__ */ jsx(RadioGroupItem, { id: `ms-q-${field.name}-${opt}`, value: opt }),
441
+ /* @__PURE__ */ jsx(Label, { htmlFor: `ms-q-${field.name}-${opt}`, children: opt })
442
+ ] }, opt)) })
443
+ }
444
+ ),
445
+ field.type === "checkbox" && /* @__PURE__ */ jsx(
446
+ Controller,
447
+ {
448
+ control,
449
+ name: `formFields.${field.name}`,
450
+ render: ({ field: rhf }) => (
451
+ // The question's Label above already carries the text and
452
+ // binds to this id — a second inline label would double the
453
+ // visible text AND the accessible name.
454
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: /* @__PURE__ */ jsx(
455
+ Checkbox,
378
456
  {
379
457
  id: `ms-q-${field.name}`,
380
- className: "h-11 border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary md:h-12",
381
- ...register(`formFields.${field.name}`)
382
- }
383
- ),
384
- (field.type === "select" || field.type === "radio") && /* @__PURE__ */ jsx(
385
- Controller,
386
- {
387
- control,
388
- name: `formFields.${field.name}`,
389
- render: ({ field: rhf }) => field.type === "select" ? /* @__PURE__ */ jsxs(Select, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: [
390
- /* @__PURE__ */ jsx(SelectTrigger, { id: `ms-q-${field.name}`, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
391
- /* @__PURE__ */ jsx(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt, children: opt }, opt)) })
392
- ] }) : /* @__PURE__ */ jsx(RadioGroup, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
393
- /* @__PURE__ */ jsx(RadioGroupItem, { id: `ms-q-${field.name}-${opt}`, value: opt }),
394
- /* @__PURE__ */ jsx(Label, { htmlFor: `ms-q-${field.name}-${opt}`, children: opt })
395
- ] }, opt)) })
458
+ checked: Boolean(rhf.value),
459
+ onCheckedChange: (v) => rhf.onChange(v === true)
396
460
  }
397
- ),
398
- field.type === "checkbox" && /* @__PURE__ */ jsx(
399
- Controller,
461
+ ) })
462
+ )
463
+ }
464
+ )
465
+ ] }, field.name);
466
+ const declaredByName = new Map(supportedFields.map((f) => [f.name, f]));
467
+ const slotResolves = (name) => Boolean(builtInFields[name] || declaredByName.has(name));
468
+ const slotNode = (name) => {
469
+ const builtIn = builtInFields[name];
470
+ if (builtIn) return builtIn;
471
+ const declared = declaredByName.get(name);
472
+ return declared ? renderDeclaredField(declared) : null;
473
+ };
474
+ const unplacedFields = fieldRows ? supportedFields.filter((f) => !fieldRows.some((row) => row.some((slot) => slot.name === f.name))) : [];
475
+ const placedRows = (fieldRows ?? []).map((row) => row.filter((slot) => slotResolves(slot.name))).filter((row) => row.length > 0);
476
+ const submitButton = /* @__PURE__ */ jsx(
477
+ Button,
478
+ {
479
+ type: "submit",
480
+ loading: isSubmitting,
481
+ disabled: isSubmitting,
482
+ className: footerNote ? "md:w-60" : void 0,
483
+ children: submitLabel ?? "Confirm Booking"
484
+ }
485
+ );
486
+ return /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: FORM_STACK, noValidate: true, children: [
487
+ /* @__PURE__ */ jsx(HoneypotField, { ...honeypotInputProps }),
488
+ fieldRows ? /* @__PURE__ */ jsxs(Fragment2, { children: [
489
+ placedRows.map((row, rowIndex) => /* @__PURE__ */ jsx(
490
+ "div",
491
+ {
492
+ className: "grid grid-cols-2 gap-[var(--spacing-system-m)] md:grid-cols-4",
493
+ children: row.map((slot) => /* @__PURE__ */ jsx(
494
+ "div",
400
495
  {
401
- control,
402
- name: `formFields.${field.name}`,
403
- render: ({ field: rhf }) => (
404
- // The question's Label above already carries the text and
405
- // binds to this id — a second inline label would double the
406
- // visible text AND the accessible name.
407
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: /* @__PURE__ */ jsx(
408
- Checkbox,
409
- {
410
- id: `ms-q-${field.name}`,
411
- checked: Boolean(rhf.value),
412
- onCheckedChange: (v) => rhf.onChange(v === true)
413
- }
414
- ) })
415
- )
416
- }
417
- )
418
- ]
419
- },
420
- field.name
421
- )),
496
+ className: cn(
497
+ // A two-field row stays side by side on a phone — the rule
498
+ // the built-in name pair has always followed: two short
499
+ // fields cost one line instead of two on the layout that can
500
+ // least afford them.
501
+ row.length === 2 ? "col-span-1" : "col-span-2",
502
+ SPAN_CLASS[slot.span ?? evenSpan(row.length)]
503
+ ),
504
+ children: slotNode(slot.name)
505
+ },
506
+ slot.name
507
+ ))
508
+ },
509
+ row.map((s) => s.name).join("|") || rowIndex
510
+ )),
511
+ unplacedFields.map((field) => /* @__PURE__ */ jsx(Fragment, { children: renderDeclaredField(field) }, field.name))
512
+ ] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
513
+ builtInFields.email,
514
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-[var(--spacing-system-m)]", children: [
515
+ builtInFields.firstName,
516
+ builtInFields.lastName
517
+ ] }),
518
+ supportedFields.map((field) => /* @__PURE__ */ jsx(Fragment, { children: renderDeclaredField(field) }, field.name))
519
+ ] }),
520
+ consent && /* @__PURE__ */ jsx(
521
+ CheckboxBlock,
522
+ {
523
+ id: "ms-host-consent",
524
+ checked: consented,
525
+ onCheckedChange: (v) => {
526
+ setConsented(v);
527
+ if (v) setConsentError(null);
528
+ },
529
+ disabled: isSubmitting,
530
+ error: consentError ?? void 0,
531
+ label: consent.label,
532
+ description: consent.description
533
+ }
534
+ ),
422
535
  legalConsent && // Same out-of-flow message as the fields above, so a missed consent
423
536
  // box doesn't shove the submit button down the card. With no error the
424
537
  // wrapper is `display:contents` and the panel below is the flex item,
425
538
  // exactly as it was.
426
- /* @__PURE__ */ jsx(FieldWrapper, { error: errors.legalConsentResponses?.message, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)] rounded-md border border-ods-border p-[var(--spacing-system-m)]", children: [
427
- /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.processingConsentText }),
428
- legalConsent.communicationConsentText && /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.communicationConsentText }),
539
+ /* @__PURE__ */ jsx(FieldWrapper, { error: errors.legalConsentResponses?.message, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
429
540
  /* @__PURE__ */ jsx(
430
541
  Controller,
431
542
  {
432
543
  control,
433
544
  name: "legalConsentResponses",
434
- render: ({ field: rhf }) => /* @__PURE__ */ jsx(Fragment, { children: legalConsent.communicationConsentCheckboxes.map((box) => {
545
+ render: ({ field: rhf }) => /* @__PURE__ */ jsx(Fragment2, { children: legalConsent.communicationConsentCheckboxes.map((box, index) => {
435
546
  const responses = rhf.value ?? [];
436
547
  const current = responses.find((r) => r.communicationTypeId === box.communicationTypeId);
437
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
548
+ return (
549
+ // The design system's consent row (`checkbox-block`, the
550
+ // same block the waitlist form uses): box, label, caption
551
+ // in one bordered row. GDPR surface — HubSpot's copy is
552
+ // rendered VERBATIM, never edited. The processing statement
553
+ // rides as the caption of the FIRST row, once: it describes
554
+ // the consent as a whole, not each channel.
438
555
  /* @__PURE__ */ jsx(
439
- Checkbox,
556
+ CheckboxBlock,
440
557
  {
441
558
  id: `ms-consent-${box.communicationTypeId}`,
442
559
  checked: current?.consented ?? false,
443
560
  onCheckedChange: (v) => rhf.onChange(
444
561
  responses.map(
445
- (r) => r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v === true } : r
562
+ (r) => r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v } : r
446
563
  )
447
- )
448
- }
449
- ),
450
- /* @__PURE__ */ jsx(Label, { htmlFor: `ms-consent-${box.communicationTypeId}`, children: box.label })
451
- ] }, box.communicationTypeId);
564
+ ),
565
+ label: box.label,
566
+ description: index === 0 ? legalConsent.processingConsentText : void 0
567
+ },
568
+ box.communicationTypeId
569
+ )
570
+ );
452
571
  }) })
453
572
  }
454
573
  ),
574
+ legalConsent.communicationConsentText && /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.communicationConsentText }),
455
575
  legalConsent.privacyPolicyText && /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.privacyPolicyText })
456
576
  ] }) }),
457
- /* @__PURE__ */ jsx("div", { className: "flex", children: /* @__PURE__ */ jsx(Button, { type: "submit", loading: isSubmitting, disabled: isSubmitting, children: "Confirm Booking" }) })
577
+ footerNote ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-[var(--spacing-system-m)]", children: [
578
+ /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: footerNote }),
579
+ submitButton
580
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex", children: submitButton })
581
+ ] });
582
+ }
583
+ function BookingFormSkeleton() {
584
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)] lg:p-0", children: [
585
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-[var(--spacing-system-m)] md:grid-cols-2", children: [
586
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" }),
587
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" })
588
+ ] }),
589
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" }),
590
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[7.75rem] w-full" }),
591
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.25rem] w-full" }),
592
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-40" }) })
458
593
  ] });
459
594
  }
460
595
 
@@ -542,6 +677,7 @@ function SchedulerContextPanel({
542
677
  onBack,
543
678
  backLabel = "Back",
544
679
  locked = false,
680
+ fixedDuration = false,
545
681
  showTimezone = true,
546
682
  className
547
683
  }) {
@@ -585,8 +721,9 @@ function SchedulerContextPanel({
585
721
  " hosts on this calendar"
586
722
  ] })
587
723
  ] }),
588
- locked || durationsMs.length <= 1 ? (
589
- // Post-selection, or a link that offers one length: a static line,
724
+ locked || fixedDuration || durationsMs.length <= 1 ? (
725
+ // Post-selection, a fixed-length flow, or a link that offers one
726
+ // length: a static line,
590
727
  // which is what sits beside the host on the header-strip layouts.
591
728
  selectedDurationMs != null && /* @__PURE__ */ jsxs3("p", { className: "shrink-0 text-ods-text-secondary text-h6", children: [
592
729
  formatDurationCompact(selectedDurationMs / 1e3),
@@ -596,7 +733,7 @@ function SchedulerContextPanel({
596
733
  ] }),
597
734
  title && /* @__PURE__ */ jsx3("p", { className: "text-ods-text-primary text-h3", children: title }),
598
735
  description && /* @__PURE__ */ jsx3("p", { className: "text-ods-text-secondary text-h6", children: description }),
599
- !locked && durationsMs.length > 1 && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-[var(--spacing-system-xxs)]", children: [
736
+ !locked && !fixedDuration && durationsMs.length > 1 && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-[var(--spacing-system-xxs)]", children: [
600
737
  /* @__PURE__ */ jsx3("p", { className: FIELD_LABEL_CLASS, children: "Duration" }),
601
738
  /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-[var(--spacing-system-xs)]", children: durationsMs.map((ms) => /* @__PURE__ */ jsx3(
602
739
  Button,
@@ -639,7 +776,7 @@ function SchedulerContextPanel({
639
776
  // src/components/meeting-scheduler/slot-picker.tsx
640
777
  import { ChevronRight } from "lucide-react";
641
778
  import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
642
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
779
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
643
780
  function dayKeyInZone(ms, timeZone) {
644
781
  return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(
645
782
  new Date(ms)
@@ -750,7 +887,7 @@ function SlotPickerSkeleton({ monthOffset = 0 }) {
750
887
  ] });
751
888
  }
752
889
  function TimeChipsSkeleton() {
753
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
890
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
754
891
  /* @__PURE__ */ jsx4("div", { className: COLUMN_HEADING_CLASS, children: /* @__PURE__ */ jsx4(Skeleton, { className: "inline-block h-4 w-40 align-middle" }) }),
755
892
  /* @__PURE__ */ jsx4("div", { className: CHIP_GRID_CLASS, children: Array.from({ length: 16 }, (_, i) => (
756
893
  // h-11 md:h-12 — the loaded chip's own default-size height.
@@ -767,7 +904,8 @@ function SlotPicker({
767
904
  onSelectSlot,
768
905
  selectedDay,
769
906
  onSelectDay,
770
- isLoading = false
907
+ isLoading = false,
908
+ isSubmitting = false
771
909
  }) {
772
910
  const slotsByDay = useMemo3(() => {
773
911
  const map = /* @__PURE__ */ new Map();
@@ -801,7 +939,7 @@ function SlotPicker({
801
939
  }
802
940
  )
803
941
  ] }),
804
- /* @__PURE__ */ jsx4("div", { className: cn(COLUMN_STACK, COLUMN_BOX, "min-w-0 md:flex-1"), children: isLoading ? /* @__PURE__ */ jsx4(TimeChipsSkeleton, {}) : selectedDay ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
942
+ /* @__PURE__ */ jsx4("div", { className: cn(COLUMN_STACK, COLUMN_BOX, "min-w-0 md:flex-1"), children: isLoading ? /* @__PURE__ */ jsx4(TimeChipsSkeleton, {}) : selectedDay ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
805
943
  /* @__PURE__ */ jsx4("p", { className: COLUMN_HEADING_CLASS, children: new Intl.DateTimeFormat(void 0, {
806
944
  weekday: "long",
807
945
  month: "long",
@@ -813,7 +951,12 @@ function SlotPicker({
813
951
  Button,
814
952
  {
815
953
  variant: isSelected ? void 0 : "outline",
816
- onClick: () => onSelectSlot(ms),
954
+ disabled: isSubmitting,
955
+ loading: isSubmitting && isSelected,
956
+ onClick: () => {
957
+ if (isSubmitting) return;
958
+ onSelectSlot(ms);
959
+ },
817
960
  className: CHIP_CLASS,
818
961
  children: timeLabelInZone(ms, timezone)
819
962
  },
@@ -849,7 +992,7 @@ function SlotPicker({
849
992
 
850
993
  // src/components/meeting-scheduler/directory.tsx
851
994
  import { ChevronRight as ChevronRight2 } from "lucide-react";
852
- import { useEffect, useMemo as useMemo4, useState as useState2 } from "react";
995
+ import { useEffect as useEffect2, useMemo as useMemo4, useState as useState3 } from "react";
853
996
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
854
997
  function useNextAvailableLabel(ms) {
855
998
  const hydrated = useIsHydrated();
@@ -926,11 +1069,11 @@ function MeetingSchedulerDirectory({
926
1069
  contactHref,
927
1070
  className
928
1071
  }) {
929
- const [data, setData] = useState2(initialData);
930
- const [isLoading, setIsLoading] = useState2(initialData === null);
931
- const [error, setError] = useState2(null);
932
- const [page, setPage] = useState2(1);
933
- useEffect(() => {
1072
+ const [data, setData] = useState3(initialData);
1073
+ const [isLoading, setIsLoading] = useState3(initialData === null);
1074
+ const [error, setError] = useState3(null);
1075
+ const [page, setPage] = useState3(1);
1076
+ useEffect2(() => {
934
1077
  if (initialData !== null) return void 0;
935
1078
  const controller = new AbortController();
936
1079
  let active = true;
@@ -1019,16 +1162,18 @@ var CONTEXT_PANEL_CLASS = "p-[var(--spacing-system-l)] shrink-0 lg:w-[280px] bor
1019
1162
  var ACTION_PANEL_CLASS = "flex-1 min-w-0 flex flex-col md:min-h-0 lg:p-[var(--spacing-system-l)]";
1020
1163
  var PANEL_STEP_CLASS = "p-[var(--spacing-system-l)] md:min-h-0 md:overflow-y-auto lg:p-0";
1021
1164
  var MEETING_SCHEDULER_H = "md:h-[34.375rem] lg:h-[23.75rem]";
1165
+ var MEETING_SCHEDULER_DETAILS_FIRST_H = "md:h-[39.875rem]";
1022
1166
  var CARD_CLASS = cn(
1023
1167
  "overflow-hidden rounded-md border border-ods-border bg-ods-card",
1024
- "md:flex md:flex-col",
1025
- MEETING_SCHEDULER_H
1168
+ "md:flex md:flex-col"
1169
+ // The height is NOT baked in any more: it is chosen per flow at each render
1170
+ // site (`flowHeight`), because details-first needs a taller box and both
1171
+ // constants are module scope.
1026
1172
  );
1027
1173
  var CARD_INNER_CLASS = "flex flex-col md:min-h-0 md:flex-1 lg:h-full lg:flex-row";
1028
1174
  var CARD_DEGRADED_CLASS = cn(
1029
1175
  "flex flex-col items-start gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)]",
1030
- "md:justify-center md:overflow-y-auto",
1031
- MEETING_SCHEDULER_H
1176
+ "md:justify-center md:overflow-y-auto"
1032
1177
  );
1033
1178
  function isNativelyBookable(availability) {
1034
1179
  if (!availability.formFields.every(isSupportedFormField)) return false;
@@ -1050,7 +1195,9 @@ function HubSpotMeetingScheduler({
1050
1195
  fallbackUrl,
1051
1196
  onBack,
1052
1197
  onBooked,
1053
- className
1198
+ className,
1199
+ flow = "slot-first",
1200
+ detailsForm: DetailsForm = BookingForm
1054
1201
  }) {
1055
1202
  const {
1056
1203
  availability,
@@ -1063,8 +1210,9 @@ function HubSpotMeetingScheduler({
1063
1210
  book,
1064
1211
  isSubmitting
1065
1212
  } = useMeetingBooking({ meetingId, apiBaseUrl, initialAvailability });
1213
+ const detailsFirst = flow === "details-first";
1066
1214
  const hydrated = useIsHydrated();
1067
- const [pickedTimezone, setTimezone] = useState3(null);
1215
+ const [pickedTimezone, setTimezone] = useState4(null);
1068
1216
  const resolvedLocalTimezone = useMemo5(() => {
1069
1217
  if (!hydrated) return null;
1070
1218
  try {
@@ -1074,26 +1222,29 @@ function HubSpotMeetingScheduler({
1074
1222
  }
1075
1223
  }, [hydrated]);
1076
1224
  const timezone = pickedTimezone ?? displayTimezone ?? resolvedLocalTimezone;
1077
- const [step, setStep] = useState3("slot");
1078
- const [pickedDurationMs, setDurationMs] = useState3(null);
1079
- const [pickedDay, setSelectedDay] = useState3(null);
1080
- const [selectedSlot, setSelectedSlot] = useState3(null);
1081
- const [confirmation, setConfirmation] = useState3(null);
1082
- const [bookingError, setBookingError] = useState3(null);
1225
+ const [step, setStep] = useState4(detailsFirst ? "details" : "slot");
1226
+ const [stash, setStash] = useState4(null);
1227
+ const formOnly = detailsFirst && step === "details";
1228
+ const [pickedDurationMs, setDurationMs] = useState4(null);
1229
+ const [pickedDay, setSelectedDay] = useState4(null);
1230
+ const [selectedSlot, setSelectedSlot] = useState4(null);
1231
+ const [confirmation, setConfirmation] = useState4(null);
1232
+ const [bookingError, setBookingError] = useState4(null);
1083
1233
  const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals();
1084
1234
  const { toast } = useToast();
1085
1235
  const autoAdvanceCount = useRef2(0);
1086
- const [machineFor, setMachineFor] = useState3(meetingId);
1236
+ const [machineFor, setMachineFor] = useState4(meetingId);
1087
1237
  if (machineFor !== meetingId) {
1088
1238
  setMachineFor(meetingId);
1089
- setStep("slot");
1239
+ setStep(detailsFirst ? "details" : "slot");
1090
1240
  setDurationMs(null);
1091
1241
  setSelectedDay(null);
1092
1242
  setSelectedSlot(null);
1093
1243
  setConfirmation(null);
1094
1244
  setBookingError(null);
1245
+ setStash(null);
1095
1246
  }
1096
- useEffect2(() => {
1247
+ useEffect3(() => {
1097
1248
  autoAdvanceCount.current = 0;
1098
1249
  }, [meetingId]);
1099
1250
  const durations = useMemo5(() => availability?.durationsMs ?? [], [availability?.durationsMs]);
@@ -1116,7 +1267,7 @@ function HubSpotMeetingScheduler({
1116
1267
  if (pickedDay && dayKeys.has(pickedDay)) return pickedDay;
1117
1268
  return dayKeyInZone(inMonth[0], timezone);
1118
1269
  }, [step, timezone, slots, pickedDay, monthOffset]);
1119
- useEffect2(() => {
1270
+ useEffect3(() => {
1120
1271
  if (step === "slot" && availability && !isFetchingAvailability && durationMs != null && slots.length === 0 && availability.hasMore && autoAdvanceCount.current < 3) {
1121
1272
  autoAdvanceCount.current += 1;
1122
1273
  setMonthOffset(monthOffset + 1);
@@ -1126,10 +1277,30 @@ function HubSpotMeetingScheduler({
1126
1277
  setStep("slot");
1127
1278
  setBookingError(null);
1128
1279
  }, []);
1280
+ const backToDetails = useCallback3(() => {
1281
+ setStep("details");
1282
+ setBookingError(null);
1283
+ }, []);
1284
+ const stashDetails = useCallback3(
1285
+ (payload) => {
1286
+ setBookingError(null);
1287
+ setStash({ payload, signals: getSignals() });
1288
+ setStep("slot");
1289
+ autoAdvanceCount.current = 0;
1290
+ void refetchAvailability();
1291
+ return Promise.resolve();
1292
+ },
1293
+ [getSignals, refetchAvailability]
1294
+ );
1129
1295
  const handleSubmit = useCallback3(
1130
1296
  async (payload) => {
1297
+ const priorError = bookingError;
1131
1298
  setBookingError(null);
1132
1299
  const result = await book(payload);
1300
+ if (!result.ok && result.message === "Already submitting") {
1301
+ setBookingError(priorError);
1302
+ return;
1303
+ }
1133
1304
  if (result.ok && result.confirmation) {
1134
1305
  setConfirmation(result.confirmation);
1135
1306
  setStep("confirmed");
@@ -1141,8 +1312,10 @@ function HubSpotMeetingScheduler({
1141
1312
  if (code === "SLOT_TAKEN") {
1142
1313
  setSelectedSlot(null);
1143
1314
  setStep("slot");
1144
- resetSignals();
1315
+ if (!detailsFirst) resetSignals();
1145
1316
  void refetchAvailability();
1317
+ } else if (detailsFirst && (code === "VALIDATION" || code === "INVALID_EMAIL")) {
1318
+ setStep("details");
1146
1319
  } else {
1147
1320
  toast({
1148
1321
  variant: "error",
@@ -1151,29 +1324,31 @@ function HubSpotMeetingScheduler({
1151
1324
  });
1152
1325
  }
1153
1326
  },
1154
- [book, onBooked, refetchAvailability, resetSignals, toast]
1327
+ [book, bookingError, detailsFirst, onBooked, refetchAvailability, resetSignals, toast]
1155
1328
  );
1156
1329
  const escapeHatch = fallbackUrl ? /* @__PURE__ */ jsx6(Button, { variant: "outline", size: "small-legacy", href: fallbackUrl, openInNewTab: true, children: "Open in HubSpot" }) : null;
1330
+ const flowHeight = detailsFirst ? MEETING_SCHEDULER_DETAILS_FIRST_H : MEETING_SCHEDULER_H;
1331
+ const unseededDetailsFirstFailure = detailsFirst && step === "details" && !availability && !initialAvailability && !hydrated;
1332
+ const degradedCard = /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, flowHeight, className), children: [
1333
+ /* @__PURE__ */ jsx6("p", { className: "text-ods-text-secondary text-h6", children: "We couldn't load available call times. Please try again shortly." }),
1334
+ escapeHatch
1335
+ ] });
1336
+ if (unseededDetailsFirstFailure) return degradedCard;
1157
1337
  if (isLoadingAvailability && !availability) {
1158
- return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1159
- /* @__PURE__ */ jsx6(ContextPanelSkeleton, { onBack, className: CONTEXT_PANEL_CLASS }),
1160
- /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: /* @__PURE__ */ jsx6(SlotPickerSkeleton, { monthOffset }) })
1338
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, flowHeight, formOnly && "bg-ods-bg", className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1339
+ !formOnly && /* @__PURE__ */ jsx6(ContextPanelSkeleton, { onBack, className: CONTEXT_PANEL_CLASS }),
1340
+ /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: detailsFirst && step === "details" ? /* @__PURE__ */ jsx6(BookingFormSkeleton, {}) : /* @__PURE__ */ jsx6(SlotPickerSkeleton, { monthOffset }) })
1161
1341
  ] }) });
1162
1342
  }
1163
- if (availabilityError || !availability) {
1164
- return /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, className), children: [
1165
- /* @__PURE__ */ jsx6("p", { className: "text-ods-text-secondary text-h6", children: "We couldn't load available call times. Please try again shortly." }),
1166
- escapeHatch
1167
- ] });
1168
- }
1343
+ if (availabilityError || !availability) return degradedCard;
1169
1344
  if (!isNativelyBookable(availability)) {
1170
- return /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, className), children: [
1345
+ return /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, flowHeight, className), children: [
1171
1346
  /* @__PURE__ */ jsx6("p", { className: "text-ods-text-secondary text-h6", children: "This meeting type is booked directly on HubSpot." }),
1172
1347
  escapeHatch
1173
1348
  ] });
1174
1349
  }
1175
- return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1176
- /* @__PURE__ */ jsx6(
1350
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, flowHeight, formOnly && "bg-ods-bg", className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1351
+ !formOnly && /* @__PURE__ */ jsx6(
1177
1352
  SchedulerContextPanel,
1178
1353
  {
1179
1354
  hosts: shownHosts,
@@ -1185,18 +1360,24 @@ function HubSpotMeetingScheduler({
1185
1360
  setDurationMs(ms);
1186
1361
  setSelectedDay(null);
1187
1362
  setSelectedSlot(null);
1188
- if (step === "details") setStep("slot");
1363
+ if (!detailsFirst && step === "details") setStep("slot");
1189
1364
  },
1190
1365
  timezone,
1191
1366
  onTimezoneChange: setTimezone,
1192
- onBack: step === "details" ? backToSlot : onBack,
1193
- locked: step !== "slot",
1367
+ onBack: detailsFirst ? step === "slot" ? backToDetails : onBack : step === "details" ? backToSlot : onBack,
1368
+ locked: detailsFirst ? step === "confirmed" || isSubmitting : step !== "slot",
1194
1369
  showTimezone: step !== "confirmed",
1370
+ fixedDuration: detailsFirst,
1195
1371
  className: CONTEXT_PANEL_CLASS
1196
1372
  }
1197
1373
  ),
1198
- /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: step === "confirmed" && confirmation && timezone ? /* @__PURE__ */ jsx6("div", { className: cn("flex flex-1 flex-col", PANEL_STEP_CLASS), children: /* @__PURE__ */ jsx6(Confirmation, { confirmation, timezone }) }) : step === "details" && durationMs != null && selectedSlot != null && timezone ? /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-1 flex-col gap-[var(--spacing-system-m)]", PANEL_STEP_CLASS), children: [
1199
- /* @__PURE__ */ jsxs6("p", { className: "text-ods-text-primary text-h4", children: [
1374
+ /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: step === "confirmed" && confirmation && timezone ? /* @__PURE__ */ jsx6("div", { className: cn("flex flex-1 flex-col", PANEL_STEP_CLASS), children: /* @__PURE__ */ jsx6(Confirmation, { confirmation, timezone }) }) : step === "details" && (detailsFirst ? (
1375
+ // The form is step ONE here: no slot yet, and `timezone` is null
1376
+ // on the server render, so neither may gate the PANEL. A link
1377
+ // publishing no durations has no form worth showing.
1378
+ durations.length > 0
1379
+ ) : durationMs != null && selectedSlot != null && timezone) ? /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-1 flex-col gap-[var(--spacing-system-m)]", PANEL_STEP_CLASS), children: [
1380
+ selectedSlot != null && durationMs != null && timezone != null && /* @__PURE__ */ jsxs6("p", { className: "text-ods-text-primary text-h4", children: [
1200
1381
  new Intl.DateTimeFormat(void 0, {
1201
1382
  timeZone: timezone,
1202
1383
  weekday: "long",
@@ -1211,15 +1392,19 @@ function HubSpotMeetingScheduler({
1211
1392
  formatDurationCompact(durationMs / 1e3)
1212
1393
  ] }),
1213
1394
  /* @__PURE__ */ jsx6(
1214
- BookingForm,
1395
+ DetailsForm,
1215
1396
  {
1216
1397
  availability,
1217
1398
  meetingId,
1218
- startTimeMs: selectedSlot,
1219
- durationMs,
1399
+ startTimeMs: selectedSlot ?? void 0,
1400
+ durationMs: durationMs ?? void 0,
1220
1401
  timezone,
1402
+ deferSlot: detailsFirst,
1403
+ submitLabel: detailsFirst ? "Continue" : void 0,
1404
+ footerNote: detailsFirst ? "Next step: pick a slot that works. Calendar invite lands right after." : void 0,
1405
+ initialValues: detailsFirst ? stash?.payload : void 0,
1221
1406
  isSubmitting,
1222
- onSubmit: handleSubmit,
1407
+ onSubmit: detailsFirst ? stashDetails : handleSubmit,
1223
1408
  honeypotInputProps,
1224
1409
  getSignals
1225
1410
  }
@@ -1250,12 +1435,31 @@ function HubSpotMeetingScheduler({
1250
1435
  },
1251
1436
  selectedSlot,
1252
1437
  onSelectSlot: (ms) => {
1438
+ if (!detailsFirst) {
1439
+ setSelectedSlot(ms);
1440
+ setStep("details");
1441
+ return;
1442
+ }
1443
+ if (isSubmitting || !stash) return;
1253
1444
  setSelectedSlot(ms);
1254
- setStep("details");
1445
+ void handleSubmit({
1446
+ ...stash.payload,
1447
+ ...stash.signals,
1448
+ meetingId,
1449
+ // `ms`, NOT `selectedSlot` — the setState above has not
1450
+ // committed inside this handler, so state holds null on
1451
+ // the first booking and the PREVIOUS slot on a retry.
1452
+ startTimeMs: ms,
1453
+ // Live values: both stay editable after Continue, so the
1454
+ // copies frozen at that point can be stale.
1455
+ durationMs,
1456
+ timezone
1457
+ });
1255
1458
  },
1256
1459
  selectedDay,
1257
1460
  onSelectDay: setSelectedDay,
1258
- isLoading: isLoadingAvailability
1461
+ isLoading: isLoadingAvailability,
1462
+ isSubmitting: detailsFirst ? isSubmitting : void 0
1259
1463
  }
1260
1464
  )
1261
1465
  ) : durations.length === 0 && !isFetchingAvailability ? (
@@ -1276,7 +1480,10 @@ function HubSpotMeetingScheduler({
1276
1480
  ] }) });
1277
1481
  }
1278
1482
  export {
1483
+ BookingForm,
1484
+ BookingFormSkeleton,
1279
1485
  HubSpotMeetingScheduler,
1486
+ MEETING_SCHEDULER_DETAILS_FIRST_H,
1280
1487
  MEETING_SCHEDULER_H,
1281
1488
  MeetingSchedulerDirectory,
1282
1489
  MeetingSchedulerDirectoryRowSkeleton