@flamingo-stack/openframe-frontend-core 0.0.596-2034.5680.1 → 0.0.596-2034.5684.1

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 (130) hide show
  1. package/dist/{chunk-S7OOC4OT.js → chunk-3PUXHFC4.js} +3 -3
  2. package/dist/{chunk-Y2YYCQNA.cjs → chunk-6RISTRR3.cjs} +3 -3
  3. package/dist/{chunk-Y2YYCQNA.cjs.map → chunk-6RISTRR3.cjs.map} +1 -1
  4. package/dist/{chunk-WXI4C7ES.cjs → chunk-6X63WX5D.cjs} +34 -34
  5. package/dist/{chunk-WXI4C7ES.cjs.map → chunk-6X63WX5D.cjs.map} +1 -1
  6. package/dist/{chunk-HIAPO56E.js → chunk-7XHONLYQ.js} +2 -2
  7. package/dist/{chunk-UAGJG4NJ.cjs → chunk-ANGSMT5Q.cjs} +17 -17
  8. package/dist/{chunk-UAGJG4NJ.cjs.map → chunk-ANGSMT5Q.cjs.map} +1 -1
  9. package/dist/{chunk-BBL5OVS6.cjs → chunk-B3446RB6.cjs} +17 -17
  10. package/dist/{chunk-BBL5OVS6.cjs.map → chunk-B3446RB6.cjs.map} +1 -1
  11. package/dist/{chunk-TD4RFTWA.cjs → chunk-BC73KAXX.cjs} +4 -4
  12. package/dist/{chunk-TD4RFTWA.cjs.map → chunk-BC73KAXX.cjs.map} +1 -1
  13. package/dist/{chunk-JRDS7YKY.cjs → chunk-C2UBAAGD.cjs} +1 -1
  14. package/dist/chunk-C2UBAAGD.cjs.map +1 -0
  15. package/dist/{chunk-KGV5TWR2.cjs → chunk-CBR4H6JJ.cjs} +53 -53
  16. package/dist/{chunk-KGV5TWR2.cjs.map → chunk-CBR4H6JJ.cjs.map} +1 -1
  17. package/dist/{chunk-4RIYKOHC.cjs → chunk-DD64DQHM.cjs} +4 -4
  18. package/dist/{chunk-4RIYKOHC.cjs.map → chunk-DD64DQHM.cjs.map} +1 -1
  19. package/dist/{chunk-FLCOZJDL.cjs → chunk-DR4WXTKP.cjs} +57 -53
  20. package/dist/chunk-DR4WXTKP.cjs.map +1 -0
  21. package/dist/{chunk-DOREVXII.js → chunk-E6NN7MAN.js} +2 -2
  22. package/dist/{chunk-3VDT7ETH.js → chunk-E7FDTOJ6.js} +3 -3
  23. package/dist/{chunk-OQ76ZJK5.js → chunk-EM3T7YXE.js} +2 -2
  24. package/dist/{chunk-HGBU5DDB.js → chunk-ESHCKTES.js} +4 -4
  25. package/dist/{chunk-BX3VKU7N.cjs → chunk-HCHTKE6I.cjs} +3 -3
  26. package/dist/{chunk-BX3VKU7N.cjs.map → chunk-HCHTKE6I.cjs.map} +1 -1
  27. package/dist/{chunk-DLWLWO27.js → chunk-IJ4SGRAN.js} +6 -6
  28. package/dist/{chunk-PBUFCNQB.js → chunk-J2OCCRFC.js} +4 -4
  29. package/dist/{chunk-USFJSZJM.cjs → chunk-NBJELHZE.cjs} +3 -3
  30. package/dist/{chunk-USFJSZJM.cjs.map → chunk-NBJELHZE.cjs.map} +1 -1
  31. package/dist/{chunk-2U7GSXL5.cjs → chunk-NE3SHUIJ.cjs} +3 -3
  32. package/dist/{chunk-2U7GSXL5.cjs.map → chunk-NE3SHUIJ.cjs.map} +1 -1
  33. package/dist/{chunk-FW7NFHFV.cjs → chunk-NTJGRZ7G.cjs} +33 -33
  34. package/dist/{chunk-FW7NFHFV.cjs.map → chunk-NTJGRZ7G.cjs.map} +1 -1
  35. package/dist/{chunk-AJCAYUTW.js → chunk-OKCGXXEK.js} +2 -2
  36. package/dist/{chunk-YLCGQUSM.js → chunk-QAJXIOK2.js} +2 -2
  37. package/dist/{chunk-N4Y5LTOM.js → chunk-QT6Q6SUK.js} +1 -1
  38. package/dist/chunk-QT6Q6SUK.js.map +1 -0
  39. package/dist/{chunk-FT4EHILQ.js → chunk-S2WP7ELW.js} +8 -4
  40. package/dist/chunk-S2WP7ELW.js.map +1 -0
  41. package/dist/{chunk-H4UBIKGO.cjs → chunk-UG43WQ2C.cjs} +23 -23
  42. package/dist/{chunk-H4UBIKGO.cjs.map → chunk-UG43WQ2C.cjs.map} +1 -1
  43. package/dist/{chunk-USX6IC3D.cjs → chunk-WDOERQZK.cjs} +16 -16
  44. package/dist/{chunk-USX6IC3D.cjs.map → chunk-WDOERQZK.cjs.map} +1 -1
  45. package/dist/{chunk-34ZOCHPF.js → chunk-WTK5YZYU.js} +2 -2
  46. package/dist/{chunk-63UJWPPE.js → chunk-X6TLDYMG.js} +3 -3
  47. package/dist/{chunk-GZ2JZ3OR.js → chunk-XNZMKCA4.js} +5 -5
  48. package/dist/{chunk-XZES5WTY.cjs → chunk-XTZECFMT.cjs} +10 -10
  49. package/dist/{chunk-XZES5WTY.cjs.map → chunk-XTZECFMT.cjs.map} +1 -1
  50. package/dist/{chunk-WAD2Q52B.js → chunk-YZALGXOM.js} +2 -2
  51. package/dist/components/case-studies/index.cjs +9 -9
  52. package/dist/components/case-studies/index.js +3 -3
  53. package/dist/components/contact/index.cjs +4 -4
  54. package/dist/components/contact/index.js +3 -3
  55. package/dist/components/docs/index.cjs +7 -7
  56. package/dist/components/docs/index.js +6 -6
  57. package/dist/components/embeds/index.cjs +4 -4
  58. package/dist/components/embeds/index.js +3 -3
  59. package/dist/components/faq/index.cjs +4 -4
  60. package/dist/components/faq/index.js +3 -3
  61. package/dist/components/features/index.cjs +3 -3
  62. package/dist/components/features/index.js +2 -2
  63. package/dist/components/help-center-pages/index.cjs +23 -23
  64. package/dist/components/help-center-pages/index.js +14 -14
  65. package/dist/components/index.cjs +33 -33
  66. package/dist/components/index.js +13 -13
  67. package/dist/components/meeting-scheduler/booking-form.d.ts +23 -2
  68. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  69. package/dist/components/meeting-scheduler/context-panel.d.ts +4 -1
  70. package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
  71. package/dist/components/meeting-scheduler/index.cjs +133 -90
  72. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  73. package/dist/components/meeting-scheduler/index.d.ts +11 -7
  74. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  75. package/dist/components/meeting-scheduler/index.js +108 -65
  76. package/dist/components/meeting-scheduler/index.js.map +1 -1
  77. package/dist/components/navigation/index.cjs +6 -6
  78. package/dist/components/navigation/index.js +5 -5
  79. package/dist/components/onboarding-guides/index.cjs +6 -6
  80. package/dist/components/onboarding-guides/index.js +5 -5
  81. package/dist/components/tickets/index.cjs +7 -7
  82. package/dist/components/tickets/index.js +6 -6
  83. package/dist/components/ui/field-wrapper.d.ts +2 -2
  84. package/dist/components/ui/file-manager/index.cjs +4 -4
  85. package/dist/components/ui/file-manager/index.js +1 -1
  86. package/dist/components/ui/index.cjs +3 -3
  87. package/dist/components/ui/index.js +2 -2
  88. package/dist/index.cjs +9 -7
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.js +8 -6
  91. package/dist/index.js.map +1 -1
  92. package/dist/schemas/meeting-booking-schema.cjs +8 -6
  93. package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
  94. package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
  95. package/dist/schemas/meeting-booking-schema.js +8 -6
  96. package/dist/schemas/meeting-booking-schema.js.map +1 -1
  97. package/dist/utils/hubspot-meetings-convention.d.ts +2 -0
  98. package/dist/utils/hubspot-meetings-convention.d.ts.map +1 -1
  99. package/dist/utils/index.cjs +5 -1
  100. package/dist/utils/index.cjs.map +1 -1
  101. package/dist/utils/index.js +5 -2
  102. package/dist/utils/index.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/__tests__/booking-form-field-rows.test.tsx +110 -0
  105. package/src/__tests__/meeting-scheduler-details-first.test.tsx +90 -0
  106. package/src/components/meeting-scheduler/booking-form.tsx +103 -71
  107. package/src/components/meeting-scheduler/context-panel.tsx +6 -0
  108. package/src/components/meeting-scheduler/index.tsx +78 -54
  109. package/src/components/ui/field-wrapper.tsx +2 -2
  110. package/src/components/ui/policy-link.tsx +1 -1
  111. package/src/schemas/meeting-booking-schema.ts +10 -7
  112. package/src/utils/hubspot-meetings-convention.ts +6 -1
  113. package/dist/chunk-FLCOZJDL.cjs.map +0 -1
  114. package/dist/chunk-FT4EHILQ.js.map +0 -1
  115. package/dist/chunk-JRDS7YKY.cjs.map +0 -1
  116. package/dist/chunk-N4Y5LTOM.js.map +0 -1
  117. /package/dist/{chunk-S7OOC4OT.js.map → chunk-3PUXHFC4.js.map} +0 -0
  118. /package/dist/{chunk-HIAPO56E.js.map → chunk-7XHONLYQ.js.map} +0 -0
  119. /package/dist/{chunk-DOREVXII.js.map → chunk-E6NN7MAN.js.map} +0 -0
  120. /package/dist/{chunk-3VDT7ETH.js.map → chunk-E7FDTOJ6.js.map} +0 -0
  121. /package/dist/{chunk-OQ76ZJK5.js.map → chunk-EM3T7YXE.js.map} +0 -0
  122. /package/dist/{chunk-HGBU5DDB.js.map → chunk-ESHCKTES.js.map} +0 -0
  123. /package/dist/{chunk-DLWLWO27.js.map → chunk-IJ4SGRAN.js.map} +0 -0
  124. /package/dist/{chunk-PBUFCNQB.js.map → chunk-J2OCCRFC.js.map} +0 -0
  125. /package/dist/{chunk-AJCAYUTW.js.map → chunk-OKCGXXEK.js.map} +0 -0
  126. /package/dist/{chunk-YLCGQUSM.js.map → chunk-QAJXIOK2.js.map} +0 -0
  127. /package/dist/{chunk-34ZOCHPF.js.map → chunk-WTK5YZYU.js.map} +0 -0
  128. /package/dist/{chunk-63UJWPPE.js.map → chunk-X6TLDYMG.js.map} +0 -0
  129. /package/dist/{chunk-GZ2JZ3OR.js.map → chunk-XNZMKCA4.js.map} +0 -0
  130. /package/dist/{chunk-WAD2Q52B.js.map → chunk-YZALGXOM.js.map} +0 -0
@@ -56,18 +56,6 @@ import { cn } from '../../utils/cn';
56
56
  import { formatDurationCompact } from '../../utils/format';
57
57
  import { Alert, AlertDescription, Button } from '../ui';
58
58
  import { BookingForm, BookingFormSkeleton, type BookingFormProps } from './booking-form';
59
-
60
- // The override surface: a host builds its `detailsForm` by re-rendering
61
- // `BookingForm` with `fieldRows`, so it needs both from this entry point (the
62
- // package exports `./components/meeting-scheduler`, not the file beneath it).
63
- export {
64
- BookingForm,
65
- BookingFormSkeleton,
66
- type BookingFormProps,
67
- type BookingFieldRow,
68
- type BookingFieldSlot,
69
- type BookingFormConsent,
70
- } from './booking-form';
71
59
  import { Confirmation } from './confirmation';
72
60
  import { SchedulerContextPanel, ContextPanelSkeleton } from './context-panel';
73
61
  import { SlotPicker, SlotPickerSkeleton, dayKeyInZone } from './slot-picker';
@@ -261,8 +249,7 @@ export const MEETING_SCHEDULER_H = 'md:h-[34.375rem] lg:h-[23.75rem]';
261
249
  * tablet and desktop, and the calendar stage fits the same box at both — the
262
250
  * times column scrolls inside it as it always has.
263
251
  *
264
- * A host reserving the box for a details-first card reserves THIS, not the
265
- * constant above.
252
+ * Hosts read it through `SCHEDULER_FLOW_PRESETS[flow].height`, never directly.
266
253
  */
267
254
  export const MEETING_SCHEDULER_DETAILS_FIRST_H = 'md:h-[39.875rem]';
268
255
 
@@ -277,9 +264,9 @@ export type SchedulerFlow = 'slot-first' | 'details-first';
277
264
  */
278
265
  export const SCHEDULER_FLOW_PRESETS: Record<
279
266
  SchedulerFlow,
280
- { initialStep: 'slot' | 'details'; height: string; submitLabel?: string; footerNote?: string }
267
+ { initialStep: Exclude<Step, 'confirmed'>; height: string; submitLabel: string; footerNote?: string }
281
268
  > = {
282
- 'slot-first': { initialStep: 'slot', height: MEETING_SCHEDULER_H },
269
+ 'slot-first': { initialStep: 'slot', height: MEETING_SCHEDULER_H, submitLabel: 'Confirm Booking' },
283
270
  'details-first': {
284
271
  initialStep: 'details',
285
272
  height: MEETING_SCHEDULER_DETAILS_FIRST_H,
@@ -292,7 +279,7 @@ export const SCHEDULER_FLOW_PRESETS: Record<
292
279
  * The two-panel card's box, shared verbatim by the loading skeleton and the
293
280
  * loaded card so neither can drift from the other.
294
281
  *
295
- * Both heights come from {@link MEETING_SCHEDULER_H}; what this adds is the
282
+ * The height is applied per flow (`SCHEDULER_FLOW_PRESETS[flow].height`); what this adds is the
296
283
  * flex column that makes them work. On TABLET the height is stated for the
297
284
  * same reason the desktop one is: a CEILING only pins the tall stages. The short ones — the empty
298
285
  * month ("No available times in September"), a five-week month, the cold-start
@@ -319,7 +306,7 @@ const CARD_CLASS = cn(
319
306
  'overflow-hidden rounded-md border border-ods-border bg-ods-card',
320
307
  'md:flex md:flex-col',
321
308
  // The height is NOT baked in any more: it is chosen per flow at each render
322
- // site (`flowHeight`), because details-first needs a taller box and both
309
+ // site (`cardClass`), because details-first needs a taller box and both
323
310
  // constants are module scope.
324
311
  );
325
312
 
@@ -349,13 +336,25 @@ const CARD_DEGRADED_CLASS = cn(
349
336
  export function SchedulerDegradedCard({
350
337
  flow = 'slot-first',
351
338
  className,
339
+ message,
340
+ action,
352
341
  children,
353
342
  }: {
354
343
  flow?: SchedulerFlow;
355
344
  className?: string;
356
- children: ReactNode;
345
+ /** The one line the stage says, in the card's own type — so a host's fallback never picks a different scale. */
346
+ message?: string;
347
+ /** The one way out (the escape hatch, or a host's own link). */
348
+ action?: ReactNode;
349
+ children?: ReactNode;
357
350
  }) {
358
- return <div className={cn(CARD_DEGRADED_CLASS, SCHEDULER_FLOW_PRESETS[flow].height, className)}>{children}</div>;
351
+ return (
352
+ <div className={cn(CARD_DEGRADED_CLASS, SCHEDULER_FLOW_PRESETS[flow].height, className)}>
353
+ {message && <p className="text-ods-text-secondary text-h6">{message}</p>}
354
+ {action}
355
+ {children}
356
+ </div>
357
+ );
359
358
  }
360
359
 
361
360
  /**
@@ -434,6 +433,12 @@ export function HubSpotMeetingScheduler({
434
433
  const [stash, setStash] = useState<StashedDetails | null>(null);
435
434
  /** Synchronous in-flight lock for the details-first slot click — see `onSelectSlot`. */
436
435
  const inFlightRef = useRef(false);
436
+ /** details-first: a slot's POST is in flight. The ref covers the frame before
437
+ * `isSubmitting` renders true, so every guard reads both. */
438
+ const postInFlight = useCallback(
439
+ () => detailsFirst && (isSubmitting || inFlightRef.current),
440
+ [detailsFirst, isSubmitting],
441
+ );
437
442
 
438
443
  /**
439
444
  * details-first step ONE is the form and nothing else — the mock
@@ -531,12 +536,14 @@ export function HubSpotMeetingScheduler({
531
536
  durationMs != null &&
532
537
  slots.length === 0 &&
533
538
  availability.hasMore &&
534
- autoAdvanceCount.current < 3
539
+ autoAdvanceCount.current < 3 &&
540
+ // Not while a chip's POST is in flight — paging would unmount it.
541
+ !postInFlight()
535
542
  ) {
536
543
  autoAdvanceCount.current += 1;
537
544
  setMonthOffset(monthOffset + 1);
538
545
  }
539
- }, [step, availability, isFetchingAvailability, durationMs, slots, monthOffset, setMonthOffset]);
546
+ }, [step, availability, isFetchingAvailability, durationMs, slots, monthOffset, setMonthOffset, postInFlight]);
540
547
 
541
548
  /** The back edge's destination from the form: the calendar, with any
542
549
  * submit error cleared so the visitor doesn't carry it back. */
@@ -551,6 +558,10 @@ export function HubSpotMeetingScheduler({
551
558
  // Not while a slot's POST is in flight — the step must stay on the
552
559
  // calendar the spinner lives on until the result routes it.
553
560
  if (inFlightRef.current) return;
561
+ // No slot rides back to the form: in this flow the form never legitimately
562
+ // holds one (the slot IS the submit), and a chip the server rejected with a
563
+ // slot-step toast would otherwise print in the form's summary line.
564
+ setSelectedSlot(null);
554
565
  setStep('details');
555
566
  setBookingError(null);
556
567
  }, []);
@@ -654,17 +665,13 @@ export function HubSpotMeetingScheduler({
654
665
  * last-wins, so appending it after would make a host's own `h-*` unreachable,
655
666
  * which is the override the height-inside-CARD_CLASS arrangement allows today.
656
667
  */
657
- const flowHeight = preset.height;
658
-
659
- /** One card SHAPE for both degraded returns below. */
660
- const degradedCard = (
661
- <SchedulerDegradedCard flow={flow} className={className}>
662
- <p className="text-ods-text-secondary text-h6">
663
- We couldn&apos;t load available call times. Please try again shortly.
664
- </p>
665
- {escapeHatch}
666
- </SchedulerDegradedCard>
668
+ const cardClass = cn(CARD_CLASS, preset.height, formOnly && 'bg-ods-bg', className);
669
+
670
+ /** One card SHAPE for every degraded return below. */
671
+ const degraded = (message: string) => (
672
+ <SchedulerDegradedCard flow={flow} className={className} message={message} action={escapeHatch} />
667
673
  );
674
+ const degradedCard = degraded("We couldn't load available call times. Please try again shortly.");
668
675
 
669
676
  if (isLoadingAvailability && !availability) {
670
677
  // COLD start only — a month already in the query cache renders straight
@@ -672,7 +679,7 @@ export function HubSpotMeetingScheduler({
672
679
  // (`ContextPanelSkeleton` beside the real slot-area layout), so nothing
673
680
  // shifts when it swaps.
674
681
  return (
675
- <div className={cn(CARD_CLASS, flowHeight, formOnly && 'bg-ods-bg', className)}>
682
+ <div className={cardClass}>
676
683
  <div className={CARD_INNER_CLASS}>
677
684
  {!formOnly && <ContextPanelSkeleton onBack={onBack} className={CONTEXT_PANEL_CLASS} />}
678
685
  <div className={ACTION_PANEL_CLASS}>
@@ -681,7 +688,11 @@ export function HubSpotMeetingScheduler({
681
688
  Continue (which happens at `step === 'slot'`) would swap the
682
689
  calendar out for a form skeleton and back. */}
683
690
  {detailsFirst && step === 'details' ? (
684
- <BookingFormSkeleton />
691
+ // The SAME wrapper the loaded form gets, so the skeleton never
692
+ // states its own inset.
693
+ <div className={cn('flex flex-1 flex-col', PANEL_STEP_CLASS)}>
694
+ <BookingFormSkeleton fieldRows={detailsFormProps?.fieldRows} />
695
+ </div>
685
696
  ) : (
686
697
  <SlotPickerSkeleton monthOffset={monthOffset} />
687
698
  )}
@@ -696,18 +707,13 @@ export function HubSpotMeetingScheduler({
696
707
  if (!isNativelyBookable(availability)) {
697
708
  // Fail closed — never render a half-working native form on a link with
698
709
  // questions or consent we can't faithfully reproduce.
699
- return (
700
- <SchedulerDegradedCard flow={flow} className={className}>
701
- <p className="text-ods-text-secondary text-h6">This meeting type is booked directly on HubSpot.</p>
702
- {escapeHatch}
703
- </SchedulerDegradedCard>
704
- );
710
+ return degraded('This meeting type is booked directly on HubSpot.');
705
711
  }
706
712
 
707
713
  // ---- the card ------------------------------------------------------------
708
714
 
709
715
  return (
710
- <div className={cn(CARD_CLASS, flowHeight, formOnly && 'bg-ods-bg', className)}>
716
+ <div className={cardClass}>
711
717
  <div className={CARD_INNER_CLASS}>
712
718
  {!formOnly && (
713
719
  <SchedulerContextPanel
@@ -733,16 +739,19 @@ export function HubSpotMeetingScheduler({
733
739
  // host gave us an exit at all). Two Backs on screen reading the same
734
740
  // word with different destinations was the ambiguity the details
735
741
  // step used to carry.
736
- // details-first inverts the mapping: the form is step ONE, so Back at
737
- // `details` is the host's exit and Back at `slot` returns to the form.
742
+ // details-first inverts the mapping: Back at `slot` returns to the
743
+ // form. At `details` the form-only layout mounts no panel, so a host
744
+ // exit is not rendered there (the page around the card is the exit).
738
745
  onBack={
739
746
  detailsFirst ? (step === 'slot' ? backToDetails : onBack) : step === 'details' ? backToSlot : onBack
740
747
  }
741
- // In details-first the duration/zone selectors stay live while the
742
- // form is filled — but must freeze once a POST is in flight, or a
743
- // mid-flight change clears the slot under the spinner while the body
744
- // already built carries the old duration.
745
- locked={detailsFirst ? step === 'confirmed' || isSubmitting : step !== 'slot'}
748
+ // `locked` HIDES the selectors (post-selection steps); `disabled`
749
+ // keeps them mounted but inert. details-first needs the second while
750
+ // a POST is in flight: a mid-flight change would clear the slot
751
+ // under the spinner while the body already built carries the old
752
+ // duration — and unmounting the row would shift the grid under it.
753
+ locked={detailsFirst ? step === 'confirmed' : step !== 'slot'}
754
+ disabled={detailsFirst && isSubmitting}
746
755
  // The zone still governs every time on screen — including the
747
756
  // summary line on the form — so the picker stays until there is
748
757
  // nothing left to re-read in it.
@@ -768,7 +777,7 @@ export function HubSpotMeetingScheduler({
768
777
  : durationMs != null && selectedSlot != null && timezone) ? (
769
778
  <div className={cn('flex flex-1 flex-col gap-[var(--spacing-system-m)]', PANEL_STEP_CLASS)}>
770
779
  {/* Top-aligned, and no back edge of its own: the ONE back edge
771
- lives in the context panel at every step (see `contextBack`),
780
+ lives in the context panel at every step (the `onBack` wired on `SchedulerContextPanel`),
772
781
  which is where the design puts it and the only spot that
773
782
  stays put while this side swaps between calendar and form.
774
783
 
@@ -830,9 +839,7 @@ export function HubSpotMeetingScheduler({
830
839
  onMonthOffsetChange={o => {
831
840
  // details-first: a POST is in flight from this grid; paging
832
841
  // would clear the spinning chip and swap the key under it.
833
- // Same lock as the chip handler: the ref covers the frame
834
- // before `isSubmitting` renders true.
835
- if (detailsFirst && (isSubmitting || inFlightRef.current)) return;
842
+ if (postInFlight()) return;
836
843
  setSelectedDay(null);
837
844
  setSelectedSlot(null);
838
845
  setMonthOffset(o);
@@ -851,7 +858,7 @@ export function HubSpotMeetingScheduler({
851
858
  // `isSubmitting` is the render's snapshot; a second chip
852
859
  // clicked in the same frame sees it false. The ref is the
853
860
  // synchronous lock, so the second click changes nothing.
854
- if (isSubmitting || inFlightRef.current || !stash) return;
861
+ if (postInFlight() || !stash) return;
855
862
  inFlightRef.current = true;
856
863
  setSelectedSlot(ms);
857
864
  void handleSubmit({
@@ -871,7 +878,12 @@ export function HubSpotMeetingScheduler({
871
878
  });
872
879
  }}
873
880
  selectedDay={selectedDay}
874
- onSelectDay={setSelectedDay}
881
+ onSelectDay={day => {
882
+ // Same lock: another day would swap the times column out
883
+ // from under the spinning chip.
884
+ if (postInFlight()) return;
885
+ setSelectedDay(day);
886
+ }}
875
887
  isLoading={isLoadingAvailability}
876
888
  // Distinct from `isLoading`, which swaps the whole times
877
889
  // column for a skeleton: the grid must stay up with the
@@ -900,6 +912,18 @@ export function HubSpotMeetingScheduler({
900
912
  );
901
913
  }
902
914
 
915
+ // The override surface: a host builds its `detailsForm` by re-rendering
916
+ // `BookingForm` with `fieldRows`, so it needs both from this entry point (the
917
+ // package exports `./components/meeting-scheduler`, not the file beneath it).
918
+ export {
919
+ BookingForm,
920
+ BookingFormSkeleton,
921
+ type BookingFormProps,
922
+ type BookingFieldRow,
923
+ type BookingFieldSlot,
924
+ type BookingFormConsent,
925
+ } from './booking-form';
926
+
903
927
  export {
904
928
  MeetingSchedulerDirectory,
905
929
  MeetingSchedulerDirectoryRowSkeleton,
@@ -36,8 +36,8 @@ export interface FieldWrapperProps {
36
36
  */
37
37
  labelVariant?: 'default' | 'large';
38
38
  /**
39
- * Marks the label with the accent asterisk `ContactForm` draws on its
40
- * required fields (`Label<span class="text-ods-accent">*</span>`), so a form
39
+ * Marks the label with the `RequiredMark` `ContactForm` draws on its
40
+ * required fields, so a form
41
41
  * mixing both never shows two conventions. Visual only — the CONTROL carries
42
42
  * `required`/`aria-required` for assistive tech; the star is `aria-hidden`.
43
43
  */
@@ -8,7 +8,7 @@ export function PolicyLink({ href, children }: { href: string; children: ReactNo
8
8
  return (
9
9
  <a
10
10
  href={href}
11
- className="text-ods-accent underline"
11
+ className="text-ods-accent underline underline-offset-2 hover:text-ods-accent/80"
12
12
  target="_blank"
13
13
  rel="noopener noreferrer"
14
14
  onClick={e => e.stopPropagation()}
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
-
3
2
  /**
4
3
  * Meeting-booking wire contracts + validation factory.
5
4
  *
@@ -446,6 +445,10 @@ function buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.Zod
446
445
  );
447
446
  }
448
447
 
448
+ /** The slot's wire validators — ONE declaration for the strict and the deferred schema. */
449
+ const SLOT_INSTANT = z.number().int().positive();
450
+ const TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });
451
+
449
452
  /**
450
453
  * The STRICT schema — the wire contract. The server rebuilds it from the link's
451
454
  * own fetched metadata and validates every booking against it; a slot and a
@@ -453,9 +456,9 @@ function buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.Zod
453
456
  */
454
457
  export function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {
455
458
  return buildBookingSchema(formFields, legalConsent, {
456
- startTimeMs: z.number().int().positive(),
457
- durationMs: z.number().int().positive(),
458
- timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' }),
459
+ startTimeMs: SLOT_INSTANT,
460
+ durationMs: SLOT_INSTANT,
461
+ timezone: TIMEZONE,
459
462
  });
460
463
  }
461
464
 
@@ -478,9 +481,9 @@ export function makeDeferredBookingSchema(formFields: MeetingFormField[], legalC
478
481
  // Accepting only `undefined` would reject a field with no rendered control
479
482
  // and no FieldWrapper — the submit button silently dead, type-check clean.
480
483
  return buildBookingSchema(formFields, legalConsent, {
481
- startTimeMs: z.number().int().positive().nullish(),
482
- durationMs: z.number().int().positive().nullish(),
483
- timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' }).nullish(),
484
+ startTimeMs: SLOT_INSTANT.nullish(),
485
+ durationMs: SLOT_INSTANT.nullish(),
486
+ timezone: TIMEZONE.nullish(),
484
487
  });
485
488
  }
486
489
 
@@ -33,8 +33,13 @@ export const SCHEDULING_SLUG_SHAPE = /^[a-z0-9-]+(\/[a-z0-9-]+)*$/;
33
33
  export const MAX_MONTH_OFFSET = 11;
34
34
 
35
35
  /** Full-slug shape validity (path-resolution safety only — never semantics). */
36
+ /** The one spelling of slug normalisation — every map key and lookup goes through it. */
37
+ export function normalizeSchedulingSlug(slug: string): string {
38
+ return slug.trim().toLowerCase();
39
+ }
40
+
36
41
  export function isValidSchedulingSlug(slug: string): boolean {
37
- return SCHEDULING_SLUG_SHAPE.test(slug.trim().toLowerCase());
42
+ return SCHEDULING_SLUG_SHAPE.test(normalizeSchedulingSlug(slug));
38
43
  }
39
44
 
40
45
  export interface ParsedSchedulingLinkName {