@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.
- package/dist/{chunk-S7OOC4OT.js → chunk-3PUXHFC4.js} +3 -3
- package/dist/{chunk-Y2YYCQNA.cjs → chunk-6RISTRR3.cjs} +3 -3
- package/dist/{chunk-Y2YYCQNA.cjs.map → chunk-6RISTRR3.cjs.map} +1 -1
- package/dist/{chunk-WXI4C7ES.cjs → chunk-6X63WX5D.cjs} +34 -34
- package/dist/{chunk-WXI4C7ES.cjs.map → chunk-6X63WX5D.cjs.map} +1 -1
- package/dist/{chunk-HIAPO56E.js → chunk-7XHONLYQ.js} +2 -2
- package/dist/{chunk-UAGJG4NJ.cjs → chunk-ANGSMT5Q.cjs} +17 -17
- package/dist/{chunk-UAGJG4NJ.cjs.map → chunk-ANGSMT5Q.cjs.map} +1 -1
- package/dist/{chunk-BBL5OVS6.cjs → chunk-B3446RB6.cjs} +17 -17
- package/dist/{chunk-BBL5OVS6.cjs.map → chunk-B3446RB6.cjs.map} +1 -1
- package/dist/{chunk-TD4RFTWA.cjs → chunk-BC73KAXX.cjs} +4 -4
- package/dist/{chunk-TD4RFTWA.cjs.map → chunk-BC73KAXX.cjs.map} +1 -1
- package/dist/{chunk-JRDS7YKY.cjs → chunk-C2UBAAGD.cjs} +1 -1
- package/dist/chunk-C2UBAAGD.cjs.map +1 -0
- package/dist/{chunk-KGV5TWR2.cjs → chunk-CBR4H6JJ.cjs} +53 -53
- package/dist/{chunk-KGV5TWR2.cjs.map → chunk-CBR4H6JJ.cjs.map} +1 -1
- package/dist/{chunk-4RIYKOHC.cjs → chunk-DD64DQHM.cjs} +4 -4
- package/dist/{chunk-4RIYKOHC.cjs.map → chunk-DD64DQHM.cjs.map} +1 -1
- package/dist/{chunk-FLCOZJDL.cjs → chunk-DR4WXTKP.cjs} +57 -53
- package/dist/chunk-DR4WXTKP.cjs.map +1 -0
- package/dist/{chunk-DOREVXII.js → chunk-E6NN7MAN.js} +2 -2
- package/dist/{chunk-3VDT7ETH.js → chunk-E7FDTOJ6.js} +3 -3
- package/dist/{chunk-OQ76ZJK5.js → chunk-EM3T7YXE.js} +2 -2
- package/dist/{chunk-HGBU5DDB.js → chunk-ESHCKTES.js} +4 -4
- package/dist/{chunk-BX3VKU7N.cjs → chunk-HCHTKE6I.cjs} +3 -3
- package/dist/{chunk-BX3VKU7N.cjs.map → chunk-HCHTKE6I.cjs.map} +1 -1
- package/dist/{chunk-DLWLWO27.js → chunk-IJ4SGRAN.js} +6 -6
- package/dist/{chunk-PBUFCNQB.js → chunk-J2OCCRFC.js} +4 -4
- package/dist/{chunk-USFJSZJM.cjs → chunk-NBJELHZE.cjs} +3 -3
- package/dist/{chunk-USFJSZJM.cjs.map → chunk-NBJELHZE.cjs.map} +1 -1
- package/dist/{chunk-2U7GSXL5.cjs → chunk-NE3SHUIJ.cjs} +3 -3
- package/dist/{chunk-2U7GSXL5.cjs.map → chunk-NE3SHUIJ.cjs.map} +1 -1
- package/dist/{chunk-FW7NFHFV.cjs → chunk-NTJGRZ7G.cjs} +33 -33
- package/dist/{chunk-FW7NFHFV.cjs.map → chunk-NTJGRZ7G.cjs.map} +1 -1
- package/dist/{chunk-AJCAYUTW.js → chunk-OKCGXXEK.js} +2 -2
- package/dist/{chunk-YLCGQUSM.js → chunk-QAJXIOK2.js} +2 -2
- package/dist/{chunk-N4Y5LTOM.js → chunk-QT6Q6SUK.js} +1 -1
- package/dist/chunk-QT6Q6SUK.js.map +1 -0
- package/dist/{chunk-FT4EHILQ.js → chunk-S2WP7ELW.js} +8 -4
- package/dist/chunk-S2WP7ELW.js.map +1 -0
- package/dist/{chunk-H4UBIKGO.cjs → chunk-UG43WQ2C.cjs} +23 -23
- package/dist/{chunk-H4UBIKGO.cjs.map → chunk-UG43WQ2C.cjs.map} +1 -1
- package/dist/{chunk-USX6IC3D.cjs → chunk-WDOERQZK.cjs} +16 -16
- package/dist/{chunk-USX6IC3D.cjs.map → chunk-WDOERQZK.cjs.map} +1 -1
- package/dist/{chunk-34ZOCHPF.js → chunk-WTK5YZYU.js} +2 -2
- package/dist/{chunk-63UJWPPE.js → chunk-X6TLDYMG.js} +3 -3
- package/dist/{chunk-GZ2JZ3OR.js → chunk-XNZMKCA4.js} +5 -5
- package/dist/{chunk-XZES5WTY.cjs → chunk-XTZECFMT.cjs} +10 -10
- package/dist/{chunk-XZES5WTY.cjs.map → chunk-XTZECFMT.cjs.map} +1 -1
- package/dist/{chunk-WAD2Q52B.js → chunk-YZALGXOM.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +33 -33
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/booking-form.d.ts +23 -2
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/context-panel.d.ts +4 -1
- package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +133 -90
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts +11 -7
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +108 -65
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/field-wrapper.d.ts +2 -2
- package/dist/components/ui/file-manager/index.cjs +4 -4
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/index.cjs +9 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/schemas/meeting-booking-schema.cjs +8 -6
- package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +8 -6
- package/dist/schemas/meeting-booking-schema.js.map +1 -1
- package/dist/utils/hubspot-meetings-convention.d.ts +2 -0
- package/dist/utils/hubspot-meetings-convention.d.ts.map +1 -1
- package/dist/utils/index.cjs +5 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +5 -2
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/booking-form-field-rows.test.tsx +110 -0
- package/src/__tests__/meeting-scheduler-details-first.test.tsx +90 -0
- package/src/components/meeting-scheduler/booking-form.tsx +103 -71
- package/src/components/meeting-scheduler/context-panel.tsx +6 -0
- package/src/components/meeting-scheduler/index.tsx +78 -54
- package/src/components/ui/field-wrapper.tsx +2 -2
- package/src/components/ui/policy-link.tsx +1 -1
- package/src/schemas/meeting-booking-schema.ts +10 -7
- package/src/utils/hubspot-meetings-convention.ts +6 -1
- package/dist/chunk-FLCOZJDL.cjs.map +0 -1
- package/dist/chunk-FT4EHILQ.js.map +0 -1
- package/dist/chunk-JRDS7YKY.cjs.map +0 -1
- package/dist/chunk-N4Y5LTOM.js.map +0 -1
- /package/dist/{chunk-S7OOC4OT.js.map → chunk-3PUXHFC4.js.map} +0 -0
- /package/dist/{chunk-HIAPO56E.js.map → chunk-7XHONLYQ.js.map} +0 -0
- /package/dist/{chunk-DOREVXII.js.map → chunk-E6NN7MAN.js.map} +0 -0
- /package/dist/{chunk-3VDT7ETH.js.map → chunk-E7FDTOJ6.js.map} +0 -0
- /package/dist/{chunk-OQ76ZJK5.js.map → chunk-EM3T7YXE.js.map} +0 -0
- /package/dist/{chunk-HGBU5DDB.js.map → chunk-ESHCKTES.js.map} +0 -0
- /package/dist/{chunk-DLWLWO27.js.map → chunk-IJ4SGRAN.js.map} +0 -0
- /package/dist/{chunk-PBUFCNQB.js.map → chunk-J2OCCRFC.js.map} +0 -0
- /package/dist/{chunk-AJCAYUTW.js.map → chunk-OKCGXXEK.js.map} +0 -0
- /package/dist/{chunk-YLCGQUSM.js.map → chunk-QAJXIOK2.js.map} +0 -0
- /package/dist/{chunk-34ZOCHPF.js.map → chunk-WTK5YZYU.js.map} +0 -0
- /package/dist/{chunk-63UJWPPE.js.map → chunk-X6TLDYMG.js.map} +0 -0
- /package/dist/{chunk-GZ2JZ3OR.js.map → chunk-XNZMKCA4.js.map} +0 -0
- /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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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 (`
|
|
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
|
-
|
|
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
|
|
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
|
|
658
|
-
|
|
659
|
-
/** One card SHAPE for
|
|
660
|
-
const
|
|
661
|
-
<SchedulerDegradedCard flow={flow} className={className}
|
|
662
|
-
<p className="text-ods-text-secondary text-h6">
|
|
663
|
-
We couldn'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={
|
|
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
|
-
|
|
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={
|
|
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:
|
|
737
|
-
// `details`
|
|
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
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
// already built carries the old
|
|
745
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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={
|
|
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
|
|
40
|
-
* required fields
|
|
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:
|
|
457
|
-
durationMs:
|
|
458
|
-
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:
|
|
482
|
-
durationMs:
|
|
483
|
-
timezone:
|
|
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
|
|
42
|
+
return SCHEDULING_SLUG_SHAPE.test(normalizeSchedulingSlug(slug));
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export interface ParsedSchedulingLinkName {
|