@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.
- package/dist/{chunk-HQWZ4IXU.cjs → chunk-3CH6VRJX.cjs} +24 -24
- package/dist/{chunk-HQWZ4IXU.cjs.map → chunk-3CH6VRJX.cjs.map} +1 -1
- package/dist/{chunk-LKN2KKYZ.js → chunk-4K7TCJ35.js} +6 -59
- package/dist/{chunk-LKN2KKYZ.js.map → chunk-4K7TCJ35.js.map} +1 -1
- package/dist/{chunk-YLVPQNLO.cjs → chunk-5MSHWXJ5.cjs} +170 -223
- package/dist/chunk-5MSHWXJ5.cjs.map +1 -0
- package/dist/{chunk-CCLPJSY3.cjs → chunk-73BECFSU.cjs} +15 -15
- package/dist/{chunk-CCLPJSY3.cjs.map → chunk-73BECFSU.cjs.map} +1 -1
- package/dist/{chunk-F5Q3DTP5.js → chunk-7EUE3YTC.js} +8 -8
- package/dist/{chunk-GBIC3VNA.js → chunk-7KGN7DV6.js} +6 -6
- package/dist/{chunk-YZLUOZKR.js → chunk-7Q66A4CF.js} +2 -2
- package/dist/{chunk-DRZ224RU.js → chunk-ANSZVKI2.js} +8 -8
- package/dist/{chunk-N3RHX26N.js → chunk-AZZVSEP5.js} +3 -3
- package/dist/{chunk-66I2QUFE.cjs → chunk-B26UT36N.cjs} +6 -6
- package/dist/{chunk-66I2QUFE.cjs.map → chunk-B26UT36N.cjs.map} +1 -1
- package/dist/{chunk-UBRVIVY4.cjs → chunk-B2CSYMKW.cjs} +298 -285
- package/dist/chunk-B2CSYMKW.cjs.map +1 -0
- package/dist/{chunk-ZK3AZ5YF.cjs → chunk-BTOL222V.cjs} +11 -11
- package/dist/{chunk-ZK3AZ5YF.cjs.map → chunk-BTOL222V.cjs.map} +1 -1
- package/dist/{chunk-S7VW4HVL.cjs → chunk-DODE3PD4.cjs} +49 -49
- package/dist/{chunk-S7VW4HVL.cjs.map → chunk-DODE3PD4.cjs.map} +1 -1
- package/dist/{chunk-ZZHHKE2Q.js → chunk-ECXQX4EP.js} +2 -2
- package/dist/{chunk-NEFNARFF.cjs → chunk-EEV7SSBJ.cjs} +30 -30
- package/dist/{chunk-NEFNARFF.cjs.map → chunk-EEV7SSBJ.cjs.map} +1 -1
- package/dist/{chunk-3OQIZPUX.js → chunk-EO3NEGWB.js} +4 -4
- package/dist/{chunk-MOHSA65M.js → chunk-EZM5DNII.js} +2 -2
- package/dist/{chunk-IKQVXZB5.js → chunk-GSPS6Y5U.js} +6 -6
- package/dist/{chunk-CQ35OMPJ.cjs → chunk-I2BNLWC3.cjs} +3 -3
- package/dist/{chunk-CQ35OMPJ.cjs.map → chunk-I2BNLWC3.cjs.map} +1 -1
- package/dist/{chunk-HOLK4ZMT.js → chunk-II2GBWNW.js} +4 -4
- package/dist/{chunk-PETYVGEQ.cjs → chunk-JKGVXLTT.cjs} +5 -5
- package/dist/{chunk-PETYVGEQ.cjs.map → chunk-JKGVXLTT.cjs.map} +1 -1
- package/dist/{chunk-5EM6NGBK.cjs → chunk-KOQCA5X5.cjs} +17 -27
- package/dist/chunk-KOQCA5X5.cjs.map +1 -0
- package/dist/{chunk-ACU7J5BL.cjs → chunk-KPE7OQF6.cjs} +84 -84
- package/dist/{chunk-ACU7J5BL.cjs.map → chunk-KPE7OQF6.cjs.map} +1 -1
- package/dist/{chunk-C4YJC5X7.cjs → chunk-KRKW63LG.cjs} +9 -9
- package/dist/{chunk-C4YJC5X7.cjs.map → chunk-KRKW63LG.cjs.map} +1 -1
- package/dist/{chunk-ZEG3ZJPY.cjs → chunk-LE5SZPMN.cjs} +33 -33
- package/dist/{chunk-ZEG3ZJPY.cjs.map → chunk-LE5SZPMN.cjs.map} +1 -1
- package/dist/{chunk-A5AKGC4S.js → chunk-LPMDCQLL.js} +17 -27
- package/dist/chunk-LPMDCQLL.js.map +1 -0
- package/dist/{chunk-EWOFXJGH.cjs → chunk-M3OQLL7D.cjs} +63 -63
- package/dist/{chunk-EWOFXJGH.cjs.map → chunk-M3OQLL7D.cjs.map} +1 -1
- package/dist/{chunk-FKXDDJEQ.cjs → chunk-M4YQUHOR.cjs} +5 -5
- package/dist/{chunk-FKXDDJEQ.cjs.map → chunk-M4YQUHOR.cjs.map} +1 -1
- package/dist/{chunk-5XQMEDGE.cjs → chunk-OHJ4QOWN.cjs} +12 -12
- package/dist/{chunk-5XQMEDGE.cjs.map → chunk-OHJ4QOWN.cjs.map} +1 -1
- package/dist/{chunk-Y6I2DGKC.cjs → chunk-OMKI5T2G.cjs} +1 -1
- package/dist/chunk-OMKI5T2G.cjs.map +1 -0
- package/dist/{chunk-M5HGULIY.js → chunk-OQODJ26H.js} +5 -5
- package/dist/{chunk-MEWAFY6X.js → chunk-OT5IQMSI.js} +4 -4
- package/dist/{chunk-Y22VQDVW.js → chunk-QB244ZDI.js} +4 -4
- package/dist/{chunk-3T7ZP3TL.js → chunk-QL6ECU6Y.js} +3 -3
- package/dist/{chunk-3XDPXOMS.js → chunk-R2SZYRB2.js} +7 -7
- package/dist/{chunk-QAVT6P5F.cjs → chunk-RFA5T5MW.cjs} +28 -28
- package/dist/{chunk-QAVT6P5F.cjs.map → chunk-RFA5T5MW.cjs.map} +1 -1
- package/dist/{chunk-BTF3CNDP.js → chunk-ROQO355A.js} +1 -1
- package/dist/chunk-ROQO355A.js.map +1 -0
- package/dist/{chunk-43ADGEWL.cjs → chunk-UJAMXLA3.cjs} +40 -40
- package/dist/{chunk-43ADGEWL.cjs.map → chunk-UJAMXLA3.cjs.map} +1 -1
- package/dist/{chunk-KVNA3HU2.js → chunk-XANQTIHS.js} +3 -3
- package/dist/{chunk-PMN7WDXI.js → chunk-YUHOKPKS.js} +26 -13
- package/dist/chunk-YUHOKPKS.js.map +1 -0
- package/dist/components/case-studies/index.cjs +16 -16
- package/dist/components/case-studies/index.js +6 -6
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +7 -7
- package/dist/components/contact/index.js +6 -6
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +7 -7
- package/dist/components/embeds/index.js +6 -6
- package/dist/components/faq/index.cjs +7 -7
- package/dist/components/faq/index.js +6 -6
- package/dist/components/features/index.cjs +6 -6
- package/dist/components/features/index.js +5 -5
- package/dist/components/help-center-pages/index.cjs +26 -26
- package/dist/components/help-center-pages/index.js +17 -17
- package/dist/components/icons/index.cjs +2 -2
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/mlg-logo.d.ts.map +1 -1
- package/dist/components/index.cjs +56 -56
- package/dist/components/index.js +17 -17
- package/dist/components/meeting-scheduler/booking-form.d.ts +49 -5
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/context-panel.d.ts +9 -1
- package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +369 -186
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts +55 -1
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +344 -161
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/dist/components/meeting-scheduler/slot-picker.d.ts +8 -1
- package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +9 -9
- package/dist/components/navigation/index.js +8 -8
- package/dist/components/onboarding-guides/index.cjs +9 -9
- package/dist/components/onboarding-guides/index.js +8 -8
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +10 -10
- package/dist/components/tickets/index.js +9 -9
- package/dist/components/ui/benefit-card.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -6
- package/dist/components/ui/index.js +5 -5
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +10 -10
- package/dist/index.js +9 -9
- package/dist/schemas/meeting-booking-schema.cjs +19 -4
- package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts +41 -16
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +19 -5
- package/dist/schemas/meeting-booking-schema.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.cjs +18 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +18 -40
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/source-icons.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/dispatch.tsx +0 -57
- package/src/components/icons/mlg-logo.tsx +1 -7
- package/src/components/meeting-scheduler/booking-form.tsx +371 -152
- package/src/components/meeting-scheduler/context-panel.tsx +13 -3
- package/src/components/meeting-scheduler/index.tsx +316 -69
- package/src/components/meeting-scheduler/slot-picker.tsx +17 -1
- package/src/components/ui/benefit-card.tsx +30 -9
- package/src/schemas/meeting-booking-schema.ts +58 -4
- package/src/types/tmcg.ts +0 -2
- package/src/utils/format.ts +1 -1
- package/src/utils/source-icons.ts +0 -24
- package/dist/chunk-5EM6NGBK.cjs.map +0 -1
- package/dist/chunk-A5AKGC4S.js.map +0 -1
- package/dist/chunk-BTF3CNDP.js.map +0 -1
- package/dist/chunk-PMN7WDXI.js.map +0 -1
- package/dist/chunk-UBRVIVY4.cjs.map +0 -1
- package/dist/chunk-Y6I2DGKC.cjs.map +0 -1
- package/dist/chunk-YLVPQNLO.cjs.map +0 -1
- /package/dist/{chunk-F5Q3DTP5.js.map → chunk-7EUE3YTC.js.map} +0 -0
- /package/dist/{chunk-GBIC3VNA.js.map → chunk-7KGN7DV6.js.map} +0 -0
- /package/dist/{chunk-YZLUOZKR.js.map → chunk-7Q66A4CF.js.map} +0 -0
- /package/dist/{chunk-DRZ224RU.js.map → chunk-ANSZVKI2.js.map} +0 -0
- /package/dist/{chunk-N3RHX26N.js.map → chunk-AZZVSEP5.js.map} +0 -0
- /package/dist/{chunk-ZZHHKE2Q.js.map → chunk-ECXQX4EP.js.map} +0 -0
- /package/dist/{chunk-3OQIZPUX.js.map → chunk-EO3NEGWB.js.map} +0 -0
- /package/dist/{chunk-MOHSA65M.js.map → chunk-EZM5DNII.js.map} +0 -0
- /package/dist/{chunk-IKQVXZB5.js.map → chunk-GSPS6Y5U.js.map} +0 -0
- /package/dist/{chunk-HOLK4ZMT.js.map → chunk-II2GBWNW.js.map} +0 -0
- /package/dist/{chunk-M5HGULIY.js.map → chunk-OQODJ26H.js.map} +0 -0
- /package/dist/{chunk-MEWAFY6X.js.map → chunk-OT5IQMSI.js.map} +0 -0
- /package/dist/{chunk-Y22VQDVW.js.map → chunk-QB244ZDI.js.map} +0 -0
- /package/dist/{chunk-3T7ZP3TL.js.map → chunk-QL6ECU6Y.js.map} +0 -0
- /package/dist/{chunk-3XDPXOMS.js.map → chunk-R2SZYRB2.js.map} +0 -0
- /package/dist/{chunk-KVNA3HU2.js.map → chunk-XANQTIHS.js.map} +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
|
-
import type { Ref } from 'react';
|
|
4
|
+
import { Fragment, useEffect, useMemo } from 'react';
|
|
5
|
+
import type { ReactNode, Ref } from 'react';
|
|
6
6
|
import { useForm, Controller } from 'react-hook-form';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
makeDeferredBookingSchema,
|
|
9
9
|
isSupportedFormField,
|
|
10
10
|
type MeetingAvailability,
|
|
11
|
-
type
|
|
11
|
+
type MeetingFormField,
|
|
12
|
+
type BookingFormValues,
|
|
12
13
|
} from '../../schemas/meeting-booking-schema';
|
|
14
|
+
import { cn } from '../../utils/cn';
|
|
13
15
|
import {
|
|
14
16
|
Button,
|
|
15
17
|
FieldWrapper,
|
|
@@ -22,11 +24,37 @@ import {
|
|
|
22
24
|
SelectContent,
|
|
23
25
|
SelectItem,
|
|
24
26
|
Checkbox,
|
|
27
|
+
CheckboxBlock,
|
|
25
28
|
RadioGroup,
|
|
26
29
|
RadioGroupItem,
|
|
30
|
+
Skeleton,
|
|
27
31
|
} from '../ui';
|
|
28
32
|
import { HoneypotField } from '../ui/honeypot-field';
|
|
29
33
|
|
|
34
|
+
/**
|
|
35
|
+
* One field in a host-supplied row. `name` is a built-in (`email`, `firstName`,
|
|
36
|
+
* `lastName`) or a HubSpot-declared question's `name`.
|
|
37
|
+
*/
|
|
38
|
+
export interface BookingFieldSlot {
|
|
39
|
+
name: string;
|
|
40
|
+
/** Columns out of four at `md` and up. Defaults to an even split of the row. */
|
|
41
|
+
span?: 1 | 2 | 3 | 4;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type BookingFieldRow = BookingFieldSlot[];
|
|
45
|
+
|
|
46
|
+
/** Static so Tailwind's scanner sees every class — a template built from a
|
|
47
|
+
* runtime span would compile to nothing. */
|
|
48
|
+
const SPAN_CLASS: Record<1 | 2 | 3 | 4, string> = {
|
|
49
|
+
1: 'md:col-span-1',
|
|
50
|
+
2: 'md:col-span-2',
|
|
51
|
+
3: 'md:col-span-3',
|
|
52
|
+
4: 'md:col-span-4',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const evenSpan = (count: number): 1 | 2 | 3 | 4 =>
|
|
56
|
+
Math.min(4, Math.max(1, Math.floor(4 / Math.max(1, count)))) as 1 | 2 | 3 | 4;
|
|
57
|
+
|
|
30
58
|
/**
|
|
31
59
|
* BookingForm — attendee details + the link's declared custom questions +
|
|
32
60
|
* verbatim legal-consent copy. ContactForm's scaffolding (react-hook-form +
|
|
@@ -45,10 +73,34 @@ import { HoneypotField } from '../ui/honeypot-field';
|
|
|
45
73
|
export interface BookingFormProps {
|
|
46
74
|
availability: MeetingAvailability;
|
|
47
75
|
meetingId: string;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
/** Absent in `deferSlot` mode — the slot is chosen AFTER these answers. */
|
|
77
|
+
startTimeMs?: number;
|
|
78
|
+
durationMs?: number;
|
|
79
|
+
/** IANA zone the confirmation/invite should render in (parent-resolved).
|
|
80
|
+
* Null until hydration, which is why `deferSlot` relaxes it. */
|
|
81
|
+
timezone: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Collect-only mode (`flow="details-first"`): validate against the deferred
|
|
84
|
+
* schema, and hand the values up instead of POSTing. The parent re-attaches
|
|
85
|
+
* the authoritative slot/duration/timezone when it submits.
|
|
86
|
+
*/
|
|
87
|
+
deferSlot?: boolean;
|
|
88
|
+
/** Repopulates the form on a remount — the back edge, or an error return. */
|
|
89
|
+
initialValues?: Record<string, unknown>;
|
|
90
|
+
/** Defaults to "Confirm Booking"; details-first says "Continue". */
|
|
91
|
+
submitLabel?: string;
|
|
92
|
+
/** Small print beside the submit (details-first sets expectations). */
|
|
93
|
+
footerNote?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Re-arrange the fields into rows instead of the built-in order (email, the
|
|
96
|
+
* name pair, then each declared question full width). Reuse, not a fork: the
|
|
97
|
+
* SAME controls, validation, consent block and honeypot — only their grouping
|
|
98
|
+
* changes, so a host can match a mock without owning the machine.
|
|
99
|
+
*
|
|
100
|
+
* A slot naming nothing is skipped; a declared question no row claims is
|
|
101
|
+
* appended full width. Both are deliberate — see `slotNode`/`unplacedFields`.
|
|
102
|
+
*/
|
|
103
|
+
fieldRows?: BookingFieldRow[];
|
|
52
104
|
isSubmitting: boolean;
|
|
53
105
|
onSubmit: (payload: Record<string, unknown>) => Promise<void>;
|
|
54
106
|
/** From useHumanitySignals — parent owns the instance so it can resetSignals(). */
|
|
@@ -62,6 +114,11 @@ export function BookingForm({
|
|
|
62
114
|
startTimeMs,
|
|
63
115
|
durationMs,
|
|
64
116
|
timezone,
|
|
117
|
+
deferSlot = false,
|
|
118
|
+
initialValues,
|
|
119
|
+
submitLabel,
|
|
120
|
+
footerNote,
|
|
121
|
+
fieldRows,
|
|
65
122
|
isSubmitting,
|
|
66
123
|
onSubmit,
|
|
67
124
|
honeypotInputProps,
|
|
@@ -69,33 +126,74 @@ export function BookingForm({
|
|
|
69
126
|
}: BookingFormProps) {
|
|
70
127
|
const { formFields, legalConsent } = availability;
|
|
71
128
|
const supportedFields = useMemo(() => formFields.filter(isSupportedFormField), [formFields]);
|
|
72
|
-
|
|
129
|
+
// The DEFERRED schema in both flows: it is the wider of the two, and a strict
|
|
130
|
+
// resolver is not assignable to `Resolver<BookingFormValues>`. The strict
|
|
131
|
+
// schema is the server's contract — see `makeBookingSchema`'s docblock.
|
|
132
|
+
const schema = useMemo(
|
|
133
|
+
() => makeDeferredBookingSchema(supportedFields, legalConsent),
|
|
134
|
+
[supportedFields, legalConsent],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const consentDefaults = useMemo(
|
|
138
|
+
() =>
|
|
139
|
+
(legalConsent?.communicationConsentCheckboxes ?? []).map(c => ({
|
|
140
|
+
communicationTypeId: c.communicationTypeId,
|
|
141
|
+
consented: false,
|
|
142
|
+
})),
|
|
143
|
+
[legalConsent],
|
|
144
|
+
);
|
|
73
145
|
|
|
74
146
|
const {
|
|
75
147
|
register,
|
|
76
148
|
control,
|
|
77
149
|
handleSubmit,
|
|
150
|
+
setValue,
|
|
78
151
|
formState: { errors },
|
|
79
|
-
} = useForm<
|
|
152
|
+
} = useForm<BookingFormValues>({
|
|
80
153
|
resolver: zodResolver(schema),
|
|
81
154
|
defaultValues: {
|
|
82
155
|
meetingId,
|
|
83
|
-
|
|
84
|
-
durationMs,
|
|
156
|
+
// Genuinely OMITTED, not present-as-undefined, when the slot is deferred.
|
|
157
|
+
...(deferSlot ? {} : { startTimeMs, durationMs }),
|
|
85
158
|
timezone,
|
|
86
159
|
locale: typeof navigator !== 'undefined' ? navigator.language : undefined,
|
|
87
160
|
firstName: '',
|
|
88
161
|
lastName: '',
|
|
89
162
|
email: '',
|
|
90
163
|
formFields: {},
|
|
91
|
-
legalConsentResponses:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
95
|
-
},
|
|
164
|
+
legalConsentResponses: consentDefaults,
|
|
165
|
+
// A remount (back edge, or an error return) would otherwise lose every
|
|
166
|
+
// answer — `defaultValues` is snapshotted once and never re-read.
|
|
167
|
+
...(initialValues ?? {}),
|
|
168
|
+
} as BookingFormValues,
|
|
96
169
|
});
|
|
97
170
|
|
|
171
|
+
const priorConsents = initialValues?.legalConsentResponses as typeof consentDefaults | undefined;
|
|
172
|
+
|
|
173
|
+
// The seeded availability is refetched immediately on mount, so the link's
|
|
174
|
+
// consent set can change WHILE this form is open. Reconcile rather than
|
|
175
|
+
// remount: carry each `consented` across by id and default new ids to false.
|
|
176
|
+
// A wholesale reset would drop what the visitor has typed; leaving it alone
|
|
177
|
+
// would make a newly-declared required consent impossible to satisfy.
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
setValue(
|
|
180
|
+
'legalConsentResponses',
|
|
181
|
+
consentDefaults.map(next => ({
|
|
182
|
+
...next,
|
|
183
|
+
consented: priorConsents?.find(r => r.communicationTypeId === next.communicationTypeId)?.consented ?? false,
|
|
184
|
+
})),
|
|
185
|
+
{ shouldDirty: false },
|
|
186
|
+
);
|
|
187
|
+
}, [consentDefaults, priorConsents, setValue]);
|
|
188
|
+
|
|
98
189
|
const submit = handleSubmit(async data => {
|
|
190
|
+
if (deferSlot) {
|
|
191
|
+
// Collect-only. Signals are captured HERE, while this form and its
|
|
192
|
+
// honeypot are still mounted — `getSignals()` reads a detached ref once
|
|
193
|
+
// they unmount, which would silently disable the decoy.
|
|
194
|
+
await onSubmit({ ...data, meetingId });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
99
197
|
await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() });
|
|
100
198
|
});
|
|
101
199
|
|
|
@@ -116,6 +214,161 @@ export function BookingForm({
|
|
|
116
214
|
// smallest ODS step that houses it.
|
|
117
215
|
const FORM_STACK = 'flex flex-col gap-[var(--spacing-system-l)]';
|
|
118
216
|
|
|
217
|
+
// Every field as a standalone node, so the default order below and any
|
|
218
|
+
// host-supplied `fieldRows` compose the SAME controls. Extracting them is what
|
|
219
|
+
// makes an injected layout a re-arrangement rather than a fork of this form.
|
|
220
|
+
const builtInFields: Record<string, ReactNode> = {
|
|
221
|
+
email: (
|
|
222
|
+
<FieldWrapper label="Email" htmlFor="ms-email" error={errors.email?.message}>
|
|
223
|
+
<Input
|
|
224
|
+
id="ms-email"
|
|
225
|
+
type="email"
|
|
226
|
+
required
|
|
227
|
+
autoComplete="email"
|
|
228
|
+
placeholder="jane@company.com"
|
|
229
|
+
aria-invalid={!!errors.email}
|
|
230
|
+
className={inputClass}
|
|
231
|
+
{...register('email')}
|
|
232
|
+
/>
|
|
233
|
+
</FieldWrapper>
|
|
234
|
+
),
|
|
235
|
+
firstName: (
|
|
236
|
+
<FieldWrapper label="First Name" htmlFor="ms-first-name" error={errors.firstName?.message}>
|
|
237
|
+
<Input
|
|
238
|
+
id="ms-first-name"
|
|
239
|
+
required
|
|
240
|
+
autoComplete="given-name"
|
|
241
|
+
placeholder="Jane"
|
|
242
|
+
aria-invalid={!!errors.firstName}
|
|
243
|
+
className={inputClass}
|
|
244
|
+
{...register('firstName')}
|
|
245
|
+
/>
|
|
246
|
+
</FieldWrapper>
|
|
247
|
+
),
|
|
248
|
+
lastName: (
|
|
249
|
+
<FieldWrapper label="Last Name" htmlFor="ms-last-name" error={errors.lastName?.message}>
|
|
250
|
+
<Input
|
|
251
|
+
id="ms-last-name"
|
|
252
|
+
required
|
|
253
|
+
autoComplete="family-name"
|
|
254
|
+
placeholder="Doe"
|
|
255
|
+
aria-invalid={!!errors.lastName}
|
|
256
|
+
className={inputClass}
|
|
257
|
+
{...register('lastName')}
|
|
258
|
+
/>
|
|
259
|
+
</FieldWrapper>
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const renderDeclaredField = (field: MeetingFormField): ReactNode => (
|
|
264
|
+
<FieldWrapper key={field.name} label={field.label} htmlFor={`ms-q-${field.name}`} error={fieldError(field.name)}>
|
|
265
|
+
{field.type === 'textarea' && (
|
|
266
|
+
<Textarea
|
|
267
|
+
id={`ms-q-${field.name}`}
|
|
268
|
+
className="border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary"
|
|
269
|
+
{...register(`formFields.${field.name}` as never)}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
{field.type === 'text' && (
|
|
273
|
+
<Input
|
|
274
|
+
id={`ms-q-${field.name}`}
|
|
275
|
+
className="h-11 border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary md:h-12"
|
|
276
|
+
{...register(`formFields.${field.name}` as never)}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
{(field.type === 'select' || field.type === 'radio') && (
|
|
280
|
+
<Controller
|
|
281
|
+
control={control}
|
|
282
|
+
name={`formFields.${field.name}` as never}
|
|
283
|
+
render={({ field: rhf }) =>
|
|
284
|
+
field.type === 'select' ? (
|
|
285
|
+
<Select value={rhf.value ?? ''} onValueChange={rhf.onChange}>
|
|
286
|
+
<SelectTrigger id={`ms-q-${field.name}`}>
|
|
287
|
+
<SelectValue placeholder="Select…" />
|
|
288
|
+
</SelectTrigger>
|
|
289
|
+
<SelectContent>
|
|
290
|
+
{(field.options ?? []).map(opt => (
|
|
291
|
+
<SelectItem key={opt} value={opt}>
|
|
292
|
+
{opt}
|
|
293
|
+
</SelectItem>
|
|
294
|
+
))}
|
|
295
|
+
</SelectContent>
|
|
296
|
+
</Select>
|
|
297
|
+
) : (
|
|
298
|
+
<RadioGroup value={rhf.value ?? ''} onValueChange={rhf.onChange}>
|
|
299
|
+
{(field.options ?? []).map(opt => (
|
|
300
|
+
<div key={opt} className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
301
|
+
<RadioGroupItem id={`ms-q-${field.name}-${opt}`} value={opt} />
|
|
302
|
+
<Label htmlFor={`ms-q-${field.name}-${opt}`}>{opt}</Label>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</RadioGroup>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
{field.type === 'checkbox' && (
|
|
311
|
+
<Controller
|
|
312
|
+
control={control}
|
|
313
|
+
name={`formFields.${field.name}` as never}
|
|
314
|
+
render={({ field: rhf }) => (
|
|
315
|
+
// The question's Label above already carries the text and
|
|
316
|
+
// binds to this id — a second inline label would double the
|
|
317
|
+
// visible text AND the accessible name.
|
|
318
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
319
|
+
<Checkbox
|
|
320
|
+
id={`ms-q-${field.name}`}
|
|
321
|
+
checked={Boolean(rhf.value)}
|
|
322
|
+
onCheckedChange={v => rhf.onChange(v === true)}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
</FieldWrapper>
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const declaredByName = new Map(supportedFields.map(f => [f.name, f]));
|
|
332
|
+
|
|
333
|
+
const slotResolves = (name: string): boolean => Boolean(builtInFields[name] || declaredByName.has(name));
|
|
334
|
+
|
|
335
|
+
const slotNode = (name: string): ReactNode => {
|
|
336
|
+
const builtIn = builtInFields[name];
|
|
337
|
+
if (builtIn) return builtIn;
|
|
338
|
+
const declared = declaredByName.get(name);
|
|
339
|
+
// A name matching nothing is SKIPPED, not an error: a row may reference a
|
|
340
|
+
// question the link has not declared yet (declaring it in HubSpot is what
|
|
341
|
+
// turns it on), and throwing would take the whole form down over config.
|
|
342
|
+
return declared ? renderDeclaredField(declared) : null;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** Declared questions no row claims — appended full width, so a question added
|
|
346
|
+
* in HubSpot can never go invisible by omission from a layout written before it. */
|
|
347
|
+
const unplacedFields = fieldRows
|
|
348
|
+
? supportedFields.filter(f => !fieldRows.some(row => row.some(slot => slot.name === f.name)))
|
|
349
|
+
: [];
|
|
350
|
+
|
|
351
|
+
/** Rows whose every slot names a question the link has not declared are
|
|
352
|
+
* DROPPED, not rendered empty: an empty grid still eats one form gap, so a
|
|
353
|
+
* layout written ahead of the HubSpot config would print blank bands. */
|
|
354
|
+
const placedRows = (fieldRows ?? [])
|
|
355
|
+
.map(row => row.filter(slot => slotResolves(slot.name)))
|
|
356
|
+
.filter(row => row.length > 0);
|
|
357
|
+
|
|
358
|
+
const submitButton = (
|
|
359
|
+
<Button
|
|
360
|
+
type="submit"
|
|
361
|
+
loading={isSubmitting}
|
|
362
|
+
disabled={isSubmitting}
|
|
363
|
+
// The details-first footer draws a 240px action beside its note
|
|
364
|
+
// (`4904:117335`); the bare slot-first row keeps the button at its natural
|
|
365
|
+
// width, as it always has.
|
|
366
|
+
className={footerNote ? 'md:w-60' : undefined}
|
|
367
|
+
>
|
|
368
|
+
{submitLabel ?? 'Confirm Booking'}
|
|
369
|
+
</Button>
|
|
370
|
+
);
|
|
371
|
+
|
|
119
372
|
return (
|
|
120
373
|
<form onSubmit={submit} className={FORM_STACK} noValidate>
|
|
121
374
|
<HoneypotField {...honeypotInputProps} />
|
|
@@ -132,116 +385,46 @@ export function BookingForm({
|
|
|
132
385
|
submit button off the bottom the moment validation fails. Required-ness
|
|
133
386
|
is carried by `required` on the control (read out by assistive tech),
|
|
134
387
|
not by an asterisk the design does not draw. */}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
aria-invalid={!!errors.firstName}
|
|
156
|
-
className={inputClass}
|
|
157
|
-
{...register('firstName')}
|
|
158
|
-
/>
|
|
159
|
-
</FieldWrapper>
|
|
160
|
-
<FieldWrapper label="Last Name" htmlFor="ms-last-name" error={errors.lastName?.message}>
|
|
161
|
-
<Input
|
|
162
|
-
id="ms-last-name"
|
|
163
|
-
required
|
|
164
|
-
autoComplete="family-name"
|
|
165
|
-
placeholder="Doe"
|
|
166
|
-
aria-invalid={!!errors.lastName}
|
|
167
|
-
className={inputClass}
|
|
168
|
-
{...register('lastName')}
|
|
169
|
-
/>
|
|
170
|
-
</FieldWrapper>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
{supportedFields.map(field => (
|
|
174
|
-
<FieldWrapper
|
|
175
|
-
key={field.name}
|
|
176
|
-
label={field.label}
|
|
177
|
-
htmlFor={`ms-q-${field.name}`}
|
|
178
|
-
error={fieldError(field.name)}
|
|
179
|
-
>
|
|
180
|
-
{field.type === 'textarea' && (
|
|
181
|
-
<Textarea
|
|
182
|
-
id={`ms-q-${field.name}`}
|
|
183
|
-
className="border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary"
|
|
184
|
-
{...register(`formFields.${field.name}` as never)}
|
|
185
|
-
/>
|
|
186
|
-
)}
|
|
187
|
-
{field.type === 'text' && (
|
|
188
|
-
<Input
|
|
189
|
-
id={`ms-q-${field.name}`}
|
|
190
|
-
className="h-11 border-ods-border bg-ods-card px-3 text-ods-text-primary placeholder-ods-text-secondary md:h-12"
|
|
191
|
-
{...register(`formFields.${field.name}` as never)}
|
|
192
|
-
/>
|
|
193
|
-
)}
|
|
194
|
-
{(field.type === 'select' || field.type === 'radio') && (
|
|
195
|
-
<Controller
|
|
196
|
-
control={control}
|
|
197
|
-
name={`formFields.${field.name}` as never}
|
|
198
|
-
render={({ field: rhf }) =>
|
|
199
|
-
field.type === 'select' ? (
|
|
200
|
-
<Select value={rhf.value ?? ''} onValueChange={rhf.onChange}>
|
|
201
|
-
<SelectTrigger id={`ms-q-${field.name}`}>
|
|
202
|
-
<SelectValue placeholder="Select…" />
|
|
203
|
-
</SelectTrigger>
|
|
204
|
-
<SelectContent>
|
|
205
|
-
{(field.options ?? []).map(opt => (
|
|
206
|
-
<SelectItem key={opt} value={opt}>
|
|
207
|
-
{opt}
|
|
208
|
-
</SelectItem>
|
|
209
|
-
))}
|
|
210
|
-
</SelectContent>
|
|
211
|
-
</Select>
|
|
212
|
-
) : (
|
|
213
|
-
<RadioGroup value={rhf.value ?? ''} onValueChange={rhf.onChange}>
|
|
214
|
-
{(field.options ?? []).map(opt => (
|
|
215
|
-
<div key={opt} className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
216
|
-
<RadioGroupItem id={`ms-q-${field.name}-${opt}`} value={opt} />
|
|
217
|
-
<Label htmlFor={`ms-q-${field.name}-${opt}`}>{opt}</Label>
|
|
218
|
-
</div>
|
|
219
|
-
))}
|
|
220
|
-
</RadioGroup>
|
|
221
|
-
)
|
|
222
|
-
}
|
|
223
|
-
/>
|
|
224
|
-
)}
|
|
225
|
-
{field.type === 'checkbox' && (
|
|
226
|
-
<Controller
|
|
227
|
-
control={control}
|
|
228
|
-
name={`formFields.${field.name}` as never}
|
|
229
|
-
render={({ field: rhf }) => (
|
|
230
|
-
// The question's Label above already carries the text and
|
|
231
|
-
// binds to this id — a second inline label would double the
|
|
232
|
-
// visible text AND the accessible name.
|
|
233
|
-
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
234
|
-
<Checkbox
|
|
235
|
-
id={`ms-q-${field.name}`}
|
|
236
|
-
checked={Boolean(rhf.value)}
|
|
237
|
-
onCheckedChange={v => rhf.onChange(v === true)}
|
|
238
|
-
/>
|
|
388
|
+
{fieldRows ? (
|
|
389
|
+
<>
|
|
390
|
+
{placedRows.map((row, rowIndex) => (
|
|
391
|
+
<div
|
|
392
|
+
key={row.map(s => s.name).join('|') || rowIndex}
|
|
393
|
+
className="grid grid-cols-2 gap-[var(--spacing-system-m)] md:grid-cols-4"
|
|
394
|
+
>
|
|
395
|
+
{row.map(slot => (
|
|
396
|
+
<div
|
|
397
|
+
key={slot.name}
|
|
398
|
+
className={cn(
|
|
399
|
+
// A two-field row stays side by side on a phone — the rule
|
|
400
|
+
// the built-in name pair has always followed: two short
|
|
401
|
+
// fields cost one line instead of two on the layout that can
|
|
402
|
+
// least afford them.
|
|
403
|
+
row.length === 2 ? 'col-span-1' : 'col-span-2',
|
|
404
|
+
SPAN_CLASS[slot.span ?? evenSpan(row.length)],
|
|
405
|
+
)}
|
|
406
|
+
>
|
|
407
|
+
{slotNode(slot.name)}
|
|
239
408
|
</div>
|
|
240
|
-
)}
|
|
241
|
-
|
|
242
|
-
)}
|
|
243
|
-
|
|
244
|
-
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
))}
|
|
412
|
+
{unplacedFields.map(field => (
|
|
413
|
+
<Fragment key={field.name}>{renderDeclaredField(field)}</Fragment>
|
|
414
|
+
))}
|
|
415
|
+
</>
|
|
416
|
+
) : (
|
|
417
|
+
<>
|
|
418
|
+
{builtInFields.email}
|
|
419
|
+
<div className="grid grid-cols-2 gap-[var(--spacing-system-m)]">
|
|
420
|
+
{builtInFields.firstName}
|
|
421
|
+
{builtInFields.lastName}
|
|
422
|
+
</div>
|
|
423
|
+
{supportedFields.map(field => (
|
|
424
|
+
<Fragment key={field.name}>{renderDeclaredField(field)}</Fragment>
|
|
425
|
+
))}
|
|
426
|
+
</>
|
|
427
|
+
)}
|
|
245
428
|
|
|
246
429
|
{legalConsent && (
|
|
247
430
|
// Same out-of-flow message as the fields above, so a missed consent
|
|
@@ -249,40 +432,44 @@ export function BookingForm({
|
|
|
249
432
|
// wrapper is `display:contents` and the panel below is the flex item,
|
|
250
433
|
// exactly as it was.
|
|
251
434
|
<FieldWrapper error={errors.legalConsentResponses?.message}>
|
|
252
|
-
<div className="flex flex-col gap-[var(--spacing-system-xs)]
|
|
253
|
-
{/* GDPR surface — HubSpot's consent copy rendered VERBATIM, never edited. */}
|
|
254
|
-
<p className="text-ods-text-secondary text-h6">{legalConsent.processingConsentText}</p>
|
|
255
|
-
{legalConsent.communicationConsentText && (
|
|
256
|
-
<p className="text-ods-text-secondary text-h6">{legalConsent.communicationConsentText}</p>
|
|
257
|
-
)}
|
|
435
|
+
<div className="flex flex-col gap-[var(--spacing-system-xs)]">
|
|
258
436
|
<Controller
|
|
259
437
|
control={control}
|
|
260
438
|
name="legalConsentResponses"
|
|
261
439
|
render={({ field: rhf }) => (
|
|
262
440
|
<>
|
|
263
|
-
{legalConsent.communicationConsentCheckboxes.map(box => {
|
|
441
|
+
{legalConsent.communicationConsentCheckboxes.map((box, index) => {
|
|
264
442
|
const responses = (rhf.value ?? []) as Array<{ communicationTypeId: string; consented: boolean }>;
|
|
265
443
|
const current = responses.find(r => r.communicationTypeId === box.communicationTypeId);
|
|
266
444
|
return (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
445
|
+
// The design system's consent row (`checkbox-block`, the
|
|
446
|
+
// same block the waitlist form uses): box, label, caption
|
|
447
|
+
// in one bordered row. GDPR surface — HubSpot's copy is
|
|
448
|
+
// rendered VERBATIM, never edited. The processing statement
|
|
449
|
+
// rides as the caption of the FIRST row, once: it describes
|
|
450
|
+
// the consent as a whole, not each channel.
|
|
451
|
+
<CheckboxBlock
|
|
452
|
+
key={box.communicationTypeId}
|
|
453
|
+
id={`ms-consent-${box.communicationTypeId}`}
|
|
454
|
+
checked={current?.consented ?? false}
|
|
455
|
+
onCheckedChange={v =>
|
|
456
|
+
rhf.onChange(
|
|
457
|
+
responses.map(r =>
|
|
458
|
+
r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v } : r,
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
label={box.label}
|
|
463
|
+
description={index === 0 ? legalConsent.processingConsentText : undefined}
|
|
464
|
+
/>
|
|
281
465
|
);
|
|
282
466
|
})}
|
|
283
467
|
</>
|
|
284
468
|
)}
|
|
285
469
|
/>
|
|
470
|
+
{legalConsent.communicationConsentText && (
|
|
471
|
+
<p className="text-ods-text-secondary text-h6">{legalConsent.communicationConsentText}</p>
|
|
472
|
+
)}
|
|
286
473
|
{legalConsent.privacyPolicyText && (
|
|
287
474
|
<p className="text-ods-text-secondary text-h6">{legalConsent.privacyPolicyText}</p>
|
|
288
475
|
)}
|
|
@@ -293,11 +480,43 @@ export function BookingForm({
|
|
|
293
480
|
{/* Step navigation back to the calendar lives in the step header (the
|
|
294
481
|
app-standard BackButton, rendered by the parent) — the form ships
|
|
295
482
|
only its submit. */}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
483
|
+
{/* Two shapes, not one with a placeholder: without a note this must stay
|
|
484
|
+
the bare `flex` row it has always been, or the submit slides from the
|
|
485
|
+
left edge to the right on every existing slot-first booking. */}
|
|
486
|
+
{footerNote ? (
|
|
487
|
+
<div className="flex items-center justify-between gap-[var(--spacing-system-m)]">
|
|
488
|
+
<p className="text-ods-text-secondary text-h6">{footerNote}</p>
|
|
489
|
+
{submitButton}
|
|
490
|
+
</div>
|
|
491
|
+
) : (
|
|
492
|
+
<div className="flex">{submitButton}</div>
|
|
493
|
+
)}
|
|
301
494
|
</form>
|
|
302
495
|
);
|
|
303
496
|
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Cold-start placeholder for `flow="details-first"`, where the FORM is the
|
|
500
|
+
* first thing in the action panel.
|
|
501
|
+
*
|
|
502
|
+
* The slot-first skeleton (`SlotPickerSkeleton`) would put a grey calendar
|
|
503
|
+
* where the form belongs — above the fold on a page whose entire content is
|
|
504
|
+
* this card. Same footprint discipline as its sibling: fixed heights, no shift
|
|
505
|
+
* when the real form swaps in.
|
|
506
|
+
*/
|
|
507
|
+
export function BookingFormSkeleton() {
|
|
508
|
+
return (
|
|
509
|
+
<div className="flex flex-1 flex-col gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)] lg:p-0">
|
|
510
|
+
<div className="grid grid-cols-1 gap-[var(--spacing-system-m)] md:grid-cols-2">
|
|
511
|
+
<Skeleton className="h-[4.75rem] w-full" />
|
|
512
|
+
<Skeleton className="h-[4.75rem] w-full" />
|
|
513
|
+
</div>
|
|
514
|
+
<Skeleton className="h-[4.75rem] w-full" />
|
|
515
|
+
<Skeleton className="h-[7.75rem] w-full" />
|
|
516
|
+
<Skeleton className="h-[4.25rem] w-full" />
|
|
517
|
+
<div className="flex justify-end">
|
|
518
|
+
<Skeleton className="h-12 w-40" />
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
@@ -47,6 +47,14 @@ export interface SchedulerContextPanelProps {
|
|
|
47
47
|
* states the length and a live selector beside a filled-in form invites a
|
|
48
48
|
* change that would throw the form away. Going Back restores them. */
|
|
49
49
|
locked?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The link's length is not the visitor's to choose here: the static duration
|
|
52
|
+
* line renders and the chips never do, at every step. `flow="details-first"`
|
|
53
|
+
* sets it — the mock draws "30 min call" beside the host on the calendar step
|
|
54
|
+
* and offers no selector — and unlike `locked` it has NO effect on the
|
|
55
|
+
* timezone picker, which on that step is the only control the visitor has.
|
|
56
|
+
*/
|
|
57
|
+
fixedDuration?: boolean;
|
|
50
58
|
/** Whether the timezone picker renders. Default true: the zone governs every
|
|
51
59
|
* time on screen, the form's summary line included, so it stays reachable
|
|
52
60
|
* right up to the confirmation — where there is nothing left to re-read. */
|
|
@@ -165,6 +173,7 @@ export function SchedulerContextPanel({
|
|
|
165
173
|
onBack,
|
|
166
174
|
backLabel = 'Back',
|
|
167
175
|
locked = false,
|
|
176
|
+
fixedDuration = false,
|
|
168
177
|
showTimezone = true,
|
|
169
178
|
className,
|
|
170
179
|
}: SchedulerContextPanelProps) {
|
|
@@ -223,8 +232,9 @@ export function SchedulerContextPanel({
|
|
|
223
232
|
</div>
|
|
224
233
|
)}
|
|
225
234
|
|
|
226
|
-
{locked || durationsMs.length <= 1
|
|
227
|
-
? // Post-selection, or a link that offers one
|
|
235
|
+
{locked || fixedDuration || durationsMs.length <= 1
|
|
236
|
+
? // Post-selection, a fixed-length flow, or a link that offers one
|
|
237
|
+
// length: a static line,
|
|
228
238
|
// which is what sits beside the host on the header-strip layouts.
|
|
229
239
|
selectedDurationMs != null && (
|
|
230
240
|
<p className="shrink-0 text-ods-text-secondary text-h6">
|
|
@@ -239,7 +249,7 @@ export function SchedulerContextPanel({
|
|
|
239
249
|
|
|
240
250
|
{/* Several lengths on offer: chips, which need a full row of their
|
|
241
251
|
own at every width — so they stay OUT of the identity row. */}
|
|
242
|
-
{!locked && durationsMs.length > 1 && (
|
|
252
|
+
{!locked && !fixedDuration && durationsMs.length > 1 && (
|
|
243
253
|
<div className="flex flex-col gap-[var(--spacing-system-xxs)]">
|
|
244
254
|
<p className={FIELD_LABEL_CLASS}>Duration</p>
|
|
245
255
|
<div className="flex flex-wrap gap-[var(--spacing-system-xs)]">
|