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

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 +49 -5
  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 +369 -186
  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 +344 -161
  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 +371 -152
  131. package/src/components/meeting-scheduler/context-panel.tsx +13 -3
  132. package/src/components/meeting-scheduler/index.tsx +316 -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 useState3 } 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,27 @@ 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 } 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,
280
300
  isSubmitting,
281
301
  onSubmit,
282
302
  honeypotInputProps,
@@ -284,31 +304,57 @@ function BookingForm({
284
304
  }) {
285
305
  const { formFields, legalConsent } = availability;
286
306
  const supportedFields = useMemo(() => formFields.filter(isSupportedFormField), [formFields]);
287
- const schema = useMemo(() => makeBookingSchema(supportedFields, legalConsent), [supportedFields, legalConsent]);
307
+ const schema = useMemo(
308
+ () => makeDeferredBookingSchema(supportedFields, legalConsent),
309
+ [supportedFields, legalConsent]
310
+ );
311
+ const consentDefaults = useMemo(
312
+ () => (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({
313
+ communicationTypeId: c.communicationTypeId,
314
+ consented: false
315
+ })),
316
+ [legalConsent]
317
+ );
288
318
  const {
289
319
  register,
290
320
  control,
291
321
  handleSubmit,
322
+ setValue,
292
323
  formState: { errors }
293
324
  } = useForm({
294
325
  resolver: zodResolver(schema),
295
326
  defaultValues: {
296
327
  meetingId,
297
- startTimeMs,
298
- durationMs,
328
+ // Genuinely OMITTED, not present-as-undefined, when the slot is deferred.
329
+ ...deferSlot ? {} : { startTimeMs, durationMs },
299
330
  timezone,
300
331
  locale: typeof navigator !== "undefined" ? navigator.language : void 0,
301
332
  firstName: "",
302
333
  lastName: "",
303
334
  email: "",
304
335
  formFields: {},
305
- legalConsentResponses: (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({
306
- communicationTypeId: c.communicationTypeId,
307
- consented: false
308
- }))
336
+ legalConsentResponses: consentDefaults,
337
+ // A remount (back edge, or an error return) would otherwise lose every
338
+ // answer — `defaultValues` is snapshotted once and never re-read.
339
+ ...initialValues ?? {}
309
340
  }
310
341
  });
342
+ const priorConsents = initialValues?.legalConsentResponses;
343
+ useEffect(() => {
344
+ setValue(
345
+ "legalConsentResponses",
346
+ consentDefaults.map((next) => ({
347
+ ...next,
348
+ consented: priorConsents?.find((r) => r.communicationTypeId === next.communicationTypeId)?.consented ?? false
349
+ })),
350
+ { shouldDirty: false }
351
+ );
352
+ }, [consentDefaults, priorConsents, setValue]);
311
353
  const submit = handleSubmit(async (data) => {
354
+ if (deferSlot) {
355
+ await onSubmit({ ...data, meetingId });
356
+ return;
357
+ }
312
358
  await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() });
313
359
  });
314
360
  const fieldError = (name) => {
@@ -317,9 +363,8 @@ function BookingForm({
317
363
  };
318
364
  const inputClass = "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12";
319
365
  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(
366
+ const builtInFields = {
367
+ email: /* @__PURE__ */ jsx(FieldWrapper, { label: "Email", htmlFor: "ms-email", error: errors.email?.message, children: /* @__PURE__ */ jsx(
323
368
  Input,
324
369
  {
325
370
  id: "ms-email",
@@ -332,129 +377,195 @@ function BookingForm({
332
377
  ...register("email")
333
378
  }
334
379
  ) }),
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,
380
+ firstName: /* @__PURE__ */ jsx(FieldWrapper, { label: "First Name", htmlFor: "ms-first-name", error: errors.firstName?.message, children: /* @__PURE__ */ jsx(
381
+ Input,
363
382
  {
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,
383
+ id: "ms-first-name",
384
+ required: true,
385
+ autoComplete: "given-name",
386
+ placeholder: "Jane",
387
+ "aria-invalid": !!errors.firstName,
388
+ className: inputClass,
389
+ ...register("firstName")
390
+ }
391
+ ) }),
392
+ lastName: /* @__PURE__ */ jsx(FieldWrapper, { label: "Last Name", htmlFor: "ms-last-name", error: errors.lastName?.message, children: /* @__PURE__ */ jsx(
393
+ Input,
394
+ {
395
+ id: "ms-last-name",
396
+ required: true,
397
+ autoComplete: "family-name",
398
+ placeholder: "Doe",
399
+ "aria-invalid": !!errors.lastName,
400
+ className: inputClass,
401
+ ...register("lastName")
402
+ }
403
+ ) })
404
+ };
405
+ const renderDeclaredField = (field) => /* @__PURE__ */ jsxs(FieldWrapper, { label: field.label, htmlFor: `ms-q-${field.name}`, error: fieldError(field.name), children: [
406
+ field.type === "textarea" && /* @__PURE__ */ jsx(
407
+ Textarea,
408
+ {
409
+ id: `ms-q-${field.name}`,
410
+ className: "border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary",
411
+ ...register(`formFields.${field.name}`)
412
+ }
413
+ ),
414
+ field.type === "text" && /* @__PURE__ */ jsx(
415
+ Input,
416
+ {
417
+ id: `ms-q-${field.name}`,
418
+ className: "h-11 border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary md:h-12",
419
+ ...register(`formFields.${field.name}`)
420
+ }
421
+ ),
422
+ (field.type === "select" || field.type === "radio") && /* @__PURE__ */ jsx(
423
+ Controller,
424
+ {
425
+ control,
426
+ name: `formFields.${field.name}`,
427
+ render: ({ field: rhf }) => field.type === "select" ? /* @__PURE__ */ jsxs(Select, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: [
428
+ /* @__PURE__ */ jsx(SelectTrigger, { id: `ms-q-${field.name}`, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
429
+ /* @__PURE__ */ jsx(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt, children: opt }, opt)) })
430
+ ] }) : /* @__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: [
431
+ /* @__PURE__ */ jsx(RadioGroupItem, { id: `ms-q-${field.name}-${opt}`, value: opt }),
432
+ /* @__PURE__ */ jsx(Label, { htmlFor: `ms-q-${field.name}-${opt}`, children: opt })
433
+ ] }, opt)) })
434
+ }
435
+ ),
436
+ field.type === "checkbox" && /* @__PURE__ */ jsx(
437
+ Controller,
438
+ {
439
+ control,
440
+ name: `formFields.${field.name}`,
441
+ render: ({ field: rhf }) => (
442
+ // The question's Label above already carries the text and
443
+ // binds to this id — a second inline label would double the
444
+ // visible text AND the accessible name.
445
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: /* @__PURE__ */ jsx(
446
+ Checkbox,
378
447
  {
379
448
  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}`)
449
+ checked: Boolean(rhf.value),
450
+ onCheckedChange: (v) => rhf.onChange(v === true)
382
451
  }
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)) })
396
- }
397
- ),
398
- field.type === "checkbox" && /* @__PURE__ */ jsx(
399
- Controller,
452
+ ) })
453
+ )
454
+ }
455
+ )
456
+ ] }, field.name);
457
+ const declaredByName = new Map(supportedFields.map((f) => [f.name, f]));
458
+ const slotResolves = (name) => Boolean(builtInFields[name] || declaredByName.has(name));
459
+ const slotNode = (name) => {
460
+ const builtIn = builtInFields[name];
461
+ if (builtIn) return builtIn;
462
+ const declared = declaredByName.get(name);
463
+ return declared ? renderDeclaredField(declared) : null;
464
+ };
465
+ const unplacedFields = fieldRows ? supportedFields.filter((f) => !fieldRows.some((row) => row.some((slot) => slot.name === f.name))) : [];
466
+ const placedRows = (fieldRows ?? []).map((row) => row.filter((slot) => slotResolves(slot.name))).filter((row) => row.length > 0);
467
+ const submitButton = /* @__PURE__ */ jsx(
468
+ Button,
469
+ {
470
+ type: "submit",
471
+ loading: isSubmitting,
472
+ disabled: isSubmitting,
473
+ className: footerNote ? "md:w-60" : void 0,
474
+ children: submitLabel ?? "Confirm Booking"
475
+ }
476
+ );
477
+ return /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: FORM_STACK, noValidate: true, children: [
478
+ /* @__PURE__ */ jsx(HoneypotField, { ...honeypotInputProps }),
479
+ fieldRows ? /* @__PURE__ */ jsxs(Fragment2, { children: [
480
+ placedRows.map((row, rowIndex) => /* @__PURE__ */ jsx(
481
+ "div",
482
+ {
483
+ className: "grid grid-cols-2 gap-[var(--spacing-system-m)] md:grid-cols-4",
484
+ children: row.map((slot) => /* @__PURE__ */ jsx(
485
+ "div",
400
486
  {
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
- )),
487
+ className: cn(
488
+ // A two-field row stays side by side on a phone — the rule
489
+ // the built-in name pair has always followed: two short
490
+ // fields cost one line instead of two on the layout that can
491
+ // least afford them.
492
+ row.length === 2 ? "col-span-1" : "col-span-2",
493
+ SPAN_CLASS[slot.span ?? evenSpan(row.length)]
494
+ ),
495
+ children: slotNode(slot.name)
496
+ },
497
+ slot.name
498
+ ))
499
+ },
500
+ row.map((s) => s.name).join("|") || rowIndex
501
+ )),
502
+ unplacedFields.map((field) => /* @__PURE__ */ jsx(Fragment, { children: renderDeclaredField(field) }, field.name))
503
+ ] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
504
+ builtInFields.email,
505
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-[var(--spacing-system-m)]", children: [
506
+ builtInFields.firstName,
507
+ builtInFields.lastName
508
+ ] }),
509
+ supportedFields.map((field) => /* @__PURE__ */ jsx(Fragment, { children: renderDeclaredField(field) }, field.name))
510
+ ] }),
422
511
  legalConsent && // Same out-of-flow message as the fields above, so a missed consent
423
512
  // box doesn't shove the submit button down the card. With no error the
424
513
  // wrapper is `display:contents` and the panel below is the flex item,
425
514
  // 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 }),
515
+ /* @__PURE__ */ jsx(FieldWrapper, { error: errors.legalConsentResponses?.message, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
429
516
  /* @__PURE__ */ jsx(
430
517
  Controller,
431
518
  {
432
519
  control,
433
520
  name: "legalConsentResponses",
434
- render: ({ field: rhf }) => /* @__PURE__ */ jsx(Fragment, { children: legalConsent.communicationConsentCheckboxes.map((box) => {
521
+ render: ({ field: rhf }) => /* @__PURE__ */ jsx(Fragment2, { children: legalConsent.communicationConsentCheckboxes.map((box, index) => {
435
522
  const responses = rhf.value ?? [];
436
523
  const current = responses.find((r) => r.communicationTypeId === box.communicationTypeId);
437
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
524
+ return (
525
+ // The design system's consent row (`checkbox-block`, the
526
+ // same block the waitlist form uses): box, label, caption
527
+ // in one bordered row. GDPR surface — HubSpot's copy is
528
+ // rendered VERBATIM, never edited. The processing statement
529
+ // rides as the caption of the FIRST row, once: it describes
530
+ // the consent as a whole, not each channel.
438
531
  /* @__PURE__ */ jsx(
439
- Checkbox,
532
+ CheckboxBlock,
440
533
  {
441
534
  id: `ms-consent-${box.communicationTypeId}`,
442
535
  checked: current?.consented ?? false,
443
536
  onCheckedChange: (v) => rhf.onChange(
444
537
  responses.map(
445
- (r) => r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v === true } : r
538
+ (r) => r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v } : r
446
539
  )
447
- )
448
- }
449
- ),
450
- /* @__PURE__ */ jsx(Label, { htmlFor: `ms-consent-${box.communicationTypeId}`, children: box.label })
451
- ] }, box.communicationTypeId);
540
+ ),
541
+ label: box.label,
542
+ description: index === 0 ? legalConsent.processingConsentText : void 0
543
+ },
544
+ box.communicationTypeId
545
+ )
546
+ );
452
547
  }) })
453
548
  }
454
549
  ),
550
+ legalConsent.communicationConsentText && /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.communicationConsentText }),
455
551
  legalConsent.privacyPolicyText && /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: legalConsent.privacyPolicyText })
456
552
  ] }) }),
457
- /* @__PURE__ */ jsx("div", { className: "flex", children: /* @__PURE__ */ jsx(Button, { type: "submit", loading: isSubmitting, disabled: isSubmitting, children: "Confirm Booking" }) })
553
+ footerNote ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-[var(--spacing-system-m)]", children: [
554
+ /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-h6", children: footerNote }),
555
+ submitButton
556
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex", children: submitButton })
557
+ ] });
558
+ }
559
+ function BookingFormSkeleton() {
560
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)] lg:p-0", children: [
561
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-[var(--spacing-system-m)] md:grid-cols-2", children: [
562
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" }),
563
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" })
564
+ ] }),
565
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.75rem] w-full" }),
566
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[7.75rem] w-full" }),
567
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-[4.25rem] w-full" }),
568
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-40" }) })
458
569
  ] });
459
570
  }
460
571
 
@@ -542,6 +653,7 @@ function SchedulerContextPanel({
542
653
  onBack,
543
654
  backLabel = "Back",
544
655
  locked = false,
656
+ fixedDuration = false,
545
657
  showTimezone = true,
546
658
  className
547
659
  }) {
@@ -585,8 +697,9 @@ function SchedulerContextPanel({
585
697
  " hosts on this calendar"
586
698
  ] })
587
699
  ] }),
588
- locked || durationsMs.length <= 1 ? (
589
- // Post-selection, or a link that offers one length: a static line,
700
+ locked || fixedDuration || durationsMs.length <= 1 ? (
701
+ // Post-selection, a fixed-length flow, or a link that offers one
702
+ // length: a static line,
590
703
  // which is what sits beside the host on the header-strip layouts.
591
704
  selectedDurationMs != null && /* @__PURE__ */ jsxs3("p", { className: "shrink-0 text-ods-text-secondary text-h6", children: [
592
705
  formatDurationCompact(selectedDurationMs / 1e3),
@@ -596,7 +709,7 @@ function SchedulerContextPanel({
596
709
  ] }),
597
710
  title && /* @__PURE__ */ jsx3("p", { className: "text-ods-text-primary text-h3", children: title }),
598
711
  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: [
712
+ !locked && !fixedDuration && durationsMs.length > 1 && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-[var(--spacing-system-xxs)]", children: [
600
713
  /* @__PURE__ */ jsx3("p", { className: FIELD_LABEL_CLASS, children: "Duration" }),
601
714
  /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-[var(--spacing-system-xs)]", children: durationsMs.map((ms) => /* @__PURE__ */ jsx3(
602
715
  Button,
@@ -639,7 +752,7 @@ function SchedulerContextPanel({
639
752
  // src/components/meeting-scheduler/slot-picker.tsx
640
753
  import { ChevronRight } from "lucide-react";
641
754
  import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
642
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
755
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
643
756
  function dayKeyInZone(ms, timeZone) {
644
757
  return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(
645
758
  new Date(ms)
@@ -750,7 +863,7 @@ function SlotPickerSkeleton({ monthOffset = 0 }) {
750
863
  ] });
751
864
  }
752
865
  function TimeChipsSkeleton() {
753
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
866
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
754
867
  /* @__PURE__ */ jsx4("div", { className: COLUMN_HEADING_CLASS, children: /* @__PURE__ */ jsx4(Skeleton, { className: "inline-block h-4 w-40 align-middle" }) }),
755
868
  /* @__PURE__ */ jsx4("div", { className: CHIP_GRID_CLASS, children: Array.from({ length: 16 }, (_, i) => (
756
869
  // h-11 md:h-12 — the loaded chip's own default-size height.
@@ -767,7 +880,8 @@ function SlotPicker({
767
880
  onSelectSlot,
768
881
  selectedDay,
769
882
  onSelectDay,
770
- isLoading = false
883
+ isLoading = false,
884
+ isSubmitting = false
771
885
  }) {
772
886
  const slotsByDay = useMemo3(() => {
773
887
  const map = /* @__PURE__ */ new Map();
@@ -801,7 +915,7 @@ function SlotPicker({
801
915
  }
802
916
  )
803
917
  ] }),
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: [
918
+ /* @__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
919
  /* @__PURE__ */ jsx4("p", { className: COLUMN_HEADING_CLASS, children: new Intl.DateTimeFormat(void 0, {
806
920
  weekday: "long",
807
921
  month: "long",
@@ -813,7 +927,12 @@ function SlotPicker({
813
927
  Button,
814
928
  {
815
929
  variant: isSelected ? void 0 : "outline",
816
- onClick: () => onSelectSlot(ms),
930
+ disabled: isSubmitting,
931
+ loading: isSubmitting && isSelected,
932
+ onClick: () => {
933
+ if (isSubmitting) return;
934
+ onSelectSlot(ms);
935
+ },
817
936
  className: CHIP_CLASS,
818
937
  children: timeLabelInZone(ms, timezone)
819
938
  },
@@ -849,7 +968,7 @@ function SlotPicker({
849
968
 
850
969
  // src/components/meeting-scheduler/directory.tsx
851
970
  import { ChevronRight as ChevronRight2 } from "lucide-react";
852
- import { useEffect, useMemo as useMemo4, useState as useState2 } from "react";
971
+ import { useEffect as useEffect2, useMemo as useMemo4, useState as useState2 } from "react";
853
972
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
854
973
  function useNextAvailableLabel(ms) {
855
974
  const hydrated = useIsHydrated();
@@ -930,7 +1049,7 @@ function MeetingSchedulerDirectory({
930
1049
  const [isLoading, setIsLoading] = useState2(initialData === null);
931
1050
  const [error, setError] = useState2(null);
932
1051
  const [page, setPage] = useState2(1);
933
- useEffect(() => {
1052
+ useEffect2(() => {
934
1053
  if (initialData !== null) return void 0;
935
1054
  const controller = new AbortController();
936
1055
  let active = true;
@@ -1019,16 +1138,18 @@ var CONTEXT_PANEL_CLASS = "p-[var(--spacing-system-l)] shrink-0 lg:w-[280px] bor
1019
1138
  var ACTION_PANEL_CLASS = "flex-1 min-w-0 flex flex-col md:min-h-0 lg:p-[var(--spacing-system-l)]";
1020
1139
  var PANEL_STEP_CLASS = "p-[var(--spacing-system-l)] md:min-h-0 md:overflow-y-auto lg:p-0";
1021
1140
  var MEETING_SCHEDULER_H = "md:h-[34.375rem] lg:h-[23.75rem]";
1141
+ var MEETING_SCHEDULER_DETAILS_FIRST_H = "md:h-[39.875rem]";
1022
1142
  var CARD_CLASS = cn(
1023
1143
  "overflow-hidden rounded-md border border-ods-border bg-ods-card",
1024
- "md:flex md:flex-col",
1025
- MEETING_SCHEDULER_H
1144
+ "md:flex md:flex-col"
1145
+ // The height is NOT baked in any more: it is chosen per flow at each render
1146
+ // site (`flowHeight`), because details-first needs a taller box and both
1147
+ // constants are module scope.
1026
1148
  );
1027
1149
  var CARD_INNER_CLASS = "flex flex-col md:min-h-0 md:flex-1 lg:h-full lg:flex-row";
1028
1150
  var CARD_DEGRADED_CLASS = cn(
1029
1151
  "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
1152
+ "md:justify-center md:overflow-y-auto"
1032
1153
  );
1033
1154
  function isNativelyBookable(availability) {
1034
1155
  if (!availability.formFields.every(isSupportedFormField)) return false;
@@ -1050,7 +1171,9 @@ function HubSpotMeetingScheduler({
1050
1171
  fallbackUrl,
1051
1172
  onBack,
1052
1173
  onBooked,
1053
- className
1174
+ className,
1175
+ flow = "slot-first",
1176
+ detailsForm: DetailsForm = BookingForm
1054
1177
  }) {
1055
1178
  const {
1056
1179
  availability,
@@ -1063,6 +1186,7 @@ function HubSpotMeetingScheduler({
1063
1186
  book,
1064
1187
  isSubmitting
1065
1188
  } = useMeetingBooking({ meetingId, apiBaseUrl, initialAvailability });
1189
+ const detailsFirst = flow === "details-first";
1066
1190
  const hydrated = useIsHydrated();
1067
1191
  const [pickedTimezone, setTimezone] = useState3(null);
1068
1192
  const resolvedLocalTimezone = useMemo5(() => {
@@ -1074,7 +1198,9 @@ function HubSpotMeetingScheduler({
1074
1198
  }
1075
1199
  }, [hydrated]);
1076
1200
  const timezone = pickedTimezone ?? displayTimezone ?? resolvedLocalTimezone;
1077
- const [step, setStep] = useState3("slot");
1201
+ const [step, setStep] = useState3(detailsFirst ? "details" : "slot");
1202
+ const [stash, setStash] = useState3(null);
1203
+ const formOnly = detailsFirst && step === "details";
1078
1204
  const [pickedDurationMs, setDurationMs] = useState3(null);
1079
1205
  const [pickedDay, setSelectedDay] = useState3(null);
1080
1206
  const [selectedSlot, setSelectedSlot] = useState3(null);
@@ -1086,14 +1212,15 @@ function HubSpotMeetingScheduler({
1086
1212
  const [machineFor, setMachineFor] = useState3(meetingId);
1087
1213
  if (machineFor !== meetingId) {
1088
1214
  setMachineFor(meetingId);
1089
- setStep("slot");
1215
+ setStep(detailsFirst ? "details" : "slot");
1090
1216
  setDurationMs(null);
1091
1217
  setSelectedDay(null);
1092
1218
  setSelectedSlot(null);
1093
1219
  setConfirmation(null);
1094
1220
  setBookingError(null);
1221
+ setStash(null);
1095
1222
  }
1096
- useEffect2(() => {
1223
+ useEffect3(() => {
1097
1224
  autoAdvanceCount.current = 0;
1098
1225
  }, [meetingId]);
1099
1226
  const durations = useMemo5(() => availability?.durationsMs ?? [], [availability?.durationsMs]);
@@ -1116,7 +1243,7 @@ function HubSpotMeetingScheduler({
1116
1243
  if (pickedDay && dayKeys.has(pickedDay)) return pickedDay;
1117
1244
  return dayKeyInZone(inMonth[0], timezone);
1118
1245
  }, [step, timezone, slots, pickedDay, monthOffset]);
1119
- useEffect2(() => {
1246
+ useEffect3(() => {
1120
1247
  if (step === "slot" && availability && !isFetchingAvailability && durationMs != null && slots.length === 0 && availability.hasMore && autoAdvanceCount.current < 3) {
1121
1248
  autoAdvanceCount.current += 1;
1122
1249
  setMonthOffset(monthOffset + 1);
@@ -1126,10 +1253,30 @@ function HubSpotMeetingScheduler({
1126
1253
  setStep("slot");
1127
1254
  setBookingError(null);
1128
1255
  }, []);
1256
+ const backToDetails = useCallback3(() => {
1257
+ setStep("details");
1258
+ setBookingError(null);
1259
+ }, []);
1260
+ const stashDetails = useCallback3(
1261
+ (payload) => {
1262
+ setBookingError(null);
1263
+ setStash({ payload, signals: getSignals() });
1264
+ setStep("slot");
1265
+ autoAdvanceCount.current = 0;
1266
+ void refetchAvailability();
1267
+ return Promise.resolve();
1268
+ },
1269
+ [getSignals, refetchAvailability]
1270
+ );
1129
1271
  const handleSubmit = useCallback3(
1130
1272
  async (payload) => {
1273
+ const priorError = bookingError;
1131
1274
  setBookingError(null);
1132
1275
  const result = await book(payload);
1276
+ if (!result.ok && result.message === "Already submitting") {
1277
+ setBookingError(priorError);
1278
+ return;
1279
+ }
1133
1280
  if (result.ok && result.confirmation) {
1134
1281
  setConfirmation(result.confirmation);
1135
1282
  setStep("confirmed");
@@ -1141,8 +1288,10 @@ function HubSpotMeetingScheduler({
1141
1288
  if (code === "SLOT_TAKEN") {
1142
1289
  setSelectedSlot(null);
1143
1290
  setStep("slot");
1144
- resetSignals();
1291
+ if (!detailsFirst) resetSignals();
1145
1292
  void refetchAvailability();
1293
+ } else if (detailsFirst && (code === "VALIDATION" || code === "INVALID_EMAIL")) {
1294
+ setStep("details");
1146
1295
  } else {
1147
1296
  toast({
1148
1297
  variant: "error",
@@ -1151,29 +1300,31 @@ function HubSpotMeetingScheduler({
1151
1300
  });
1152
1301
  }
1153
1302
  },
1154
- [book, onBooked, refetchAvailability, resetSignals, toast]
1303
+ [book, bookingError, detailsFirst, onBooked, refetchAvailability, resetSignals, toast]
1155
1304
  );
1156
1305
  const escapeHatch = fallbackUrl ? /* @__PURE__ */ jsx6(Button, { variant: "outline", size: "small-legacy", href: fallbackUrl, openInNewTab: true, children: "Open in HubSpot" }) : null;
1306
+ const flowHeight = detailsFirst ? MEETING_SCHEDULER_DETAILS_FIRST_H : MEETING_SCHEDULER_H;
1307
+ const unseededDetailsFirstFailure = detailsFirst && step === "details" && !availability && !initialAvailability && !hydrated;
1308
+ const degradedCard = /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, flowHeight, className), children: [
1309
+ /* @__PURE__ */ jsx6("p", { className: "text-ods-text-secondary text-h6", children: "We couldn't load available call times. Please try again shortly." }),
1310
+ escapeHatch
1311
+ ] });
1312
+ if (unseededDetailsFirstFailure) return degradedCard;
1157
1313
  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 }) })
1314
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, flowHeight, formOnly && "bg-ods-bg", className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1315
+ !formOnly && /* @__PURE__ */ jsx6(ContextPanelSkeleton, { onBack, className: CONTEXT_PANEL_CLASS }),
1316
+ /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: detailsFirst && step === "details" ? /* @__PURE__ */ jsx6(BookingFormSkeleton, {}) : /* @__PURE__ */ jsx6(SlotPickerSkeleton, { monthOffset }) })
1161
1317
  ] }) });
1162
1318
  }
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
- }
1319
+ if (availabilityError || !availability) return degradedCard;
1169
1320
  if (!isNativelyBookable(availability)) {
1170
- return /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, className), children: [
1321
+ return /* @__PURE__ */ jsxs6("div", { className: cn(CARD_DEGRADED_CLASS, flowHeight, className), children: [
1171
1322
  /* @__PURE__ */ jsx6("p", { className: "text-ods-text-secondary text-h6", children: "This meeting type is booked directly on HubSpot." }),
1172
1323
  escapeHatch
1173
1324
  ] });
1174
1325
  }
1175
- return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1176
- /* @__PURE__ */ jsx6(
1326
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, flowHeight, formOnly && "bg-ods-bg", className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1327
+ !formOnly && /* @__PURE__ */ jsx6(
1177
1328
  SchedulerContextPanel,
1178
1329
  {
1179
1330
  hosts: shownHosts,
@@ -1185,18 +1336,24 @@ function HubSpotMeetingScheduler({
1185
1336
  setDurationMs(ms);
1186
1337
  setSelectedDay(null);
1187
1338
  setSelectedSlot(null);
1188
- if (step === "details") setStep("slot");
1339
+ if (!detailsFirst && step === "details") setStep("slot");
1189
1340
  },
1190
1341
  timezone,
1191
1342
  onTimezoneChange: setTimezone,
1192
- onBack: step === "details" ? backToSlot : onBack,
1193
- locked: step !== "slot",
1343
+ onBack: detailsFirst ? step === "slot" ? backToDetails : onBack : step === "details" ? backToSlot : onBack,
1344
+ locked: detailsFirst ? step === "confirmed" || isSubmitting : step !== "slot",
1194
1345
  showTimezone: step !== "confirmed",
1346
+ fixedDuration: detailsFirst,
1195
1347
  className: CONTEXT_PANEL_CLASS
1196
1348
  }
1197
1349
  ),
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: [
1350
+ /* @__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 ? (
1351
+ // The form is step ONE here: no slot yet, and `timezone` is null
1352
+ // on the server render, so neither may gate the PANEL. A link
1353
+ // publishing no durations has no form worth showing.
1354
+ durations.length > 0
1355
+ ) : durationMs != null && selectedSlot != null && timezone) ? /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-1 flex-col gap-[var(--spacing-system-m)]", PANEL_STEP_CLASS), children: [
1356
+ selectedSlot != null && durationMs != null && timezone != null && /* @__PURE__ */ jsxs6("p", { className: "text-ods-text-primary text-h4", children: [
1200
1357
  new Intl.DateTimeFormat(void 0, {
1201
1358
  timeZone: timezone,
1202
1359
  weekday: "long",
@@ -1211,15 +1368,19 @@ function HubSpotMeetingScheduler({
1211
1368
  formatDurationCompact(durationMs / 1e3)
1212
1369
  ] }),
1213
1370
  /* @__PURE__ */ jsx6(
1214
- BookingForm,
1371
+ DetailsForm,
1215
1372
  {
1216
1373
  availability,
1217
1374
  meetingId,
1218
- startTimeMs: selectedSlot,
1219
- durationMs,
1375
+ startTimeMs: selectedSlot ?? void 0,
1376
+ durationMs: durationMs ?? void 0,
1220
1377
  timezone,
1378
+ deferSlot: detailsFirst,
1379
+ submitLabel: detailsFirst ? "Continue" : void 0,
1380
+ footerNote: detailsFirst ? "Next step: pick a slot that works. Calendar invite lands right after." : void 0,
1381
+ initialValues: detailsFirst ? stash?.payload : void 0,
1221
1382
  isSubmitting,
1222
- onSubmit: handleSubmit,
1383
+ onSubmit: detailsFirst ? stashDetails : handleSubmit,
1223
1384
  honeypotInputProps,
1224
1385
  getSignals
1225
1386
  }
@@ -1250,12 +1411,31 @@ function HubSpotMeetingScheduler({
1250
1411
  },
1251
1412
  selectedSlot,
1252
1413
  onSelectSlot: (ms) => {
1414
+ if (!detailsFirst) {
1415
+ setSelectedSlot(ms);
1416
+ setStep("details");
1417
+ return;
1418
+ }
1419
+ if (isSubmitting || !stash) return;
1253
1420
  setSelectedSlot(ms);
1254
- setStep("details");
1421
+ void handleSubmit({
1422
+ ...stash.payload,
1423
+ ...stash.signals,
1424
+ meetingId,
1425
+ // `ms`, NOT `selectedSlot` — the setState above has not
1426
+ // committed inside this handler, so state holds null on
1427
+ // the first booking and the PREVIOUS slot on a retry.
1428
+ startTimeMs: ms,
1429
+ // Live values: both stay editable after Continue, so the
1430
+ // copies frozen at that point can be stale.
1431
+ durationMs,
1432
+ timezone
1433
+ });
1255
1434
  },
1256
1435
  selectedDay,
1257
1436
  onSelectDay: setSelectedDay,
1258
- isLoading: isLoadingAvailability
1437
+ isLoading: isLoadingAvailability,
1438
+ isSubmitting: detailsFirst ? isSubmitting : void 0
1259
1439
  }
1260
1440
  )
1261
1441
  ) : durations.length === 0 && !isFetchingAvailability ? (
@@ -1276,7 +1456,10 @@ function HubSpotMeetingScheduler({
1276
1456
  ] }) });
1277
1457
  }
1278
1458
  export {
1459
+ BookingForm,
1460
+ BookingFormSkeleton,
1279
1461
  HubSpotMeetingScheduler,
1462
+ MEETING_SCHEDULER_DETAILS_FIRST_H,
1280
1463
  MEETING_SCHEDULER_H,
1281
1464
  MeetingSchedulerDirectory,
1282
1465
  MeetingSchedulerDirectoryRowSkeleton