@flamingo-stack/openframe-frontend-core 0.0.561-1852.4997.2 → 0.0.561

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 (146) hide show
  1. package/dist/{chunk-KGFCHGCW.js → chunk-44U25GD5.js} +2 -2
  2. package/dist/{chunk-DCB4EBA5.cjs → chunk-4LHEQMOV.cjs} +3 -3
  3. package/dist/chunk-4LHEQMOV.cjs.map +1 -0
  4. package/dist/{chunk-PYN65LCP.js → chunk-5AEHCGQV.js} +6 -6
  5. package/dist/{chunk-K4UCFJNL.cjs → chunk-AGQ4VTUX.cjs} +29 -29
  6. package/dist/{chunk-K4UCFJNL.cjs.map → chunk-AGQ4VTUX.cjs.map} +1 -1
  7. package/dist/{chunk-HI3A5HFI.js → chunk-B6TM3QAL.js} +4 -4
  8. package/dist/{chunk-NISIZABN.cjs → chunk-CR2CMLTQ.cjs} +4 -4
  9. package/dist/{chunk-NISIZABN.cjs.map → chunk-CR2CMLTQ.cjs.map} +1 -1
  10. package/dist/{chunk-K535WIFS.cjs → chunk-D3OY4CJU.cjs} +3 -3
  11. package/dist/{chunk-K535WIFS.cjs.map → chunk-D3OY4CJU.cjs.map} +1 -1
  12. package/dist/{chunk-RVHAX7VP.cjs → chunk-DAVVVBNL.cjs} +26 -26
  13. package/dist/{chunk-RVHAX7VP.cjs.map → chunk-DAVVVBNL.cjs.map} +1 -1
  14. package/dist/{chunk-V5VJTNOG.cjs → chunk-DHRJFUBR.cjs} +11 -11
  15. package/dist/{chunk-V5VJTNOG.cjs.map → chunk-DHRJFUBR.cjs.map} +1 -1
  16. package/dist/{chunk-LNWLK4TC.cjs → chunk-EK346HD7.cjs} +39 -39
  17. package/dist/{chunk-LNWLK4TC.cjs.map → chunk-EK346HD7.cjs.map} +1 -1
  18. package/dist/{chunk-SKQ7AMUD.cjs → chunk-FSWJCGRJ.cjs} +7 -7
  19. package/dist/{chunk-SKQ7AMUD.cjs.map → chunk-FSWJCGRJ.cjs.map} +1 -1
  20. package/dist/{chunk-3IBCKS3I.js → chunk-GKGQ4DWE.js} +121 -49
  21. package/dist/chunk-GKGQ4DWE.js.map +1 -0
  22. package/dist/{chunk-XTDUFU5O.cjs → chunk-GWRQ5EEI.cjs} +954 -882
  23. package/dist/{chunk-XTDUFU5O.cjs.map → chunk-GWRQ5EEI.cjs.map} +1 -1
  24. package/dist/{chunk-IJ6HBN2J.cjs → chunk-GYC3OTBI.cjs} +9 -9
  25. package/dist/{chunk-IJ6HBN2J.cjs.map → chunk-GYC3OTBI.cjs.map} +1 -1
  26. package/dist/{chunk-SFU6A36Q.js → chunk-JCQUWIPT.js} +3 -3
  27. package/dist/{chunk-NZNKHIFA.js → chunk-KRYLVEXU.js} +2 -2
  28. package/dist/{chunk-LSWJ5QYP.js → chunk-KWNVQZ5Y.js} +4 -4
  29. package/dist/{chunk-ZD5QQJH4.js → chunk-MLSDBNKT.js} +2 -2
  30. package/dist/{chunk-NTTSXF7Z.js → chunk-NGY4KET4.js} +2 -2
  31. package/dist/{chunk-O45VULK4.js → chunk-OCJG3X4U.js} +2 -2
  32. package/dist/{chunk-KKIAKLI6.js → chunk-ONDI64M3.js} +2 -2
  33. package/dist/{chunk-FMQO7OMJ.js → chunk-QGQ26NEE.js} +2 -2
  34. package/dist/{chunk-RSZXB6J2.js → chunk-RDKJCZHY.js} +3 -3
  35. package/dist/chunk-RDKJCZHY.js.map +1 -0
  36. package/dist/{chunk-ELFXMLSX.cjs → chunk-SUPBFRUK.cjs} +5 -5
  37. package/dist/{chunk-ELFXMLSX.cjs.map → chunk-SUPBFRUK.cjs.map} +1 -1
  38. package/dist/{chunk-BODITFZI.cjs → chunk-UJ7MSY6F.cjs} +31 -31
  39. package/dist/{chunk-BODITFZI.cjs.map → chunk-UJ7MSY6F.cjs.map} +1 -1
  40. package/dist/{chunk-INN323YY.cjs → chunk-UV53Z64O.cjs} +14 -14
  41. package/dist/{chunk-INN323YY.cjs.map → chunk-UV53Z64O.cjs.map} +1 -1
  42. package/dist/{chunk-TCQ3VL5R.cjs → chunk-WCM4BGHO.cjs} +97 -97
  43. package/dist/{chunk-TCQ3VL5R.cjs.map → chunk-WCM4BGHO.cjs.map} +1 -1
  44. package/dist/{chunk-DQI2S2SY.js → chunk-YUAKZNK5.js} +6 -6
  45. package/dist/{chunk-5XPYQ7NU.cjs → chunk-ZENRHE55.cjs} +62 -62
  46. package/dist/{chunk-5XPYQ7NU.cjs.map → chunk-ZENRHE55.cjs.map} +1 -1
  47. package/dist/{chunk-CLQ6L4JG.js → chunk-ZJ7RI62F.js} +2 -2
  48. package/dist/components/calendar.cjs +81 -4
  49. package/dist/components/calendar.cjs.map +1 -1
  50. package/dist/components/calendar.js +81 -4
  51. package/dist/components/calendar.js.map +1 -1
  52. package/dist/components/case-studies/index.cjs +9 -9
  53. package/dist/components/case-studies/index.js +3 -3
  54. package/dist/components/chat/index.cjs +3 -3
  55. package/dist/components/chat/index.js +2 -2
  56. package/dist/components/contact/index.cjs +4 -4
  57. package/dist/components/contact/index.js +3 -3
  58. package/dist/components/docs/index.cjs +6 -6
  59. package/dist/components/docs/index.js +5 -5
  60. package/dist/components/embeds/index.cjs +4 -4
  61. package/dist/components/embeds/index.js +3 -3
  62. package/dist/components/faq/index.cjs +4 -4
  63. package/dist/components/faq/index.js +3 -3
  64. package/dist/components/features/index.cjs +5 -3
  65. package/dist/components/features/index.cjs.map +1 -1
  66. package/dist/components/features/index.d.ts +1 -1
  67. package/dist/components/features/index.d.ts.map +1 -1
  68. package/dist/components/features/index.js +4 -2
  69. package/dist/components/features/use-walkthrough-video.d.ts +1 -1
  70. package/dist/components/features/use-walkthrough-video.d.ts.map +1 -1
  71. package/dist/components/features/{floating-walkthrough-video.d.ts → walkthrough-video.d.ts} +61 -7
  72. package/dist/components/features/walkthrough-video.d.ts.map +1 -0
  73. package/dist/components/help-center-pages/index.cjs +23 -23
  74. package/dist/components/help-center-pages/index.js +14 -14
  75. package/dist/components/index.cjs +144 -140
  76. package/dist/components/index.cjs.map +1 -1
  77. package/dist/components/index.js +16 -12
  78. package/dist/components/index.js.map +1 -1
  79. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  80. package/dist/components/meeting-scheduler/context-panel.d.ts +24 -6
  81. package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
  82. package/dist/components/meeting-scheduler/index.cjs +529 -425
  83. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  84. package/dist/components/meeting-scheduler/index.d.ts +54 -1
  85. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  86. package/dist/components/meeting-scheduler/index.js +507 -403
  87. package/dist/components/meeting-scheduler/index.js.map +1 -1
  88. package/dist/components/meeting-scheduler/slot-picker.d.ts +35 -31
  89. package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
  90. package/dist/components/navigation/index.cjs +3 -3
  91. package/dist/components/navigation/index.js +2 -2
  92. package/dist/components/onboarding-guides/index.cjs +6 -6
  93. package/dist/components/onboarding-guides/index.js +5 -5
  94. package/dist/components/related-content/index.cjs +4 -4
  95. package/dist/components/related-content/index.js +3 -3
  96. package/dist/components/tickets/index.cjs +7 -7
  97. package/dist/components/tickets/index.js +6 -6
  98. package/dist/components/ui/date-picker.d.ts +62 -1
  99. package/dist/components/ui/date-picker.d.ts.map +1 -1
  100. package/dist/components/ui/field-wrapper.d.ts +7 -0
  101. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  102. package/dist/components/ui/file-manager/index.cjs +36 -36
  103. package/dist/components/ui/file-manager/index.js +1 -1
  104. package/dist/components/ui/index.cjs +5 -3
  105. package/dist/components/ui/index.cjs.map +1 -1
  106. package/dist/components/ui/index.js +4 -2
  107. package/dist/hooks/use-meeting-booking.d.ts +31 -8
  108. package/dist/hooks/use-meeting-booking.d.ts.map +1 -1
  109. package/dist/index.cjs +7 -3
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.js +6 -2
  112. package/package.json +1 -1
  113. package/src/components/features/{.floating-walkthrough-video.md → .walkthrough-video.md} +8 -5
  114. package/src/components/features/board/types.ts +1 -1
  115. package/src/components/features/index.ts +1 -1
  116. package/src/components/features/use-walkthrough-video.ts +1 -1
  117. package/src/components/features/{floating-walkthrough-video.tsx → walkthrough-video.tsx} +108 -22
  118. package/src/components/meeting-scheduler/booking-form.tsx +60 -51
  119. package/src/components/meeting-scheduler/confirmation.tsx +1 -1
  120. package/src/components/meeting-scheduler/context-panel.tsx +166 -68
  121. package/src/components/meeting-scheduler/index.tsx +227 -81
  122. package/src/components/meeting-scheduler/slot-picker.tsx +275 -127
  123. package/src/components/ui/date-picker.tsx +200 -47
  124. package/src/components/ui/field-wrapper.tsx +9 -2
  125. package/src/hooks/use-meeting-booking.ts +97 -73
  126. package/dist/chunk-3IBCKS3I.js.map +0 -1
  127. package/dist/chunk-7J4DGFHZ.js +0 -88
  128. package/dist/chunk-7J4DGFHZ.js.map +0 -1
  129. package/dist/chunk-DCB4EBA5.cjs.map +0 -1
  130. package/dist/chunk-MKPJYEOR.cjs +0 -88
  131. package/dist/chunk-MKPJYEOR.cjs.map +0 -1
  132. package/dist/chunk-RSZXB6J2.js.map +0 -1
  133. package/dist/components/features/floating-walkthrough-video.d.ts.map +0 -1
  134. /package/dist/{chunk-KGFCHGCW.js.map → chunk-44U25GD5.js.map} +0 -0
  135. /package/dist/{chunk-PYN65LCP.js.map → chunk-5AEHCGQV.js.map} +0 -0
  136. /package/dist/{chunk-HI3A5HFI.js.map → chunk-B6TM3QAL.js.map} +0 -0
  137. /package/dist/{chunk-SFU6A36Q.js.map → chunk-JCQUWIPT.js.map} +0 -0
  138. /package/dist/{chunk-NZNKHIFA.js.map → chunk-KRYLVEXU.js.map} +0 -0
  139. /package/dist/{chunk-LSWJ5QYP.js.map → chunk-KWNVQZ5Y.js.map} +0 -0
  140. /package/dist/{chunk-ZD5QQJH4.js.map → chunk-MLSDBNKT.js.map} +0 -0
  141. /package/dist/{chunk-NTTSXF7Z.js.map → chunk-NGY4KET4.js.map} +0 -0
  142. /package/dist/{chunk-O45VULK4.js.map → chunk-OCJG3X4U.js.map} +0 -0
  143. /package/dist/{chunk-KKIAKLI6.js.map → chunk-ONDI64M3.js.map} +0 -0
  144. /package/dist/{chunk-FMQO7OMJ.js.map → chunk-QGQ26NEE.js.map} +0 -0
  145. /package/dist/{chunk-DQI2S2SY.js.map → chunk-YUAKZNK5.js.map} +0 -0
  146. /package/dist/{chunk-CLQ6L4JG.js.map → chunk-ZJ7RI62F.js.map} +0 -0
@@ -1,18 +1,16 @@
1
1
  "use client";
2
2
  "use client";
3
- import {
4
- Calendar
5
- } from "../../chunk-7J4DGFHZ.js";
6
3
  import {
7
4
  PersistentPaginationWrapper
8
- } from "../../chunk-CLQ6L4JG.js";
5
+ } from "../../chunk-ZJ7RI62F.js";
9
6
  import {
10
7
  EmptyState
11
- } from "../../chunk-KKIAKLI6.js";
8
+ } from "../../chunk-ONDI64M3.js";
12
9
  import {
13
10
  Autocomplete,
14
11
  AvatarStack,
15
12
  BackButton,
13
+ DatePickerCalendar,
16
14
  HoneypotField,
17
15
  MAX_MONTH_OFFSET,
18
16
  RadioGroup,
@@ -26,7 +24,7 @@ import {
26
24
  StatusBadge,
27
25
  Textarea,
28
26
  formatDurationCompact
29
- } from "../../chunk-3IBCKS3I.js";
27
+ } from "../../chunk-GKGQ4DWE.js";
30
28
  import "../../chunk-7EUIW46X.js";
31
29
  import "../../chunk-LXC6P2EO.js";
32
30
  import "../../chunk-EL5YVPD5.js";
@@ -36,17 +34,16 @@ import {
36
34
  } from "../../chunk-ZNEVGXMU.js";
37
35
  import "../../chunk-ZJJERZUA.js";
38
36
  import "../../chunk-GJAHN7LH.js";
39
- import {
40
- ClockIcon
41
- } from "../../chunk-JMZH4KMJ.js";
37
+ import "../../chunk-JMZH4KMJ.js";
42
38
  import "../../chunk-W72U7OU7.js";
43
39
  import "../../chunk-UWY722RH.js";
44
40
  import {
45
41
  Checkbox,
42
+ FieldWrapper,
46
43
  Input,
47
44
  Label,
48
45
  Skeleton
49
- } from "../../chunk-RSZXB6J2.js";
46
+ } from "../../chunk-RDKJCZHY.js";
50
47
  import "../../chunk-6U3IUD57.js";
51
48
  import {
52
49
  Button
@@ -64,10 +61,11 @@ import "../../chunk-GGWZFCYS.js";
64
61
 
65
62
  // src/components/meeting-scheduler/index.tsx
66
63
  init_cn();
67
- import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef2, useState as useState3 } from "react";
64
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef2, useState as useState3 } from "react";
68
65
 
69
66
  // src/hooks/use-meeting-booking.ts
70
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
67
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
68
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
71
69
  var KNOWN_CODES = [
72
70
  "SLOT_TAKEN",
73
71
  "VALIDATION",
@@ -75,15 +73,15 @@ var KNOWN_CODES = [
75
73
  "TEMPORARILY_UNAVAILABLE",
76
74
  "MEETING_UNAVAILABLE"
77
75
  ];
76
+ var AVAILABILITY_STALE_MS = 3e4;
77
+ function meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) {
78
+ return ["meeting-availability", apiBaseUrl, meetingId, monthOffset];
79
+ }
78
80
  function useMeetingBooking(options) {
79
81
  const { meetingId, apiBaseUrl = "", initialAvailability } = options;
80
- const seededFor = initialAvailability?.meetingId === meetingId ? initialAvailability : void 0;
81
- const [availability, setAvailability] = useState(seededFor ?? null);
82
- const [monthOffset, setMonthOffsetState] = useState(seededFor?.monthOffset ?? 0);
83
- const [isLoadingAvailability, setIsLoadingAvailability] = useState(!seededFor);
84
- const [availabilityError, setAvailabilityError] = useState(null);
85
- const [isSubmitting, setIsSubmitting] = useState(false);
86
- const submittingRef = useRef(false);
82
+ const queryClient = useQueryClient();
83
+ const seededOffset = initialAvailability?.meetingId === meetingId ? initialAvailability.monthOffset : void 0;
84
+ const [monthOffset, setMonthOffsetState] = useState(seededOffset ?? 0);
87
85
  const setMonthOffset = useCallback((offset) => {
88
86
  setMonthOffsetState(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)));
89
87
  }, []);
@@ -93,48 +91,27 @@ function useMeetingBooking(options) {
93
91
  prevMeetingIdRef.current = meetingId;
94
92
  setMonthOffsetState(0);
95
93
  }, [meetingId]);
96
- useEffect(() => {
97
- const controller = new AbortController();
98
- let active = true;
99
- const load = async () => {
100
- setAvailabilityError(null);
101
- if (!availability || availability.meetingId !== meetingId || availability.monthOffset !== monthOffset) {
102
- setIsLoadingAvailability(true);
103
- }
104
- try {
105
- const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) });
106
- const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, {
107
- signal: controller.signal
108
- });
109
- if (!res.ok) throw new Error(`availability ${res.status}`);
110
- const data = await res.json();
111
- if (active) setAvailability(data);
112
- } catch (err) {
113
- if (!active || err instanceof DOMException && err.name === "AbortError") return;
114
- setAvailabilityError(err instanceof Error ? err.message : "Failed to load availability");
115
- } finally {
116
- if (active) setIsLoadingAvailability(false);
117
- }
118
- };
119
- void load();
120
- return () => {
121
- active = false;
122
- controller.abort();
123
- };
124
- }, [meetingId, monthOffset, apiBaseUrl]);
125
- const refetchAvailability = useCallback(async () => {
126
- try {
94
+ const seed = initialAvailability?.meetingId === meetingId && initialAvailability.monthOffset === monthOffset ? initialAvailability : void 0;
95
+ const availabilityQuery = useQuery({
96
+ queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset),
97
+ queryFn: async ({ signal }) => {
127
98
  const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) });
128
- const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`);
129
- if (res.ok) setAvailability(await res.json());
130
- } catch {
131
- }
132
- }, [meetingId, monthOffset, apiBaseUrl]);
133
- const book = useCallback(
134
- async (payload) => {
135
- if (submittingRef.current) return { ok: false, code: "VALIDATION", message: "Already submitting" };
136
- submittingRef.current = true;
137
- setIsSubmitting(true);
99
+ const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, { signal });
100
+ if (!res.ok) throw new Error(`availability ${res.status}`);
101
+ return await res.json();
102
+ },
103
+ staleTime: AVAILABILITY_STALE_MS,
104
+ initialData: seed,
105
+ // Stamped as infinitely old on purpose: the seed is first-paint
106
+ // scaffolding for a 60-second-volatile resource, not truth, so it paints
107
+ // immediately AND is refetched immediately.
108
+ initialDataUpdatedAt: seed ? 0 : void 0
109
+ });
110
+ const refetchAvailability = useCallback(async () => {
111
+ await queryClient.invalidateQueries({ queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) }).catch(() => void 0);
112
+ }, [queryClient, apiBaseUrl, meetingId, monthOffset]);
113
+ const bookMutation = useMutation({
114
+ mutationFn: async (payload) => {
138
115
  try {
139
116
  const res = await contentFetch(`${apiBaseUrl}/api/meetings/book`, {
140
117
  method: "POST",
@@ -148,22 +125,37 @@ function useMeetingBooking(options) {
148
125
  return { ok: false, code, message: typeof data.error === "string" ? data.error : void 0 };
149
126
  } catch {
150
127
  return { ok: false, code: "TEMPORARILY_UNAVAILABLE" };
128
+ }
129
+ },
130
+ // A booking is not a read — never retried, at any layer.
131
+ retry: false
132
+ });
133
+ const submittingRef = useRef(false);
134
+ const { mutateAsync } = bookMutation;
135
+ const book = useCallback(
136
+ async (payload) => {
137
+ if (submittingRef.current) return { ok: false, code: "VALIDATION", message: "Already submitting" };
138
+ submittingRef.current = true;
139
+ try {
140
+ return await mutateAsync(payload);
151
141
  } finally {
152
142
  submittingRef.current = false;
153
- setIsSubmitting(false);
154
143
  }
155
144
  },
156
- [apiBaseUrl]
145
+ [mutateAsync]
157
146
  );
158
147
  return {
159
- availability,
160
- isLoadingAvailability,
161
- availabilityError,
148
+ availability: availabilityQuery.data ?? null,
149
+ /** No availability for this month yet — the times column has nothing to show. */
150
+ isLoadingAvailability: availabilityQuery.isPending,
151
+ /** ANY request in flight, including a silent revalidation of cached data. */
152
+ isFetchingAvailability: availabilityQuery.isFetching,
153
+ availabilityError: availabilityQuery.error ? availabilityQuery.error.message : null,
162
154
  monthOffset,
163
155
  setMonthOffset,
164
156
  refetchAvailability,
165
157
  book,
166
- isSubmitting
158
+ isSubmitting: bookMutation.isPending
167
159
  };
168
160
  }
169
161
 
@@ -258,7 +250,14 @@ function makeBookingSchema(formFields, legalConsent) {
258
250
  // src/components/meeting-scheduler/context-panel.tsx
259
251
  import { useMemo } from "react";
260
252
  init_cn();
261
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
253
+ import { jsx, jsxs } from "react/jsx-runtime";
254
+ var PANEL_STACK = "flex flex-col gap-[var(--spacing-system-l)]";
255
+ var IDENTITY_ROW = "flex items-center justify-between gap-[var(--spacing-system-m)] lg:flex-col lg:items-stretch lg:gap-[var(--spacing-system-l)]";
256
+ var STRIP_BODY = "flex flex-col gap-[var(--spacing-system-l)] md:flex-row md:items-start lg:flex-col";
257
+ var STRIP_MAIN = "flex min-w-0 flex-col gap-[var(--spacing-system-l)] md:flex-1";
258
+ var STRIP_ASIDE = "flex flex-col gap-[var(--spacing-system-xxs)] md:w-[280px] md:shrink-0 lg:w-full";
259
+ var BACK_BUTTON_CLASS = "py-0";
260
+ var FIELD_LABEL_CLASS = "text-h4 text-ods-text-primary";
262
261
  function zoneLabel(tz) {
263
262
  try {
264
263
  const parts = new Intl.DateTimeFormat("en-US", {
@@ -271,25 +270,28 @@ function zoneLabel(tz) {
271
270
  return tz.replace(/_/g, " ");
272
271
  }
273
272
  }
274
- function ContextPanelSkeleton({ className }) {
275
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-[var(--spacing-system-mf)]", className), children: [
276
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-s)]", children: [
277
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-12 rounded-full shrink-0" }),
278
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-[var(--spacing-system-xxs)]", children: [
279
- /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-32" }),
280
- /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
281
- ] })
282
- ] }),
283
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
284
- /* @__PURE__ */ jsx("p", { className: "text-h5 text-ods-text-secondary", children: "Duration" }),
285
- /* @__PURE__ */ jsxs("div", { className: "flex gap-[var(--spacing-system-xs)]", children: [
286
- /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-20" }),
287
- /* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-20" })
273
+ function ContextPanelSkeleton({
274
+ className,
275
+ onBack,
276
+ backLabel = "Back"
277
+ }) {
278
+ return /* @__PURE__ */ jsxs("div", { className: cn(PANEL_STACK, className), children: [
279
+ onBack && /* @__PURE__ */ jsx(BackButton, { label: backLabel, onClick: onBack, className: BACK_BUTTON_CLASS }),
280
+ /* @__PURE__ */ jsxs("div", { className: STRIP_BODY, children: [
281
+ /* @__PURE__ */ jsx("div", { className: STRIP_MAIN, children: /* @__PURE__ */ jsxs("div", { className: IDENTITY_ROW, children: [
282
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xs)] lg:flex-none", children: [
283
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-8 shrink-0 rounded-full" }),
284
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-[var(--spacing-system-xxs)]", children: [
285
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-32" }),
286
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })
287
+ ] })
288
+ ] }),
289
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20 shrink-0" })
290
+ ] }) }),
291
+ /* @__PURE__ */ jsxs("div", { className: STRIP_ASIDE, children: [
292
+ /* @__PURE__ */ jsx("p", { className: FIELD_LABEL_CLASS, children: "Timezone" }),
293
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" })
288
294
  ] })
289
- ] }),
290
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
291
- /* @__PURE__ */ jsx("p", { className: "text-h5 text-ods-text-secondary", children: "Timezone" }),
292
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" })
293
295
  ] })
294
296
  ] });
295
297
  }
@@ -302,7 +304,10 @@ function SchedulerContextPanel({
302
304
  onSelectDuration,
303
305
  timezone,
304
306
  onTimezoneChange,
307
+ onBack,
308
+ backLabel = "Back",
305
309
  locked = false,
310
+ showTimezone = true,
306
311
  className
307
312
  }) {
308
313
  const zoneOptions = useMemo(() => {
@@ -317,49 +322,67 @@ function SchedulerContextPanel({
317
322
  if (!zones.includes(timezone)) zones = [timezone, ...zones];
318
323
  return zones.map((tz) => ({ value: tz, label: zoneLabel(tz) }));
319
324
  }, [timezone]);
320
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-[var(--spacing-system-mf)]", className), children: [
321
- hosts.length > 0 && hosts.length <= 3 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-[var(--spacing-system-s)]", children: hosts.map((host) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-s)]", children: [
322
- /* @__PURE__ */ jsx(SquareAvatar, { variant: "round", size: "lg", src: host.avatarUrl ?? void 0, alt: host.name, fallback: host.name }),
323
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0", children: [
324
- /* @__PURE__ */ jsx("p", { className: "text-h4 text-ods-text-primary truncate", children: host.name }),
325
- host.title && /* @__PURE__ */ jsx("p", { className: "text-h6 text-ods-text-secondary truncate", children: host.title })
326
- ] })
327
- ] }, host.name)) }),
328
- hosts.length > 3 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
329
- /* @__PURE__ */ jsx(AvatarStack, { people: hosts, max: 4, size: "lg" }),
330
- /* @__PURE__ */ jsxs("p", { className: "text-h6 text-ods-text-secondary", children: [
331
- hosts.length,
332
- " hosts on this calendar"
333
- ] })
334
- ] }),
335
- title && /* @__PURE__ */ jsx("p", { className: "text-h3 text-ods-text-primary", children: title }),
336
- description && /* @__PURE__ */ jsx("p", { className: "text-h6 text-ods-text-secondary", children: description }),
337
- locked ? (
338
- // Post-selection: selectors are GONE, not disabled the step summary
339
- // owns the chosen time/zone; the panel keeps a single static line.
340
- selectedDurationMs != null && /* @__PURE__ */ jsxs("p", { className: "text-h6 text-ods-text-secondary", children: [
341
- formatDurationCompact(selectedDurationMs / 1e3),
342
- " call"
343
- ] })
344
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
345
- durationsMs.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
346
- /* @__PURE__ */ jsx("p", { className: "text-h5 text-ods-text-secondary", children: "Duration" }),
347
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-[var(--spacing-system-xs)]", children: durationsMs.map((ms) => /* @__PURE__ */ jsx(
348
- Button,
349
- {
350
- variant: selectedDurationMs === ms ? void 0 : "outline",
351
- size: "small-legacy",
352
- onClick: () => onSelectDuration(ms),
353
- children: formatDurationCompact(ms / 1e3)
354
- },
355
- ms
356
- )) })
357
- ] }) : selectedDurationMs != null && /* @__PURE__ */ jsxs("p", { className: "text-h6 text-ods-text-secondary", children: [
358
- formatDurationCompact(selectedDurationMs / 1e3),
359
- " call"
325
+ return /* @__PURE__ */ jsxs("div", { className: cn(PANEL_STACK, className), children: [
326
+ onBack && /* @__PURE__ */ jsx(BackButton, { label: backLabel, onClick: onBack, className: BACK_BUTTON_CLASS }),
327
+ /* @__PURE__ */ jsxs("div", { className: STRIP_BODY, children: [
328
+ /* @__PURE__ */ jsxs("div", { className: STRIP_MAIN, children: [
329
+ /* @__PURE__ */ jsxs("div", { className: IDENTITY_ROW, children: [
330
+ hosts.length > 0 && hosts.length <= 3 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-[var(--spacing-system-s)]", children: hosts.map((host) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
331
+ /* @__PURE__ */ jsx(
332
+ SquareAvatar,
333
+ {
334
+ variant: "round",
335
+ size: "sm",
336
+ src: host.avatarUrl ?? void 0,
337
+ alt: host.name,
338
+ fallback: host.name
339
+ }
340
+ ),
341
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0", children: [
342
+ /* @__PURE__ */ jsx("p", { className: "text-h4 text-ods-text-primary truncate", children: host.name }),
343
+ host.title && /* @__PURE__ */ jsx("p", { className: "text-h6 text-ods-text-secondary truncate", children: host.title })
344
+ ] })
345
+ ] }, host.name)) }),
346
+ hosts.length > 3 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
347
+ /* @__PURE__ */ jsx(AvatarStack, { people: hosts, max: 4, size: "lg" }),
348
+ /* @__PURE__ */ jsxs("p", { className: "text-h6 text-ods-text-secondary", children: [
349
+ hosts.length,
350
+ " hosts on this calendar"
351
+ ] })
352
+ ] }),
353
+ locked || durationsMs.length <= 1 ? (
354
+ // Post-selection, or a link that offers one length: a static line,
355
+ // which is what sits beside the host on the header-strip layouts.
356
+ selectedDurationMs != null && /* @__PURE__ */ jsxs("p", { className: "shrink-0 text-h6 text-ods-text-secondary", children: [
357
+ formatDurationCompact(selectedDurationMs / 1e3),
358
+ " call"
359
+ ] })
360
+ ) : null
361
+ ] }),
362
+ title && /* @__PURE__ */ jsx("p", { className: "text-h3 text-ods-text-primary", children: title }),
363
+ description && /* @__PURE__ */ jsx("p", { className: "text-h6 text-ods-text-secondary", children: description }),
364
+ !locked && durationsMs.length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xxs)]", children: [
365
+ /* @__PURE__ */ jsx("p", { className: FIELD_LABEL_CLASS, children: "Duration" }),
366
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-[var(--spacing-system-xs)]", children: durationsMs.map((ms) => /* @__PURE__ */ jsx(
367
+ Button,
368
+ {
369
+ variant: selectedDurationMs === ms ? void 0 : "outline",
370
+ size: "small-legacy",
371
+ onClick: () => onSelectDuration(ms),
372
+ children: formatDurationCompact(ms / 1e3)
373
+ },
374
+ ms
375
+ )) })
376
+ ] })
360
377
  ] }),
361
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-[var(--spacing-system-xs)]", children: [
362
- /* @__PURE__ */ jsx("p", { className: "text-h5 text-ods-text-secondary", children: "Timezone" }),
378
+ showTimezone && // Once a slot is chosen the picker is desktop-only, which is what all
379
+ // three mocks draw. On the narrow layouts this panel is a strip
380
+ // COMPETING with the form for the screen's height, and the step's own
381
+ // summary line states the zone one row below — so the fact survives,
382
+ // only the control steps aside. The 280px sidebar has the room, so
383
+ // there it stays.
384
+ /* @__PURE__ */ jsxs("div", { className: cn(STRIP_ASIDE, locked && "max-lg:hidden"), children: [
385
+ /* @__PURE__ */ jsx("p", { className: FIELD_LABEL_CLASS, children: "Timezone" }),
363
386
  timezone ? /* @__PURE__ */ jsx(
364
387
  Autocomplete,
365
388
  {
@@ -369,7 +392,6 @@ function SchedulerContextPanel({
369
392
  },
370
393
  options: zoneOptions,
371
394
  placeholder: "Search timezone\u2026",
372
- startAdornment: /* @__PURE__ */ jsx(ClockIcon, { className: "size-4 shrink-0 text-ods-text-secondary" }),
373
395
  noOptionsText: "No matching timezone",
374
396
  showClearAll: false
375
397
  }
@@ -380,10 +402,10 @@ function SchedulerContextPanel({
380
402
  }
381
403
 
382
404
  // src/components/meeting-scheduler/slot-picker.tsx
383
- import { useMemo as useMemo2 } from "react";
384
- import { ChevronLeft, ChevronRight } from "lucide-react";
405
+ import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
406
+ import { ChevronRight } from "lucide-react";
385
407
  init_cn();
386
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
408
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
387
409
  function dayKeyInZone(ms, timeZone) {
388
410
  return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(
389
411
  new Date(ms)
@@ -400,38 +422,109 @@ function dateFromDayKey(key) {
400
422
  function timeLabelInZone(ms, timeZone) {
401
423
  return new Intl.DateTimeFormat(void 0, { timeZone, hour: "numeric", minute: "2-digit" }).format(new Date(ms));
402
424
  }
403
- var CALENDAR_MIN_H = "min-h-[20.5rem]";
404
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
425
+ var CALENDAR_W = "md:w-[19rem] md:shrink-0 lg:w-[16rem]";
426
+ var COLUMN_HEADING_CLASS = "shrink-0 text-h4 text-ods-text-primary";
427
+ var COLUMN_STACK = "flex min-h-0 flex-col gap-[var(--spacing-system-xxs)]";
428
+ var SLOT_PICKER_HEADING = "Select Date & Time";
429
+ var CHIP_GRID_CLASS = cn(
430
+ "grid grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] content-start gap-[var(--spacing-system-xs)]",
431
+ "overflow-y-auto",
432
+ // PHONE: a stated window (~3 rows, the mock's 156px) that scrolls, rather
433
+ // than a list that runs the length of the day. Nothing else on the phone
434
+ // card has a height — the design lets each step be as tall as it needs —
435
+ // but a busy day is forty slots, and letting them all through turns a
436
+ // 700px card into a 1500px one and buries whatever the page puts below
437
+ // it. This is the one nested scroller worth having: a small, obviously
438
+ // bounded list with a visible edge, exactly as drawn.
439
+ "max-h-[9.75rem] md:max-h-none",
440
+ // No height of its own, and none borrowed from the calendar either: the card
441
+ // states one height, the panel and this row pass it down, and what is left
442
+ // after the column heading IS the scroll box. A long day scrolls inside it.
443
+ //
444
+ // `min-h-0` is the load-bearing half — a flex item's default
445
+ // `min-height:auto` refuses to shrink below its content, so without it the
446
+ // chips grow the column, then the row, then the card, and `overflow-y-auto`
447
+ // never gets a box smaller than its contents to scroll inside. That is the
448
+ // exact failure that kept this card at 404px when it was told to be 318.
449
+ "flex-1 min-h-0"
450
+ );
451
+ var CHIP_CLASS = "w-full px-[var(--spacing-system-xxs)]";
452
+ var SLOT_ROW_CLASS = cn(
453
+ "flex min-w-0 flex-col md:flex-row",
454
+ // Default `stretch` at every width, deliberately. Stacked, that is what
455
+ // makes each section span the card — an `items-start` here (it was here, to
456
+ // stop a column growing taller than its month) collapses both sections to
457
+ // their intrinsic WIDTH instead, which on a phone left the calendar and the
458
+ // chips huddled against the left edge with half the card empty. The
459
+ // calendar no longer needs protecting: its height is fixed, not derived.
460
+ "md:min-h-0 md:flex-1 lg:gap-[var(--spacing-system-lf)]"
461
+ );
462
+ var COLUMN_BOX = "p-[var(--spacing-system-l)] lg:p-0";
463
+ var COLUMN_DIVIDER = "border-b border-ods-border md:border-b-0 md:border-r lg:border-0";
405
464
  function monthLabelFor(offset) {
406
465
  const now = /* @__PURE__ */ new Date();
407
466
  const month = new Date(now.getFullYear(), now.getMonth() + offset, 1);
408
467
  return new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(month);
409
468
  }
410
- function CalendarSkeleton({ monthOffset = 0 }) {
411
- return /* @__PURE__ */ jsxs2("div", { className: cn("flex flex-col gap-[var(--spacing-system-s)]", CALENDAR_MIN_H), children: [
412
- /* @__PURE__ */ jsxs2("div", { className: "relative h-8", children: [
413
- /* @__PURE__ */ jsxs2("div", { className: "absolute inset-x-0 top-0 flex h-8 items-center justify-between", children: [
414
- /* @__PURE__ */ jsx2("span", { className: "flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60", children: /* @__PURE__ */ jsx2(ChevronLeft, { className: "h-4 w-4 text-ods-text-muted" }) }),
415
- /* @__PURE__ */ jsx2("span", { className: "flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60", children: /* @__PURE__ */ jsx2(ChevronRight, { className: "h-4 w-4 text-ods-text-muted" }) })
416
- ] }),
417
- /* @__PURE__ */ jsx2("div", { className: "flex h-8 items-center justify-center", children: /* @__PURE__ */ jsx2("p", { className: "text-h6 text-ods-text-primary", children: monthLabelFor(monthOffset) }) })
418
- ] }),
419
- /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-y-1 py-1", children: [
420
- /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-7", children: WEEKDAY_LABELS.map((d) => /* @__PURE__ */ jsx2("div", { className: "flex h-9 items-center justify-center text-h6 font-normal text-ods-text-secondary", children: d }, d)) }),
421
- Array.from({ length: 6 }, (_, row) => /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-7", children: Array.from({ length: 7 }, (_2, col) => /* @__PURE__ */ jsx2("div", { className: "flex h-9 items-center justify-center", children: /* @__PURE__ */ jsx2(Skeleton, { className: "h-9 w-9 rounded-md" }) }, col)) }, `w-${row}`))
422
- ] })
423
- ] });
469
+ var DISABLE_EVERY_DAY = () => true;
470
+ var NOOP = () => void 0;
471
+ function DayCalendar({ monthOffset, onMonthOffsetChange, selectedDay, onSelectDay, slotsByDay }) {
472
+ const now = /* @__PURE__ */ new Date();
473
+ const startMonth = new Date(now.getFullYear(), now.getMonth(), 1);
474
+ const endMonth = new Date(now.getFullYear(), now.getMonth() + MAX_MONTH_OFFSET + 1, 0);
475
+ const visibleMonth = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1);
476
+ const dayHasNoSlots = useCallback2(
477
+ (day) => !slotsByDay?.has(calendarDayKey(day)),
478
+ [slotsByDay]
479
+ );
480
+ return /* @__PURE__ */ jsx2("div", { className: "flex min-h-0 flex-1 flex-col", "aria-busy": slotsByDay === null, children: /* @__PURE__ */ jsx2(
481
+ DatePickerCalendar,
482
+ {
483
+ mode: "single",
484
+ bare: true,
485
+ month: visibleMonth,
486
+ onMonthChange: (month) => {
487
+ const offset = (month.getFullYear() - now.getFullYear()) * 12 + (month.getMonth() - now.getMonth());
488
+ onMonthOffsetChange(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)));
489
+ },
490
+ fromDate: startMonth,
491
+ toDate: endMonth,
492
+ selected: selectedDay ? dateFromDayKey(selectedDay) : void 0,
493
+ onSelect: (value) => {
494
+ const day = value;
495
+ if (!day) return;
496
+ const key = calendarDayKey(day);
497
+ if (slotsByDay?.has(key)) onSelectDay(key);
498
+ },
499
+ disabledDays: slotsByDay === null ? DISABLE_EVERY_DAY : dayHasNoSlots
500
+ }
501
+ ) });
424
502
  }
425
503
  function SlotPickerSkeleton({ monthOffset = 0 }) {
426
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col md:flex-row gap-[var(--spacing-system-lf)]", children: [
427
- /* @__PURE__ */ jsx2("div", { className: cn("shrink-0", CALENDAR_MIN_H), children: /* @__PURE__ */ jsx2(CalendarSkeleton, { monthOffset }) }),
428
- /* @__PURE__ */ jsx2("div", { className: "flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]", children: /* @__PURE__ */ jsx2(TimeChipsSkeleton, {}) })
504
+ return /* @__PURE__ */ jsxs2("div", { className: SLOT_ROW_CLASS, children: [
505
+ /* @__PURE__ */ jsxs2("div", { className: cn(COLUMN_STACK, COLUMN_BOX, COLUMN_DIVIDER, CALENDAR_W, "md:overflow-y-auto"), children: [
506
+ /* @__PURE__ */ jsx2("p", { className: COLUMN_HEADING_CLASS, children: SLOT_PICKER_HEADING }),
507
+ /* @__PURE__ */ jsx2(
508
+ DayCalendar,
509
+ {
510
+ monthOffset,
511
+ onMonthOffsetChange: NOOP,
512
+ selectedDay: null,
513
+ onSelectDay: NOOP,
514
+ slotsByDay: null
515
+ }
516
+ )
517
+ ] }),
518
+ /* @__PURE__ */ jsx2("div", { className: cn(COLUMN_STACK, COLUMN_BOX, "min-w-0 md:flex-1"), children: /* @__PURE__ */ jsx2(TimeChipsSkeleton, {}) })
429
519
  ] });
430
520
  }
431
521
  function TimeChipsSkeleton() {
432
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
433
- /* @__PURE__ */ jsx2(Skeleton, { className: "h-4 md:h-5 w-40" }),
434
- /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start", children: Array.from({ length: 9 }, (_, i) => /* @__PURE__ */ jsx2(Skeleton, { className: "h-10 w-full" }, i)) })
522
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
523
+ /* @__PURE__ */ jsx2("div", { className: COLUMN_HEADING_CLASS, children: /* @__PURE__ */ jsx2(Skeleton, { className: "inline-block h-4 w-40 align-middle" }) }),
524
+ /* @__PURE__ */ jsx2("div", { className: CHIP_GRID_CLASS, children: Array.from({ length: 16 }, (_, i) => (
525
+ // h-11 md:h-12 — the loaded chip's own default-size height.
526
+ /* @__PURE__ */ jsx2(Skeleton, { className: cn(CHIP_CLASS, "h-11 md:h-12") }, i)
527
+ )) })
435
528
  ] });
436
529
  }
437
530
  function SlotPicker({
@@ -456,58 +549,70 @@ function SlotPicker({
456
549
  for (const list of map.values()) list.sort((a, b) => a - b);
457
550
  return map;
458
551
  }, [slots, timezone]);
459
- const now = /* @__PURE__ */ new Date();
460
- const startMonth = new Date(now.getFullYear(), now.getMonth(), 1);
461
- const endMonth = new Date(now.getFullYear(), now.getMonth() + MAX_MONTH_OFFSET, 1);
462
- const visibleMonth = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1);
552
+ const visibleMonth = useMemo2(() => {
553
+ const now = /* @__PURE__ */ new Date();
554
+ return new Date(now.getFullYear(), now.getMonth() + monthOffset, 1);
555
+ }, [monthOffset]);
463
556
  const daySlots = selectedDay ? slotsByDay.get(selectedDay) ?? [] : [];
464
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col md:flex-row gap-[var(--spacing-system-lf)]", children: [
465
- /* @__PURE__ */ jsx2("div", { className: cn("shrink-0", CALENDAR_MIN_H), children: isLoading ? /* @__PURE__ */ jsx2(CalendarSkeleton, { monthOffset }) : /* @__PURE__ */ jsx2(
466
- Calendar,
467
- {
468
- month: visibleMonth,
469
- startMonth,
470
- endMonth,
471
- onMonthChange: (month) => {
472
- const offset = (month.getFullYear() - now.getFullYear()) * 12 + (month.getMonth() - now.getMonth());
473
- onMonthOffsetChange(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)));
474
- },
475
- mode: "single",
476
- required: true,
477
- selected: selectedDay ? dateFromDayKey(selectedDay) : void 0,
478
- onSelect: (day) => {
479
- if (!day) return;
480
- const key = calendarDayKey(day);
481
- if (slotsByDay.has(key)) onSelectDay(key);
482
- },
483
- disabled: (day) => !slotsByDay.has(calendarDayKey(day)),
484
- className: "p-0"
485
- }
486
- ) }),
487
- /* @__PURE__ */ jsx2("div", { className: "flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]", children: isLoading ? /* @__PURE__ */ jsx2(TimeChipsSkeleton, {}) : selectedDay ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
488
- /* @__PURE__ */ jsx2("p", { className: "text-h6 text-ods-text-primary", children: new Intl.DateTimeFormat(void 0, {
557
+ const monthPrefix = `${visibleMonth.getFullYear()}-${String(visibleMonth.getMonth() + 1).padStart(2, "0")}`;
558
+ const monthHasSlots = [...slotsByDay.keys()].some((key) => key.startsWith(monthPrefix));
559
+ return /* @__PURE__ */ jsxs2("div", { className: SLOT_ROW_CLASS, children: [
560
+ /* @__PURE__ */ jsxs2("div", { className: cn(COLUMN_STACK, COLUMN_BOX, COLUMN_DIVIDER, CALENDAR_W, "md:overflow-y-auto"), children: [
561
+ /* @__PURE__ */ jsx2("p", { className: COLUMN_HEADING_CLASS, children: SLOT_PICKER_HEADING }),
562
+ /* @__PURE__ */ jsx2(
563
+ DayCalendar,
564
+ {
565
+ monthOffset,
566
+ onMonthOffsetChange,
567
+ selectedDay,
568
+ onSelectDay,
569
+ slotsByDay: isLoading ? null : slotsByDay
570
+ }
571
+ )
572
+ ] }),
573
+ /* @__PURE__ */ jsx2("div", { className: cn(COLUMN_STACK, COLUMN_BOX, "min-w-0 md:flex-1"), children: isLoading ? /* @__PURE__ */ jsx2(TimeChipsSkeleton, {}) : selectedDay ? /* @__PURE__ */ jsxs2(Fragment, { children: [
574
+ /* @__PURE__ */ jsx2("p", { className: COLUMN_HEADING_CLASS, children: new Intl.DateTimeFormat(void 0, {
489
575
  weekday: "long",
490
576
  month: "long",
491
577
  day: "numeric"
492
578
  }).format(dateFromDayKey(selectedDay)) }),
493
- /* @__PURE__ */ jsx2("div", { className: "grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start max-h-80 overflow-y-auto", children: daySlots.map((ms) => {
579
+ /* @__PURE__ */ jsx2("div", { className: CHIP_GRID_CLASS, children: daySlots.map((ms) => {
494
580
  const isSelected = selectedSlot === ms;
495
581
  return /* @__PURE__ */ jsx2(
496
582
  Button,
497
583
  {
498
584
  variant: isSelected ? void 0 : "outline",
499
- size: "small-legacy",
500
585
  onClick: () => onSelectSlot(ms),
586
+ className: CHIP_CLASS,
501
587
  children: timeLabelInZone(ms, timezone)
502
588
  },
503
589
  ms
504
590
  );
505
591
  }) })
506
- ] }) : /* @__PURE__ */ jsx2("p", { className: "text-h6 text-ods-text-secondary", children: [...slotsByDay.keys()].some(
507
- (k) => k.startsWith(
508
- `${visibleMonth.getFullYear()}-${String(visibleMonth.getMonth() + 1).padStart(2, "0")}`
509
- )
510
- ) ? "Pick a day to see available times." : "No available times this month \u2014 try the next one." }) })
592
+ ] }) : monthHasSlots ? /* @__PURE__ */ jsx2("p", { className: "text-h6 text-ods-text-secondary", children: "Pick a day to see available times." }) : (
593
+ // A fully-booked month used to just SAY "try the next one" and leave
594
+ // the visitor to find the chevron in the other column. The way out
595
+ // of a dead end belongs in the dead end.
596
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-start gap-[var(--spacing-system-s)]", children: [
597
+ /* @__PURE__ */ jsxs2("p", { className: "text-h6 text-ods-text-secondary", children: [
598
+ "No available times in ",
599
+ monthLabelFor(monthOffset),
600
+ "."
601
+ ] }),
602
+ monthOffset < MAX_MONTH_OFFSET && /* @__PURE__ */ jsxs2(
603
+ Button,
604
+ {
605
+ variant: "outline",
606
+ onClick: () => onMonthOffsetChange(monthOffset + 1),
607
+ rightIcon: /* @__PURE__ */ jsx2(ChevronRight, { className: "size-5" }),
608
+ children: [
609
+ "Check ",
610
+ monthLabelFor(monthOffset + 1)
611
+ ]
612
+ }
613
+ )
614
+ ] })
615
+ ) })
511
616
  ] });
512
617
  }
513
618
 
@@ -515,7 +620,7 @@ function SlotPicker({
515
620
  import { useMemo as useMemo3 } from "react";
516
621
  import { useForm, Controller } from "react-hook-form";
517
622
  import { zodResolver } from "@hookform/resolvers/zod";
518
- import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
623
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
519
624
  function BookingForm({
520
625
  availability,
521
626
  meetingId,
@@ -560,124 +665,115 @@ function BookingForm({
560
665
  const err = errors.formFields?.[name];
561
666
  return err?.message;
562
667
  };
563
- const inputClass = "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12";
564
- return /* @__PURE__ */ jsxs3("form", { onSubmit: submit, className: "flex flex-col space-y-4 md:space-y-6", noValidate: true, children: [
668
+ const inputClass = "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12";
669
+ const FORM_STACK = "flex flex-col gap-[var(--spacing-system-l)]";
670
+ return /* @__PURE__ */ jsxs3("form", { onSubmit: submit, className: FORM_STACK, noValidate: true, children: [
565
671
  /* @__PURE__ */ jsx3(HoneypotField, { ...honeypotInputProps }),
566
- /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6", children: [
567
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
568
- /* @__PURE__ */ jsxs3(Label, { htmlFor: "ms-first-name", children: [
569
- "First name",
570
- /* @__PURE__ */ jsx3("span", { className: "text-ods-accent", children: "*" })
571
- ] }),
572
- /* @__PURE__ */ jsx3(
573
- Input,
574
- {
575
- id: "ms-first-name",
576
- autoComplete: "given-name",
577
- placeholder: "Jane",
578
- "aria-invalid": !!errors.firstName,
579
- className: inputClass,
580
- ...register("firstName")
581
- }
582
- ),
583
- errors.firstName && /* @__PURE__ */ jsx3("span", { className: "text-ods-error text-h6 mt-1", children: errors.firstName.message })
584
- ] }),
585
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
586
- /* @__PURE__ */ jsxs3(Label, { htmlFor: "ms-last-name", children: [
587
- "Last name",
588
- /* @__PURE__ */ jsx3("span", { className: "text-ods-accent", children: "*" })
589
- ] }),
590
- /* @__PURE__ */ jsx3(
591
- Input,
592
- {
593
- id: "ms-last-name",
594
- autoComplete: "family-name",
595
- placeholder: "Doe",
596
- "aria-invalid": !!errors.lastName,
597
- className: inputClass,
598
- ...register("lastName")
599
- }
600
- ),
601
- errors.lastName && /* @__PURE__ */ jsx3("span", { className: "text-ods-error text-h6 mt-1", children: errors.lastName.message })
602
- ] })
603
- ] }),
604
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
605
- /* @__PURE__ */ jsxs3(Label, { htmlFor: "ms-email", children: [
606
- "Email",
607
- /* @__PURE__ */ jsx3("span", { className: "text-ods-accent", children: "*" })
608
- ] }),
609
- /* @__PURE__ */ jsx3(
672
+ /* @__PURE__ */ jsx3(FieldWrapper, { label: "Email", htmlFor: "ms-email", error: errors.email?.message, children: /* @__PURE__ */ jsx3(
673
+ Input,
674
+ {
675
+ id: "ms-email",
676
+ type: "email",
677
+ required: true,
678
+ autoComplete: "email",
679
+ placeholder: "jane@company.com",
680
+ "aria-invalid": !!errors.email,
681
+ className: inputClass,
682
+ ...register("email")
683
+ }
684
+ ) }),
685
+ /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-[var(--spacing-system-m)]", children: [
686
+ /* @__PURE__ */ jsx3(FieldWrapper, { label: "First Name", htmlFor: "ms-first-name", error: errors.firstName?.message, children: /* @__PURE__ */ jsx3(
610
687
  Input,
611
688
  {
612
- id: "ms-email",
613
- type: "email",
614
- autoComplete: "email",
615
- placeholder: "jane@company.com",
616
- "aria-invalid": !!errors.email,
689
+ id: "ms-first-name",
690
+ required: true,
691
+ autoComplete: "given-name",
692
+ placeholder: "Jane",
693
+ "aria-invalid": !!errors.firstName,
617
694
  className: inputClass,
618
- ...register("email")
695
+ ...register("firstName")
619
696
  }
620
- ),
621
- errors.email && /* @__PURE__ */ jsx3("span", { className: "text-ods-error text-h6 mt-1", children: errors.email.message })
622
- ] }),
623
- supportedFields.map((field) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
624
- /* @__PURE__ */ jsxs3(Label, { htmlFor: `ms-q-${field.name}`, children: [
625
- field.label,
626
- field.required && /* @__PURE__ */ jsx3("span", { className: "text-ods-accent", children: "*" })
627
- ] }),
628
- field.type === "textarea" && /* @__PURE__ */ jsx3(
629
- Textarea,
630
- {
631
- id: `ms-q-${field.name}`,
632
- className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3",
633
- ...register(`formFields.${field.name}`)
634
- }
635
- ),
636
- field.type === "text" && /* @__PURE__ */ jsx3(
697
+ ) }),
698
+ /* @__PURE__ */ jsx3(FieldWrapper, { label: "Last Name", htmlFor: "ms-last-name", error: errors.lastName?.message, children: /* @__PURE__ */ jsx3(
637
699
  Input,
638
700
  {
639
- id: `ms-q-${field.name}`,
640
- className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12",
641
- ...register(`formFields.${field.name}`)
642
- }
643
- ),
644
- (field.type === "select" || field.type === "radio") && /* @__PURE__ */ jsx3(
645
- Controller,
646
- {
647
- control,
648
- name: `formFields.${field.name}`,
649
- render: ({ field: rhf }) => field.type === "select" ? /* @__PURE__ */ jsxs3(Select, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: [
650
- /* @__PURE__ */ jsx3(SelectTrigger, { id: `ms-q-${field.name}`, children: /* @__PURE__ */ jsx3(SelectValue, { placeholder: "Select\u2026" }) }),
651
- /* @__PURE__ */ jsx3(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx3(SelectItem, { value: opt, children: opt }, opt)) })
652
- ] }) : /* @__PURE__ */ jsx3(RadioGroup, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
653
- /* @__PURE__ */ jsx3(RadioGroupItem, { id: `ms-q-${field.name}-${opt}`, value: opt }),
654
- /* @__PURE__ */ jsx3(Label, { htmlFor: `ms-q-${field.name}-${opt}`, children: opt })
655
- ] }, opt)) })
701
+ id: "ms-last-name",
702
+ required: true,
703
+ autoComplete: "family-name",
704
+ placeholder: "Doe",
705
+ "aria-invalid": !!errors.lastName,
706
+ className: inputClass,
707
+ ...register("lastName")
656
708
  }
657
- ),
658
- field.type === "checkbox" && /* @__PURE__ */ jsx3(
659
- Controller,
660
- {
661
- control,
662
- name: `formFields.${field.name}`,
663
- render: ({ field: rhf }) => (
664
- // The question's Label above already carries the text and
665
- // binds to this id — a second inline label would double the
666
- // visible text AND the accessible name.
667
- /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: /* @__PURE__ */ jsx3(
668
- Checkbox,
669
- {
670
- id: `ms-q-${field.name}`,
671
- checked: Boolean(rhf.value),
672
- onCheckedChange: (v) => rhf.onChange(v === true)
673
- }
674
- ) })
709
+ ) })
710
+ ] }),
711
+ supportedFields.map((field) => /* @__PURE__ */ jsxs3(
712
+ FieldWrapper,
713
+ {
714
+ label: field.label,
715
+ htmlFor: `ms-q-${field.name}`,
716
+ error: fieldError(field.name),
717
+ children: [
718
+ field.type === "textarea" && /* @__PURE__ */ jsx3(
719
+ Textarea,
720
+ {
721
+ id: `ms-q-${field.name}`,
722
+ className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3",
723
+ ...register(`formFields.${field.name}`)
724
+ }
725
+ ),
726
+ field.type === "text" && /* @__PURE__ */ jsx3(
727
+ Input,
728
+ {
729
+ id: `ms-q-${field.name}`,
730
+ className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12",
731
+ ...register(`formFields.${field.name}`)
732
+ }
733
+ ),
734
+ (field.type === "select" || field.type === "radio") && /* @__PURE__ */ jsx3(
735
+ Controller,
736
+ {
737
+ control,
738
+ name: `formFields.${field.name}`,
739
+ render: ({ field: rhf }) => field.type === "select" ? /* @__PURE__ */ jsxs3(Select, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: [
740
+ /* @__PURE__ */ jsx3(SelectTrigger, { id: `ms-q-${field.name}`, children: /* @__PURE__ */ jsx3(SelectValue, { placeholder: "Select\u2026" }) }),
741
+ /* @__PURE__ */ jsx3(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx3(SelectItem, { value: opt, children: opt }, opt)) })
742
+ ] }) : /* @__PURE__ */ jsx3(RadioGroup, { value: rhf.value ?? "", onValueChange: rhf.onChange, children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
743
+ /* @__PURE__ */ jsx3(RadioGroupItem, { id: `ms-q-${field.name}-${opt}`, value: opt }),
744
+ /* @__PURE__ */ jsx3(Label, { htmlFor: `ms-q-${field.name}-${opt}`, children: opt })
745
+ ] }, opt)) })
746
+ }
747
+ ),
748
+ field.type === "checkbox" && /* @__PURE__ */ jsx3(
749
+ Controller,
750
+ {
751
+ control,
752
+ name: `formFields.${field.name}`,
753
+ render: ({ field: rhf }) => (
754
+ // The question's Label above already carries the text and
755
+ // binds to this id — a second inline label would double the
756
+ // visible text AND the accessible name.
757
+ /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: /* @__PURE__ */ jsx3(
758
+ Checkbox,
759
+ {
760
+ id: `ms-q-${field.name}`,
761
+ checked: Boolean(rhf.value),
762
+ onCheckedChange: (v) => rhf.onChange(v === true)
763
+ }
764
+ ) })
765
+ )
766
+ }
675
767
  )
676
- }
677
- ),
678
- fieldError(field.name) && /* @__PURE__ */ jsx3("span", { className: "text-ods-error text-h6 mt-1", children: fieldError(field.name) })
679
- ] }, field.name)),
680
- legalConsent && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-md)]", children: [
768
+ ]
769
+ },
770
+ field.name
771
+ )),
772
+ legalConsent && // Same out-of-flow message as the fields above, so a missed consent
773
+ // box doesn't shove the submit button down the card. With no error the
774
+ // wrapper is `display:contents` and the panel below is the flex item,
775
+ // exactly as it was.
776
+ /* @__PURE__ */ jsx3(FieldWrapper, { error: errors.legalConsentResponses?.message, children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-m)]", children: [
681
777
  /* @__PURE__ */ jsx3("p", { className: "text-h6 text-ods-text-secondary", children: legalConsent.processingConsentText }),
682
778
  legalConsent.communicationConsentText && /* @__PURE__ */ jsx3("p", { className: "text-h6 text-ods-text-secondary", children: legalConsent.communicationConsentText }),
683
779
  /* @__PURE__ */ jsx3(
@@ -685,7 +781,7 @@ function BookingForm({
685
781
  {
686
782
  control,
687
783
  name: "legalConsentResponses",
688
- render: ({ field: rhf }) => /* @__PURE__ */ jsx3(Fragment3, { children: legalConsent.communicationConsentCheckboxes.map((box) => {
784
+ render: ({ field: rhf }) => /* @__PURE__ */ jsx3(Fragment2, { children: legalConsent.communicationConsentCheckboxes.map((box) => {
689
785
  const responses = rhf.value ?? [];
690
786
  const current = responses.find((r) => r.communicationTypeId === box.communicationTypeId);
691
787
  return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-[var(--spacing-system-xs)]", children: [
@@ -701,18 +797,14 @@ function BookingForm({
701
797
  )
702
798
  }
703
799
  ),
704
- /* @__PURE__ */ jsxs3(Label, { htmlFor: `ms-consent-${box.communicationTypeId}`, children: [
705
- box.label,
706
- box.required ? " *" : ""
707
- ] })
800
+ /* @__PURE__ */ jsx3(Label, { htmlFor: `ms-consent-${box.communicationTypeId}`, children: box.label })
708
801
  ] }, box.communicationTypeId);
709
802
  }) })
710
803
  }
711
804
  ),
712
- legalConsent.privacyPolicyText && /* @__PURE__ */ jsx3("p", { className: "text-h6 text-ods-text-secondary", children: legalConsent.privacyPolicyText }),
713
- errors.legalConsentResponses && /* @__PURE__ */ jsx3("p", { className: "text-h6 text-ods-error", children: errors.legalConsentResponses.message })
714
- ] }),
715
- /* @__PURE__ */ jsx3("div", { className: "flex", children: /* @__PURE__ */ jsx3(Button, { type: "submit", loading: isSubmitting, disabled: isSubmitting, className: "w-full md:w-auto", children: "Confirm booking" }) })
805
+ legalConsent.privacyPolicyText && /* @__PURE__ */ jsx3("p", { className: "text-h6 text-ods-text-secondary", children: legalConsent.privacyPolicyText })
806
+ ] }) }),
807
+ /* @__PURE__ */ jsx3("div", { className: "flex", children: /* @__PURE__ */ jsx3(Button, { type: "submit", loading: isSubmitting, disabled: isSubmitting, children: "Confirm Booking" }) })
716
808
  ] });
717
809
  }
718
810
 
@@ -728,7 +820,7 @@ function Confirmation({ confirmation, timezone }) {
728
820
  minute: "2-digit",
729
821
  timeZoneName: "short"
730
822
  }).format(new Date(confirmation.startTimeMs));
731
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-md)] text-center", children: [
823
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center", children: [
732
824
  /* @__PURE__ */ jsx4("p", { className: "text-h1", role: "img", "aria-label": "Celebration", children: "\u{1F389}" }),
733
825
  /* @__PURE__ */ jsx4("h3", { className: "text-h3 text-ods-text-primary", children: "You're booked" }),
734
826
  /* @__PURE__ */ jsx4("p", { className: "text-h5 text-ods-text-primary", children: confirmation.title }),
@@ -743,13 +835,13 @@ function Confirmation({ confirmation, timezone }) {
743
835
 
744
836
  // src/components/meeting-scheduler/directory.tsx
745
837
  init_next_link();
746
- import { useEffect as useEffect2, useMemo as useMemo4, useState as useState2 } from "react";
838
+ import { useEffect, useMemo as useMemo4, useState as useState2 } from "react";
747
839
  import { ChevronRight as ChevronRight2 } from "lucide-react";
748
840
  init_cn();
749
841
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
750
842
  function useNextAvailableLabel(ms) {
751
843
  const [label, setLabel] = useState2(null);
752
- useEffect2(() => {
844
+ useEffect(() => {
753
845
  if (ms == null) return;
754
846
  try {
755
847
  setLabel(
@@ -829,7 +921,7 @@ function MeetingSchedulerDirectory({
829
921
  const [isLoading, setIsLoading] = useState2(initialData === null);
830
922
  const [error, setError] = useState2(null);
831
923
  const [page, setPage] = useState2(1);
832
- useEffect2(() => {
924
+ useEffect(() => {
833
925
  if (initialData !== null) return;
834
926
  const controller = new AbortController();
835
927
  let active = true;
@@ -907,6 +999,21 @@ function MeetingSchedulerDirectory({
907
999
 
908
1000
  // src/components/meeting-scheduler/index.tsx
909
1001
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1002
+ var CONTEXT_PANEL_CLASS = "p-[var(--spacing-system-l)] shrink-0 lg:w-[280px] border-b lg:border-b-0 lg:border-r border-ods-border lg:min-h-0 lg:overflow-y-auto";
1003
+ var ACTION_PANEL_CLASS = "flex-1 min-w-0 flex flex-col md:min-h-0 lg:p-[var(--spacing-system-l)]";
1004
+ var PANEL_STEP_CLASS = "p-[var(--spacing-system-l)] md:min-h-0 md:overflow-y-auto lg:p-0";
1005
+ var MEETING_SCHEDULER_H = "md:h-[34.375rem] lg:h-[23.75rem]";
1006
+ var CARD_CLASS = cn(
1007
+ "rounded-md border border-ods-border bg-ods-card overflow-hidden",
1008
+ "md:flex md:flex-col",
1009
+ MEETING_SCHEDULER_H
1010
+ );
1011
+ var CARD_INNER_CLASS = "flex flex-col md:min-h-0 md:flex-1 lg:h-full lg:flex-row";
1012
+ var CARD_DEGRADED_CLASS = cn(
1013
+ "rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-m)]",
1014
+ "md:justify-center md:overflow-y-auto",
1015
+ MEETING_SCHEDULER_H
1016
+ );
910
1017
  function isNativelyBookable(availability) {
911
1018
  if (!availability.formFields.every(isSupportedFormField)) return false;
912
1019
  const consent = availability.legalConsent;
@@ -925,12 +1032,14 @@ function HubSpotMeetingScheduler({
925
1032
  hosts,
926
1033
  displayTimezone,
927
1034
  fallbackUrl,
1035
+ onBack,
928
1036
  onBooked,
929
1037
  className
930
1038
  }) {
931
1039
  const {
932
1040
  availability,
933
1041
  isLoadingAvailability,
1042
+ isFetchingAvailability,
934
1043
  availabilityError,
935
1044
  monthOffset,
936
1045
  setMonthOffset,
@@ -939,7 +1048,7 @@ function HubSpotMeetingScheduler({
939
1048
  isSubmitting
940
1049
  } = useMeetingBooking({ meetingId, apiBaseUrl, initialAvailability });
941
1050
  const [timezone, setTimezone] = useState3(displayTimezone ?? null);
942
- useEffect3(() => {
1051
+ useEffect2(() => {
943
1052
  if (displayTimezone) return;
944
1053
  try {
945
1054
  setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
@@ -954,7 +1063,7 @@ function HubSpotMeetingScheduler({
954
1063
  const [confirmation, setConfirmation] = useState3(null);
955
1064
  const [bookingError, setBookingError] = useState3(null);
956
1065
  const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals();
957
- useEffect3(() => {
1066
+ useEffect2(() => {
958
1067
  setStep("slot");
959
1068
  setDurationMs(null);
960
1069
  setSelectedDay(null);
@@ -965,14 +1074,14 @@ function HubSpotMeetingScheduler({
965
1074
  }, [meetingId]);
966
1075
  const durations = availability?.durationsMs ?? [];
967
1076
  const shownHosts = hosts ?? availability?.hosts ?? [];
968
- useEffect3(() => {
1077
+ useEffect2(() => {
969
1078
  if (durationMs == null && durations.length > 0) setDurationMs(durations[0]);
970
1079
  }, [durationMs, durations]);
971
1080
  const slots = useMemo5(() => {
972
1081
  if (!availability || durationMs == null) return [];
973
1082
  return availability.slotsByDurationMs[String(durationMs)] ?? [];
974
1083
  }, [availability, durationMs]);
975
- useEffect3(() => {
1084
+ useEffect2(() => {
976
1085
  if (step !== "slot" || !timezone) return;
977
1086
  const now = /* @__PURE__ */ new Date();
978
1087
  const visible = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1);
@@ -987,13 +1096,17 @@ function HubSpotMeetingScheduler({
987
1096
  setSelectedDay(dayKeyInZone(inMonth[0], timezone));
988
1097
  }, [step, timezone, slots, selectedDay, monthOffset]);
989
1098
  const autoAdvanceCount = useRef2(0);
990
- useEffect3(() => {
991
- if (step === "slot" && availability && !isLoadingAvailability && durationMs != null && slots.length === 0 && availability.hasMore && autoAdvanceCount.current < 3) {
1099
+ useEffect2(() => {
1100
+ if (step === "slot" && availability && !isFetchingAvailability && durationMs != null && slots.length === 0 && availability.hasMore && autoAdvanceCount.current < 3) {
992
1101
  autoAdvanceCount.current += 1;
993
1102
  setMonthOffset(monthOffset + 1);
994
1103
  }
995
- }, [step, availability, isLoadingAvailability, durationMs, slots, monthOffset, setMonthOffset]);
996
- const handleSubmit = useCallback2(
1104
+ }, [step, availability, isFetchingAvailability, durationMs, slots, monthOffset, setMonthOffset]);
1105
+ const backToSlot = useCallback3(() => {
1106
+ setStep("slot");
1107
+ setBookingError(null);
1108
+ }, []);
1109
+ const handleSubmit = useCallback3(
997
1110
  async (payload) => {
998
1111
  setBookingError(null);
999
1112
  const result = await book(payload);
@@ -1016,22 +1129,16 @@ function HubSpotMeetingScheduler({
1016
1129
  );
1017
1130
  const escapeHatch = fallbackUrl ? /* @__PURE__ */ jsx6(Button, { variant: "outline", size: "small-legacy", href: fallbackUrl, openInNewTab: true, children: "Open in HubSpot" }) : null;
1018
1131
  if (isLoadingAvailability && !availability) {
1019
- return /* @__PURE__ */ jsx6("div", { className: cn("rounded-md border border-ods-border bg-ods-card overflow-hidden", className), children: /* @__PURE__ */ jsxs6("div", { className: "flex flex-col md:flex-row", children: [
1020
- /* @__PURE__ */ jsx6(ContextPanelSkeleton, { className: "p-[var(--spacing-system-lf)] md:w-80 md:shrink-0 border-b md:border-b-0 md:border-r border-ods-border" }),
1021
- /* @__PURE__ */ jsxs6("div", { className: "flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col gap-[var(--spacing-system-md)]", children: [
1022
- /* @__PURE__ */ jsx6("p", { className: "text-h5 text-ods-text-secondary", children: "Select a date & time" }),
1023
- /* @__PURE__ */ jsx6(SlotPickerSkeleton, {})
1024
- ] })
1132
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1133
+ /* @__PURE__ */ jsx6(ContextPanelSkeleton, { onBack, className: CONTEXT_PANEL_CLASS }),
1134
+ /* @__PURE__ */ jsx6("div", { className: ACTION_PANEL_CLASS, children: /* @__PURE__ */ jsx6(SlotPickerSkeleton, { monthOffset }) })
1025
1135
  ] }) });
1026
1136
  }
1027
1137
  if (availabilityError || !availability) {
1028
1138
  return /* @__PURE__ */ jsxs6(
1029
1139
  "div",
1030
1140
  {
1031
- className: cn(
1032
- "rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]",
1033
- className
1034
- ),
1141
+ className: cn(CARD_DEGRADED_CLASS, className),
1035
1142
  children: [
1036
1143
  /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-text-secondary", children: "We couldn't load available call times. Please try again shortly." }),
1037
1144
  escapeHatch
@@ -1043,10 +1150,7 @@ function HubSpotMeetingScheduler({
1043
1150
  return /* @__PURE__ */ jsxs6(
1044
1151
  "div",
1045
1152
  {
1046
- className: cn(
1047
- "rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]",
1048
- className
1049
- ),
1153
+ className: cn(CARD_DEGRADED_CLASS, className),
1050
1154
  children: [
1051
1155
  /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-text-secondary", children: "This meeting type is booked directly on HubSpot." }),
1052
1156
  escapeHatch
@@ -1054,7 +1158,7 @@ function HubSpotMeetingScheduler({
1054
1158
  }
1055
1159
  );
1056
1160
  }
1057
- return /* @__PURE__ */ jsx6("div", { className: cn("rounded-md border border-ods-border bg-ods-card overflow-hidden", className), children: /* @__PURE__ */ jsxs6("div", { className: "flex flex-col md:flex-row", children: [
1161
+ return /* @__PURE__ */ jsx6("div", { className: cn(CARD_CLASS, className), children: /* @__PURE__ */ jsxs6("div", { className: CARD_INNER_CLASS, children: [
1058
1162
  /* @__PURE__ */ jsx6(
1059
1163
  SchedulerContextPanel,
1060
1164
  {
@@ -1071,92 +1175,92 @@ function HubSpotMeetingScheduler({
1071
1175
  },
1072
1176
  timezone,
1073
1177
  onTimezoneChange: setTimezone,
1178
+ onBack: step === "details" ? backToSlot : onBack,
1074
1179
  locked: step !== "slot",
1075
- className: "p-[var(--spacing-system-lf)] md:w-80 md:shrink-0 border-b md:border-b-0 md:border-r border-ods-border"
1180
+ showTimezone: step !== "confirmed",
1181
+ className: CONTEXT_PANEL_CLASS
1076
1182
  }
1077
1183
  ),
1078
- /* @__PURE__ */ jsx6("div", { className: "flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col", children: step === "confirmed" && confirmation && timezone ? /* @__PURE__ */ jsx6(Confirmation, { confirmation, timezone }) : step === "details" && durationMs != null && selectedSlot != null && timezone ? /* @__PURE__ */ jsxs6("div", { className: "flex flex-1 flex-col gap-[var(--spacing-system-md)]", children: [
1184
+ /* @__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: [
1185
+ /* @__PURE__ */ jsxs6("p", { className: "text-h4 text-ods-text-primary", children: [
1186
+ new Intl.DateTimeFormat(void 0, {
1187
+ timeZone: timezone,
1188
+ weekday: "long",
1189
+ month: "long",
1190
+ day: "numeric",
1191
+ hour: "numeric",
1192
+ minute: "2-digit",
1193
+ timeZoneName: "short"
1194
+ }).format(new Date(selectedSlot)),
1195
+ " ",
1196
+ "\xB7 ",
1197
+ formatDurationCompact(durationMs / 1e3)
1198
+ ] }),
1199
+ bookingError && bookingError !== "SLOT_TAKEN" && /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-start gap-[var(--spacing-system-xs)]", children: [
1200
+ /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-error", children: bookingError === "TEMPORARILY_UNAVAILABLE" ? "Scheduling is briefly unavailable \u2014 please try again in a minute." : bookingError === "MEETING_UNAVAILABLE" ? "This meeting type has reached its booking limit for today \u2014 try another time or contact us." : bookingError === "LINK_GONE" ? "This meeting type is no longer available." : "Please double-check your details and try again." }),
1201
+ escapeHatch
1202
+ ] }),
1079
1203
  /* @__PURE__ */ jsx6(
1080
- BackButton,
1204
+ BookingForm,
1081
1205
  {
1082
- label: "Back",
1083
- onClick: () => {
1084
- setStep("slot");
1085
- setBookingError(null);
1086
- }
1206
+ availability,
1207
+ meetingId,
1208
+ startTimeMs: selectedSlot,
1209
+ durationMs,
1210
+ timezone,
1211
+ isSubmitting,
1212
+ onSubmit: handleSubmit,
1213
+ honeypotInputProps,
1214
+ getSignals
1087
1215
  }
1088
- ),
1089
- /* @__PURE__ */ jsxs6("div", { className: "flex flex-1 flex-col justify-center gap-[var(--spacing-system-md)]", children: [
1090
- /* @__PURE__ */ jsxs6("p", { className: "text-h4 text-ods-text-primary", children: [
1091
- new Intl.DateTimeFormat(void 0, {
1092
- timeZone: timezone,
1093
- weekday: "long",
1094
- month: "long",
1095
- day: "numeric",
1096
- hour: "numeric",
1097
- minute: "2-digit",
1098
- timeZoneName: "short"
1099
- }).format(new Date(selectedSlot)),
1100
- " ",
1101
- "\xB7 ",
1102
- formatDurationCompact(durationMs / 1e3)
1103
- ] }),
1104
- bookingError && bookingError !== "SLOT_TAKEN" && /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-start gap-[var(--spacing-system-xs)]", children: [
1105
- /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-error", children: bookingError === "TEMPORARILY_UNAVAILABLE" ? "Scheduling is briefly unavailable \u2014 please try again in a minute." : bookingError === "MEETING_UNAVAILABLE" ? "This meeting type has reached its booking limit for today \u2014 try another time or contact us." : bookingError === "LINK_GONE" ? "This meeting type is no longer available." : "Please double-check your details and try again." }),
1106
- escapeHatch
1107
- ] }),
1216
+ )
1217
+ ] }) : /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-[var(--spacing-system-m)] md:min-h-0 md:flex-1", children: [
1218
+ bookingError === "SLOT_TAKEN" && /* @__PURE__ */ jsx6("p", { className: "px-[var(--spacing-system-l)] pt-[var(--spacing-system-l)] text-h6 text-ods-error lg:p-0", children: "That time is no longer available \u2014 if you just submitted, check your email for a confirmation before rebooking." }),
1219
+ timezone && durationMs != null ? (
1220
+ // The picker owns BOTH the loading and the empty month now —
1221
+ // it keeps the calendar up either way, so there is no branch
1222
+ // here that can take it off screen mid-flow.
1108
1223
  /* @__PURE__ */ jsx6(
1109
- BookingForm,
1224
+ SlotPicker,
1110
1225
  {
1111
- availability,
1112
- meetingId,
1113
- startTimeMs: selectedSlot,
1114
- durationMs,
1226
+ slots,
1115
1227
  timezone,
1116
- isSubmitting,
1117
- onSubmit: handleSubmit,
1118
- honeypotInputProps,
1119
- getSignals
1228
+ monthOffset,
1229
+ onMonthOffsetChange: (o) => {
1230
+ setSelectedDay(null);
1231
+ setSelectedSlot(null);
1232
+ setMonthOffset(o);
1233
+ },
1234
+ selectedSlot,
1235
+ onSelectSlot: (ms) => {
1236
+ setSelectedSlot(ms);
1237
+ setStep("details");
1238
+ },
1239
+ selectedDay,
1240
+ onSelectDay: setSelectedDay,
1241
+ isLoading: isLoadingAvailability
1120
1242
  }
1121
1243
  )
1122
- ] })
1123
- ] }) : /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-[var(--spacing-system-md)]", children: [
1124
- /* @__PURE__ */ jsx6("p", { className: "text-h5 text-ods-text-secondary", children: "Select a date & time" }),
1125
- bookingError === "SLOT_TAKEN" && /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-error", children: "That time is no longer available \u2014 if you just submitted, check your email for a confirmation before rebooking." }),
1126
- timezone && durationMs != null ? slots.length > 0 || isLoadingAvailability ? /* @__PURE__ */ jsx6(
1127
- SlotPicker,
1128
- {
1129
- slots,
1130
- timezone,
1131
- monthOffset,
1132
- onMonthOffsetChange: (o) => {
1133
- setSelectedDay(null);
1134
- setSelectedSlot(null);
1135
- setMonthOffset(o);
1136
- },
1137
- selectedSlot,
1138
- onSelectSlot: (ms) => {
1139
- setSelectedSlot(ms);
1140
- setStep("details");
1141
- },
1142
- selectedDay,
1143
- onSelectDay: setSelectedDay,
1144
- isLoading: isLoadingAvailability
1145
- }
1146
- ) : /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-start gap-[var(--spacing-system-md)]", children: [
1147
- /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-text-secondary", children: isLoadingAvailability ? "Checking more dates\u2026" : "No call times are published right now." }),
1148
- !isLoadingAvailability && escapeHatch
1149
- ] }) : (
1244
+ ) : durations.length === 0 && !isFetchingAvailability ? (
1245
+ // The LINK publishes nothing at all a different thing from a
1246
+ // fully-booked month, and the only case the escape hatch is
1247
+ // the right answer to.
1248
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-start gap-[var(--spacing-system-m)]", children: [
1249
+ /* @__PURE__ */ jsx6("p", { className: "text-h6 text-ods-text-secondary", children: "No call times are published right now." }),
1250
+ escapeHatch
1251
+ ] })
1252
+ ) : (
1150
1253
  // Pre-mount timezone-resolution window (every SSR load hits
1151
- // this for one frame-batch) — SAME per-region skeletons, no
1152
- // bare rectangle, no shift when the real picker swaps in.
1153
- /* @__PURE__ */ jsx6(SlotPickerSkeleton, {})
1254
+ // this for one frame-batch) — same layout, real calendar, so
1255
+ // nothing shifts when the picker takes over.
1256
+ /* @__PURE__ */ jsx6(SlotPickerSkeleton, { monthOffset })
1154
1257
  )
1155
1258
  ] }) })
1156
1259
  ] }) });
1157
1260
  }
1158
1261
  export {
1159
1262
  HubSpotMeetingScheduler,
1263
+ MEETING_SCHEDULER_H,
1160
1264
  MeetingSchedulerDirectory,
1161
1265
  MeetingSchedulerDirectoryRowSkeleton
1162
1266
  };