@flamingo-stack/openframe-frontend-core 0.0.550-1809.4793.1 → 0.0.550
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-4ARE6T4J.js → chunk-2I6KJN7F.js} +4 -4
- package/dist/{chunk-72SQSKU7.cjs → chunk-4A5RDQ6L.cjs} +61 -61
- package/dist/{chunk-72SQSKU7.cjs.map → chunk-4A5RDQ6L.cjs.map} +1 -1
- package/dist/{chunk-SZMZJ5Z7.js → chunk-5KAQ2JUQ.js} +2 -2
- package/dist/{chunk-TKAWA4FY.js → chunk-5VGIDZAM.js} +2 -2
- package/dist/{chunk-TV6GBR7P.cjs → chunk-6M6EZK4R.cjs} +31 -31
- package/dist/{chunk-TV6GBR7P.cjs.map → chunk-6M6EZK4R.cjs.map} +1 -1
- package/dist/{chunk-KAM2YLU4.js → chunk-6RTSYEBE.js} +2 -2
- package/dist/{chunk-SRNY3MXC.js → chunk-7T2HI44L.js} +2 -2
- package/dist/{chunk-GN2Q3WBA.js → chunk-7YDLMRVR.js} +6 -6
- package/dist/{chunk-WBGSHUXJ.cjs → chunk-A4L6BEB7.cjs} +7 -7
- package/dist/{chunk-WBGSHUXJ.cjs.map → chunk-A4L6BEB7.cjs.map} +1 -1
- package/dist/{chunk-QVW4B4YQ.cjs → chunk-CN2VQPLX.cjs} +4 -4
- package/dist/{chunk-QVW4B4YQ.cjs.map → chunk-CN2VQPLX.cjs.map} +1 -1
- package/dist/{chunk-KGMOXLYO.cjs → chunk-DCB4EBA5.cjs} +70 -28
- package/dist/chunk-DCB4EBA5.cjs.map +1 -0
- package/dist/{chunk-NRRY2RZR.cjs → chunk-EVCAPL5A.cjs} +39 -39
- package/dist/{chunk-NRRY2RZR.cjs.map → chunk-EVCAPL5A.cjs.map} +1 -1
- package/dist/{chunk-I4N3VQCU.cjs → chunk-GXNSFW3N.cjs} +30 -30
- package/dist/{chunk-I4N3VQCU.cjs.map → chunk-GXNSFW3N.cjs.map} +1 -1
- package/dist/{chunk-Q4NLR45P.cjs → chunk-H7ORFGB6.cjs} +11 -11
- package/dist/{chunk-Q4NLR45P.cjs.map → chunk-H7ORFGB6.cjs.map} +1 -1
- package/dist/{chunk-HT7P4QSO.cjs → chunk-HCGS2AAS.cjs} +14 -14
- package/dist/{chunk-HT7P4QSO.cjs.map → chunk-HCGS2AAS.cjs.map} +1 -1
- package/dist/{chunk-4WE5VFJX.js → chunk-HWYUSB5A.js} +2 -2
- package/dist/{chunk-44ERGZJT.js → chunk-IRV6TOEZ.js} +5 -5
- package/dist/{chunk-LALK7AGK.cjs → chunk-ISCWUJ72.cjs} +3 -3
- package/dist/{chunk-LALK7AGK.cjs.map → chunk-ISCWUJ72.cjs.map} +1 -1
- package/dist/{chunk-GAIU563C.cjs → chunk-ISLQTWWW.cjs} +9 -9
- package/dist/{chunk-GAIU563C.cjs.map → chunk-ISLQTWWW.cjs.map} +1 -1
- package/dist/{chunk-XQU5QBMJ.js → chunk-JS7FDV27.js} +3515 -3561
- package/dist/chunk-JS7FDV27.js.map +1 -0
- package/dist/{chunk-DDECEP24.cjs → chunk-LDS54O26.cjs} +505 -551
- package/dist/chunk-LDS54O26.cjs.map +1 -0
- package/dist/{chunk-2RTORV23.cjs → chunk-LLBU7RI5.cjs} +27 -27
- package/dist/{chunk-2RTORV23.cjs.map → chunk-LLBU7RI5.cjs.map} +1 -1
- package/dist/{chunk-CILHIWXK.js → chunk-M3GOE7OF.js} +2 -2
- package/dist/{chunk-5HTVPIQG.cjs → chunk-MGRCFN2K.cjs} +5 -5
- package/dist/{chunk-5HTVPIQG.cjs.map → chunk-MGRCFN2K.cjs.map} +1 -1
- package/dist/{chunk-WBWGQOAH.js → chunk-R7RCGLII.js} +7 -7
- package/dist/{chunk-QX5AXGLP.js → chunk-RSZXB6J2.js} +108 -66
- package/dist/chunk-RSZXB6J2.js.map +1 -0
- package/dist/{chunk-NGYZMQHP.js → chunk-U7UPMD3Z.js} +4 -4
- package/dist/{chunk-CUZTTZ5T.cjs → chunk-UYQFUEIK.cjs} +98 -98
- package/dist/{chunk-CUZTTZ5T.cjs.map → chunk-UYQFUEIK.cjs.map} +1 -1
- package/dist/{chunk-VYX4X7ND.js → chunk-WTCXDYK7.js} +2 -2
- package/dist/{chunk-6X3C2YAR.js → chunk-XO4YU5GN.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/command.d.ts +3 -3
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +141 -141
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +60 -60
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/resizable.d.ts +1 -1
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/alert.d.ts +1 -1
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/checkbox-with-description.d.ts.map +1 -1
- package/dist/components/ui/field-wrapper.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +36 -36
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +3 -3
- package/dist/components/ui/label.d.ts +1 -1
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/status-badge.d.ts +1 -1
- package/dist/components/ui/tags-input.d.ts.map +1 -1
- package/dist/hooks/use-toast.d.ts +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/src/components/features/array-entry-manager.tsx +1 -1
- package/src/components/features/changelog-manager.tsx +3 -3
- package/src/components/features/highlight-video-section.tsx +1 -1
- package/src/components/features/og-editor-preview.tsx +4 -4
- package/src/components/features/release-media-manager.tsx +1 -1
- package/src/components/features/seo-editor-preview.tsx +1 -1
- package/src/components/ui/checkbox-with-description.tsx +0 -1
- package/src/components/ui/field-wrapper.tsx +3 -2
- package/src/components/ui/field.tsx +1 -1
- package/src/components/ui/label.tsx +31 -15
- package/src/components/ui/tags-input.tsx +3 -2
- package/dist/chunk-DDECEP24.cjs.map +0 -1
- package/dist/chunk-KGMOXLYO.cjs.map +0 -1
- package/dist/chunk-QX5AXGLP.js.map +0 -1
- package/dist/chunk-XQU5QBMJ.js.map +0 -1
- /package/dist/{chunk-4ARE6T4J.js.map → chunk-2I6KJN7F.js.map} +0 -0
- /package/dist/{chunk-SZMZJ5Z7.js.map → chunk-5KAQ2JUQ.js.map} +0 -0
- /package/dist/{chunk-TKAWA4FY.js.map → chunk-5VGIDZAM.js.map} +0 -0
- /package/dist/{chunk-KAM2YLU4.js.map → chunk-6RTSYEBE.js.map} +0 -0
- /package/dist/{chunk-SRNY3MXC.js.map → chunk-7T2HI44L.js.map} +0 -0
- /package/dist/{chunk-GN2Q3WBA.js.map → chunk-7YDLMRVR.js.map} +0 -0
- /package/dist/{chunk-4WE5VFJX.js.map → chunk-HWYUSB5A.js.map} +0 -0
- /package/dist/{chunk-44ERGZJT.js.map → chunk-IRV6TOEZ.js.map} +0 -0
- /package/dist/{chunk-CILHIWXK.js.map → chunk-M3GOE7OF.js.map} +0 -0
- /package/dist/{chunk-WBWGQOAH.js.map → chunk-R7RCGLII.js.map} +0 -0
- /package/dist/{chunk-NGYZMQHP.js.map → chunk-U7UPMD3Z.js.map} +0 -0
- /package/dist/{chunk-VYX4X7ND.js.map → chunk-WTCXDYK7.js.map} +0 -0
- /package/dist/{chunk-6X3C2YAR.js.map → chunk-XO4YU5GN.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/meeting-scheduler/index.cjs","../../../src/components/meeting-scheduler/index.tsx","../../../src/hooks/use-meeting-booking.ts","../../../src/schemas/meeting-booking-schema.ts","../../../src/components/meeting-scheduler/context-panel.tsx","../../../src/components/meeting-scheduler/slot-picker.tsx","../../../src/components/meeting-scheduler/booking-form.tsx","../../../src/components/meeting-scheduler/confirmation.tsx","../../../src/components/meeting-scheduler/directory.tsx"],"names":["jsx","Fragment","jsxs","ChevronRight","useState","useCallback"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/meeting-scheduler/index.cjs","sourcesContent":[null,"'use client'\n\n/**\n * `<HubSpotMeetingScheduler />` — natively-branded booking flow over the\n * HubSpot Meetings scheduler, rendered entirely with ODS primitives (no\n * iframe, no third-party chrome, no scripts). Data flows exclusively through\n * a HOST PROXY (`GET {apiBaseUrl}/api/meetings/availability`,\n * `POST {apiBaseUrl}/api/meetings/book`) — the widget has zero HubSpot\n * knowledge and no secrets; see `docs/EMBEDDING_HUBSPOT_MEETINGS.md` for the\n * endpoint contract a host must serve (external embedders proxy through\n * their own backend — the book route is IP-rate-limited and bot-gated).\n *\n * LAYOUT (Calendly-anatomy, conversion-first): one bordered card, two\n * panels. Left/top — the CONTEXT panel: host identity (avatar + name +\n * title from `availability.hosts`), meeting title/description, duration\n * chips (the duration choice lives here, NOT as a wizard step — the visitor\n * always lands straight on the calendar), and the resolved timezone.\n * Right/bottom — the ACTION panel: calendar + time slots (first available\n * day auto-selected so slots are visible immediately), then the details\n * form, then the confirmation. Panels stack on mobile.\n *\n * Two modes, one component:\n * - SSR mode: pass `initialAvailability` (host-fetched) — the\n * zone-independent shell (context panel, calendar) renders server-side;\n * slot TIME labels wait for the client zone resolution (deterministic\n * first paint, no hydration mismatch), and availability is refetched\n * unconditionally after mount (the seed is scaffolding for a 60s-volatile\n * resource, not truth).\n * - Client mode: omit the seed — skeleton + self-fetch on mount.\n *\n * State machine: slot → details → confirmed, with a back edge, a \"book\n * another\" reset, and a `submitting` lock as the double-booking guard.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../../utils/cn'\nimport { formatDurationCompact } from '../../utils/format'\nimport { Button } from '../ui'\nimport { BackButton } from '../layout/back-button'\nimport { useHumanitySignals } from '../../hooks/use-humanity-signals'\nimport { useMeetingBooking } from '../../hooks/use-meeting-booking'\nimport {\n isSupportedFormField,\n type BookingConfirmation,\n type MeetingAvailability,\n type MeetingBookingErrorCode,\n type MeetingHost,\n} from '../../schemas/meeting-booking-schema'\nimport { SchedulerContextPanel, ContextPanelSkeleton } from './context-panel'\nimport { SlotPicker, SlotPickerSkeleton, dayKeyInZone } from './slot-picker'\nimport { BookingForm } from './booking-form'\nimport { Confirmation } from './confirmation'\n\nexport interface HubSpotMeetingSchedulerProps {\n /** Directory id of the meeting link (from the host's `/api/meetings` payload). */\n meetingId: string\n /** Endpoints prefix, default '' (same-origin `/api/meetings/*`) — FaqSection precedent. */\n apiBaseUrl?: string\n /** SSR-mode seed (host-fetched). Omitted → client mode (self-fetch on mount). */\n initialAvailability?: MeetingAvailability\n /** Meeting title shown in the context panel (host page owns the h1). */\n title?: string\n /** Short description under the title in the context panel. */\n description?: string | null\n /** Override the hosts shown (defaults to `availability.hosts`). */\n hosts?: MeetingHost[]\n /** Pin the DISPLAY zone (rendering only — never sent upstream, never a cache key). */\n displayTimezone?: string\n /** The link's public HubSpot booking URL — the \"Open in HubSpot\" escape hatch target. */\n fallbackUrl?: string\n onBooked?: (b: BookingConfirmation) => void\n className?: string\n}\n\ntype Step = 'slot' | 'details' | 'confirmed'\n\n/**\n * Fail-closed gate: a link whose declared questions include an unsupported\n * type, or whose consent block is malformed, must NOT render a half-working\n * native form (a silently dropped required question or missing consent copy\n * is worse than no native form) — the escape hatch takes over.\n */\nfunction isNativelyBookable(availability: MeetingAvailability): boolean {\n if (!availability.formFields.every(isSupportedFormField)) return false\n const consent = availability.legalConsent\n if (consent) {\n if (typeof consent.processingConsentText !== 'string') return false\n if (!Array.isArray(consent.communicationConsentCheckboxes)) return false\n }\n return true\n}\n\nexport function HubSpotMeetingScheduler({\n meetingId,\n apiBaseUrl = '',\n initialAvailability,\n title,\n description,\n hosts,\n displayTimezone,\n fallbackUrl,\n onBooked,\n className,\n}: HubSpotMeetingSchedulerProps) {\n const {\n availability,\n isLoadingAvailability,\n availabilityError,\n monthOffset,\n setMonthOffset,\n refetchAvailability,\n book,\n isSubmitting,\n } = useMeetingBooking({ meetingId, apiBaseUrl, initialAvailability })\n\n // Zone resolution happens POST-mount (Intl) unless the embedder pins one —\n // the initial (server) render stays deterministic; time labels appear once\n // a zone exists.\n const [timezone, setTimezone] = useState<string | null>(displayTimezone ?? null)\n useEffect(() => {\n if (displayTimezone) return\n try {\n setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC')\n } catch {\n setTimezone('UTC')\n }\n }, [displayTimezone])\n\n const [step, setStep] = useState<Step>('slot')\n const [durationMs, setDurationMs] = useState<number | null>(null)\n const [selectedDay, setSelectedDay] = useState<string | null>(null)\n const [selectedSlot, setSelectedSlot] = useState<number | null>(null)\n const [confirmation, setConfirmation] = useState<BookingConfirmation | null>(null)\n const [bookingError, setBookingError] = useState<MeetingBookingErrorCode | null>(null)\n\n const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()\n\n // Reset the machine when the host switches links.\n useEffect(() => {\n setStep('slot')\n setDurationMs(null)\n setSelectedDay(null)\n setSelectedSlot(null)\n setConfirmation(null)\n setBookingError(null)\n autoAdvanceCount.current = 0\n }, [meetingId])\n\n const durations = availability?.durationsMs ?? []\n const shownHosts = hosts ?? availability?.hosts ?? []\n\n // Default to the first offered duration — the visitor lands straight on\n // the calendar; duration is a context-panel chip, not a wizard step.\n useEffect(() => {\n if (durationMs == null && durations.length > 0) setDurationMs(durations[0])\n }, [durationMs, durations])\n\n const slots = useMemo(() => {\n if (!availability || durationMs == null) return []\n return availability.slotsByDurationMs[String(durationMs)] ?? []\n }, [availability, durationMs])\n\n // Auto-select the first day WITH SLOTS IN THE VISIBLE MONTH (once the zone\n // is known) so the visitor sees concrete times immediately — never a dead\n // \"pick a day\" state, and NEVER a day from another month than the calendar\n // shows (HubSpot's monthOffset payloads can carry near-term slots outside\n // the requested month — the visible grid is the authority).\n useEffect(() => {\n if (step !== 'slot' || !timezone) return\n const now = new Date()\n const visible = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1)\n const monthPrefix = `${visible.getFullYear()}-${String(visible.getMonth() + 1).padStart(2, '0')}`\n const inMonth = slots.filter((ms) => dayKeyInZone(ms, timezone).startsWith(monthPrefix))\n if (inMonth.length === 0) {\n if (selectedDay && !selectedDay.startsWith(monthPrefix)) setSelectedDay(null)\n return\n }\n const dayKeys = new Set(inMonth.map((ms) => dayKeyInZone(ms, timezone)))\n if (selectedDay && dayKeys.has(selectedDay)) return\n setSelectedDay(dayKeyInZone(inMonth[0], timezone))\n }, [step, timezone, slots, selectedDay, monthOffset])\n\n // Fully-booked month with more ahead → auto-advance (bounded, ≤3 hops)\n // before showing the empty state.\n const autoAdvanceCount = useRef(0)\n useEffect(() => {\n if (\n step === 'slot' &&\n availability &&\n !isLoadingAvailability &&\n durationMs != null &&\n slots.length === 0 &&\n availability.hasMore &&\n autoAdvanceCount.current < 3\n ) {\n autoAdvanceCount.current += 1\n setMonthOffset(monthOffset + 1)\n }\n }, [step, availability, isLoadingAvailability, durationMs, slots, monthOffset, setMonthOffset])\n\n const handleSubmit = useCallback(\n async (payload: Record<string, unknown>) => {\n setBookingError(null)\n const result = await book(payload)\n if (result.ok && result.confirmation) {\n setConfirmation(result.confirmation)\n setStep('confirmed')\n onBooked?.(result.confirmation)\n return\n }\n const code = result.code ?? 'TEMPORARILY_UNAVAILABLE'\n setBookingError(code)\n if (code === 'SLOT_TAKEN') {\n // Recover: refresh the grid (the slot vanishes) and reset the timing\n // signal so the retry isn't flagged too-fast.\n setSelectedSlot(null)\n setStep('slot')\n resetSignals()\n void refetchAvailability()\n }\n },\n [book, onBooked, refetchAvailability, resetSignals],\n )\n\n const escapeHatch = fallbackUrl ? (\n <Button variant=\"outline\" size=\"small-legacy\" href={fallbackUrl} openInNewTab>\n Open in HubSpot\n </Button>\n ) : null\n\n // ---- terminal / degraded states -----------------------------------------\n\n if (isLoadingAvailability && !availability) {\n // Per-region skeletons, composed inside the SAME card wrappers as the\n // loaded state: `ContextPanelSkeleton` (profile side), `CalendarSkeleton`\n // (day buttons), `TimeChipsSkeleton` (hour options). Identical footprints\n // by construction — loading ⇄ loaded never shifts layout.\n return (\n <div className={cn('rounded-md border border-ods-border bg-ods-card overflow-hidden', className)}>\n <div className=\"flex flex-col md:flex-row\">\n <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\" />\n <div className=\"flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col gap-[var(--spacing-system-md)]\">\n {/* Static heading renders REAL — it needs no data. */}\n <p className=\"text-h5 text-ods-text-secondary\">Select a date & time</p>\n <SlotPickerSkeleton />\n </div>\n </div>\n </div>\n )\n }\n\n if (availabilityError || !availability) {\n return (\n <div\n className={cn(\n 'rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]',\n className,\n )}\n >\n <p className=\"text-h6 text-ods-text-secondary\">\n We couldn't load available call times. Please try again shortly.\n </p>\n {escapeHatch}\n </div>\n )\n }\n\n if (!isNativelyBookable(availability)) {\n // Fail closed — never render a half-working native form on a link with\n // questions or consent we can't faithfully reproduce.\n return (\n <div\n className={cn(\n 'rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]',\n className,\n )}\n >\n <p className=\"text-h6 text-ods-text-secondary\">This meeting type is booked directly on HubSpot.</p>\n {escapeHatch}\n </div>\n )\n }\n\n // ---- the card ------------------------------------------------------------\n\n return (\n <div className={cn('rounded-md border border-ods-border bg-ods-card overflow-hidden', className)}>\n <div className=\"flex flex-col md:flex-row\">\n <SchedulerContextPanel\n hosts={shownHosts}\n title={title}\n description={description}\n durationsMs={durations}\n selectedDurationMs={durationMs}\n onSelectDuration={(ms) => {\n setDurationMs(ms)\n setSelectedDay(null)\n setSelectedSlot(null)\n if (step === 'details') setStep('slot')\n }}\n timezone={timezone}\n onTimezoneChange={setTimezone}\n locked={step !== 'slot'}\n className=\"p-[var(--spacing-system-lf)] md:w-80 md:shrink-0 border-b md:border-b-0 md:border-r border-ods-border\"\n />\n\n {/* min-height pins the card so slot ⇄ details ⇄ confirmed transitions\n never jump the page (sized to the slot step, the tallest one);\n flex column so shorter steps can center vertically. */}\n <div className=\"flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col\">\n {step === 'confirmed' && confirmation && timezone ? (\n <Confirmation confirmation={confirmation} timezone={timezone} />\n ) : step === 'details' && durationMs != null && selectedSlot != null && timezone ? (\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-system-md)]\">\n {/* App-standard BackButton — pinned at the TOP of the panel,\n exactly where the slot step's \"Select a date & time\" header\n sits; the form centers in the remaining space. */}\n <BackButton\n label=\"Back\"\n onClick={() => {\n setStep('slot')\n setBookingError(null)\n }}\n />\n <div className=\"flex flex-1 flex-col justify-center gap-[var(--spacing-system-md)]\">\n <p className=\"text-h4 text-ods-text-primary\">\n {new Intl.DateTimeFormat(undefined, {\n timeZone: timezone,\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n }).format(new Date(selectedSlot))}{' '}\n · {formatDurationCompact(durationMs / 1000)}\n </p>\n {bookingError && bookingError !== 'SLOT_TAKEN' && (\n <div className=\"flex flex-col items-start gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h6 text-ods-error\">\n {bookingError === 'TEMPORARILY_UNAVAILABLE'\n ? 'Scheduling is briefly unavailable — please try again in a minute.'\n : bookingError === 'MEETING_UNAVAILABLE'\n ? 'This meeting type has reached its booking limit for today — try another time or contact us.'\n : bookingError === 'LINK_GONE'\n ? 'This meeting type is no longer available.'\n : 'Please double-check your details and try again.'}\n </p>\n {escapeHatch}\n </div>\n )}\n <BookingForm\n availability={availability}\n meetingId={meetingId}\n startTimeMs={selectedSlot}\n durationMs={durationMs}\n timezone={timezone}\n isSubmitting={isSubmitting}\n onSubmit={handleSubmit}\n honeypotInputProps={honeypotInputProps}\n getSignals={getSignals}\n />\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-[var(--spacing-system-md)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Select a date & time</p>\n {bookingError === 'SLOT_TAKEN' && (\n <p className=\"text-h6 text-ods-error\">\n That time is no longer available — if you just submitted, check your email for a confirmation\n before rebooking.\n </p>\n )}\n {timezone && durationMs != null ? (\n slots.length > 0 || isLoadingAvailability ? (\n <SlotPicker\n slots={slots}\n timezone={timezone}\n monthOffset={monthOffset}\n onMonthOffsetChange={(o) => {\n setSelectedDay(null)\n setSelectedSlot(null)\n setMonthOffset(o)\n }}\n selectedSlot={selectedSlot}\n onSelectSlot={(ms) => {\n setSelectedSlot(ms)\n setStep('details')\n }}\n selectedDay={selectedDay}\n onSelectDay={setSelectedDay}\n isLoading={isLoadingAvailability}\n />\n ) : (\n <div className=\"flex flex-col items-start gap-[var(--spacing-system-md)]\">\n <p className=\"text-h6 text-ods-text-secondary\">\n {isLoadingAvailability ? 'Checking more dates…' : 'No call times are published right now.'}\n </p>\n {!isLoadingAvailability && escapeHatch}\n </div>\n )\n ) : (\n // Pre-mount timezone-resolution window (every SSR load hits\n // this for one frame-batch) — SAME per-region skeletons, no\n // bare rectangle, no shift when the real picker swaps in.\n <SlotPickerSkeleton />\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n}\n\nexport {\n MeetingSchedulerDirectory,\n MeetingSchedulerDirectoryRowSkeleton,\n type MeetingSchedulerDirectoryProps,\n} from './directory'\nexport type { MeetingAvailability, BookingConfirmation, MeetingBookingErrorCode, MeetingHost }\nexport type { SchedulingLink, SchedulingLinksPayload } from '../../schemas/meeting-booking-schema'\n","'use client'\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { contentFetch } from '../utils/embed-content-fetch'\nimport { MAX_MONTH_OFFSET } from '../utils/hubspot-meetings-convention'\nimport type {\n BookingConfirmation,\n MeetingAvailability,\n MeetingBookingErrorCode,\n} from '../schemas/meeting-booking-schema'\n\n/**\n * useMeetingBooking — data layer for `<HubSpotMeetingScheduler>` (the widget\n * files never inline fetch; same split as `useContactSubmission`/ContactForm).\n *\n * Availability: plain `useState` + `useEffect` + `AbortController` self-fetch\n * (NO client cache layer). An `initialAvailability` seed (SSR mode) renders\n * immediately, but the hook REFETCHES UNCONDITIONALLY after mount — the seed\n * is first-paint scaffolding for a 60-second-volatile resource, not truth.\n *\n * Booking: single POST guarded by `isSubmitting` (the only double-booking\n * guard — the transport never retries POST). Errors surface as typed\n * `MeetingBookingErrorCode`s the widget keys its recovery UI off.\n *\n * All requests go through the host proxy (`apiBaseUrl` prefix, FaqSection\n * precedent) via `contentFetch`, so embedded hosts with an embed-auth adapter\n * inherit auth with zero extra wiring.\n */\n\nexport interface BookingResult {\n ok: boolean\n confirmation?: BookingConfirmation\n code?: MeetingBookingErrorCode\n message?: string\n}\n\nconst KNOWN_CODES: MeetingBookingErrorCode[] = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n]\n\nexport function useMeetingBooking(options: {\n meetingId: string\n apiBaseUrl?: string\n initialAvailability?: MeetingAvailability\n}) {\n const { meetingId, apiBaseUrl = '', initialAvailability } = options\n const seededFor = initialAvailability?.meetingId === meetingId ? initialAvailability : undefined\n\n const [availability, setAvailability] = useState<MeetingAvailability | null>(seededFor ?? null)\n const [monthOffset, setMonthOffsetState] = useState(seededFor?.monthOffset ?? 0)\n const [isLoadingAvailability, setIsLoadingAvailability] = useState(!seededFor)\n const [availabilityError, setAvailabilityError] = useState<string | null>(null)\n const [isSubmitting, setIsSubmitting] = useState(false)\n const submittingRef = useRef(false)\n\n const setMonthOffset = useCallback((offset: number) => {\n setMonthOffsetState(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)))\n }, [])\n\n // Link switch resets paging — without this, the new link is fetched at the\n // PREVIOUS link's month. Layout effect so the fetch effect below sees the\n // reset offset in the same commit.\n const prevMeetingIdRef = useRef(meetingId)\n useLayoutEffect(() => {\n if (prevMeetingIdRef.current === meetingId) return\n prevMeetingIdRef.current = meetingId\n setMonthOffsetState(0)\n }, [meetingId])\n\n // Unconditional (re)fetch on mount and on (meetingId, monthOffset) change —\n // even when SSR-seeded (the seed may already be stale).\n useEffect(() => {\n const controller = new AbortController()\n let active = true\n const load = async () => {\n // Same-month refresh (the unconditional post-mount refetch of an SSR\n // seed) keeps the grid on screen; a DIFFERENT month or link has nothing\n // truthful to show — loading turns on so the widget renders its\n // per-region skeletons instead of a stale grid under a new caption.\n setAvailabilityError(null)\n if (!availability || availability.meetingId !== meetingId || availability.monthOffset !== monthOffset) {\n setIsLoadingAvailability(true)\n }\n try {\n const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) })\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, {\n signal: controller.signal,\n })\n if (!res.ok) throw new Error(`availability ${res.status}`)\n const data = (await res.json()) as MeetingAvailability\n if (active) setAvailability(data)\n } catch (err) {\n if (!active || (err instanceof DOMException && err.name === 'AbortError')) return\n setAvailabilityError(err instanceof Error ? err.message : 'Failed to load availability')\n } finally {\n if (active) setIsLoadingAvailability(false)\n }\n }\n void load()\n return () => {\n active = false\n controller.abort()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refetch is keyed on (meetingId, monthOffset) only; `availability` is read for skeleton gating, not as a trigger\n }, [meetingId, monthOffset, apiBaseUrl])\n\n const refetchAvailability = useCallback(async () => {\n try {\n const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) })\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`)\n if (res.ok) setAvailability((await res.json()) as MeetingAvailability)\n } catch {\n /* recovery refetch is best-effort — the stale grid stays visible */\n }\n }, [meetingId, monthOffset, apiBaseUrl])\n\n const book = useCallback(\n async (payload: Record<string, unknown>): Promise<BookingResult> => {\n if (submittingRef.current) return { ok: false, code: 'VALIDATION', message: 'Already submitting' }\n submittingRef.current = true\n setIsSubmitting(true)\n try {\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/book`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const data = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (res.ok) return { ok: true, confirmation: data as unknown as BookingConfirmation }\n const rawCode = typeof data.code === 'string' ? data.code : ''\n const code = (KNOWN_CODES as string[]).includes(rawCode)\n ? (rawCode as MeetingBookingErrorCode)\n : 'TEMPORARILY_UNAVAILABLE'\n return { ok: false, code, message: typeof data.error === 'string' ? data.error : undefined }\n } catch {\n return { ok: false, code: 'TEMPORARILY_UNAVAILABLE' }\n } finally {\n submittingRef.current = false\n setIsSubmitting(false)\n }\n },\n [apiBaseUrl],\n )\n\n return {\n availability,\n isLoadingAvailability,\n availabilityError,\n monthOffset,\n setMonthOffset,\n refetchAvailability,\n book,\n isSubmitting,\n } as const\n}\n","import { z } from 'zod'\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string\n monthOffset: number\n hasMore: boolean\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[]\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>\n formFields: MeetingFormField[]\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[]\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string\n avatarUrl: string | null\n /** Job title / role line under the name (null → omitted). */\n title: string | null\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string\n title: string\n description: string | null\n kind: 'personal' | 'team'\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[]\n hosts: MeetingHost[]\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>\n fetchedAt: string\n}\n\nexport interface MeetingFormField {\n name: string\n label: string\n type: string\n required: boolean\n options?: string[]\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string\n processingConsentCheckboxLabel: string | null\n communicationConsentText: string | null\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string\n label: string\n required: boolean\n }>\n privacyPolicyText: string | null\n isLegitimateInterest: boolean\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string\n title: string\n startTimeMs: number\n durationMs: number\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream.\n */\nexport type MeetingBookingErrorCode =\n | 'SLOT_TAKEN'\n | 'VALIDATION'\n | 'LINK_GONE'\n | 'TEMPORARILY_UNAVAILABLE'\n | 'MEETING_UNAVAILABLE'\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * HubSpot custom-question types the native form supports. The widget's\n * renderer switches over THIS set and `makeBookingSchema` maps over THIS set;\n * fail-closed = a field whose `type` is not in the set (the widget then\n * renders the \"Open in HubSpot\" escape hatch for that link instead of a\n * half-working native form). Exact upstream type strings are pinned against\n * the rollout fixture link — extend here (renderer + validator move together).\n */\nexport const SUPPORTED_FORM_FIELD_TYPES = ['text', 'textarea', 'select', 'radio', 'checkbox'] as const\nexport type SupportedFormFieldType = (typeof SUPPORTED_FORM_FIELD_TYPES)[number]\n\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return (SUPPORTED_FORM_FIELD_TYPES as readonly string[]).includes(field.type)\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+\\-]+)+)$/\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz })\n return true\n } catch {\n return false\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0\n } catch {\n return false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nexport function makeBookingSchema(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n) {\n const answers: Record<string, z.ZodTypeAny> = {}\n for (const field of formFields) {\n if (!isSupportedFormField(field)) continue // unsupported types are fail-closed at render time\n let validator: z.ZodTypeAny\n switch (field.type as SupportedFormFieldType) {\n case 'checkbox':\n validator = z.boolean()\n break\n case 'select':\n case 'radio':\n validator = z\n .string()\n .refine((v) => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n })\n break\n case 'textarea':\n validator = z.string().max(5000, { message: `${field.label} is too long` })\n break\n case 'text':\n default:\n validator = z.string().max(1000, { message: `${field.label} is too long` })\n break\n }\n if (field.required) {\n validator =\n field.type === 'checkbox'\n ? z.literal(true, { message: `${field.label} is required` })\n : (validator as z.ZodString).min(1, { message: `${field.label} is required` })\n } else if (field.type !== 'checkbox') {\n validator = (validator as z.ZodString).optional().or(z.literal(''))\n } else {\n validator = z.boolean().optional()\n }\n answers[field.name] = validator\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter((c) => c.required)\n .map((c) => c.communicationTypeId)\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = formFields.some((f) => isSupportedFormField(f) && f.required)\n const answersObject = z.object(answers)\n\n return z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: z.number().int().positive(),\n durationMs: z.number().int().positive(),\n firstName: z.string().min(1, { message: 'First name is required' }).max(255),\n lastName: z.string().min(1, { message: 'Last name is required' }).max(255),\n email: z.string().email({ message: 'Please enter a valid email address' }).max(255),\n timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' }),\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? []\n const ok = requiredConsentIds.every((id) =>\n responses.some((r) => r.communicationTypeId === id && r.consented === true),\n )\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n })\n }\n })\n}\n\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>\n","'use client'\n\nimport { useMemo } from 'react'\nimport { SquareAvatar, AvatarStack, Button, Skeleton, Autocomplete } from '../ui'\nimport { ClockIcon } from '../icons-v2-generated'\nimport { cn } from '../../utils/cn'\nimport { formatDurationCompact } from '../../utils/format'\nimport type { MeetingHost } from '../../schemas/meeting-booking-schema'\n\n/**\n * ContextPanel — the \"who / what / how long\" side of the scheduler card\n * (Calendly-anatomy left panel; stacks on top on mobile). Everything here is\n * trust surface: host identity (avatar + name + title), the meeting's own\n * title/description, the duration (chips when the link offers several — the\n * duration choice lives HERE, not as a separate wizard step), and a\n * SEARCHABLE timezone picker (all IANA zones with live GMT offsets;\n * rendering-only — the wire is always epoch-ms).\n *\n * The picker renders only once the parent resolves the zone post-mount — SSR\n * output stays deterministic; a same-footprint skeleton holds the space.\n */\n\nexport interface SchedulerContextPanelProps {\n hosts: MeetingHost[]\n title?: string\n description?: string | null\n durationsMs: number[]\n selectedDurationMs: number | null\n onSelectDuration: (ms: number) => void\n /** Resolved IANA zone (null until the client resolves it). */\n timezone: string | null\n onTimezoneChange?: (tz: string) => void\n /** True once a slot is chosen (details/confirmed steps) — the duration and\n * timezone SELECTORS disappear entirely (the step summary carries both; a\n * disabled picker next to a form just reads as noise) and a static\n * duration line takes their place. Going Back restores them. */\n locked?: boolean\n className?: string\n}\n\nfunction zoneLabel(tz: string): string {\n try {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone: tz,\n timeZoneName: 'shortOffset',\n }).formatToParts(new Date())\n const offset = parts.find((p) => p.type === 'timeZoneName')?.value ?? ''\n return offset ? `${tz.replace(/_/g, ' ')} (${offset})` : tz.replace(/_/g, ' ')\n } catch {\n return tz.replace(/_/g, ' ')\n }\n}\n\n/**\n * Same-footprint skeleton — swaps with the loaded panel with zero shift.\n * STATIC labels (\"Duration\", \"Timezone\") render REAL; only data-driven\n * content (host identity, duration chips, the zone value) is skeleton.\n */\nexport function ContextPanelSkeleton({ className }: { className?: string }) {\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-mf)]', className)}>\n <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n <Skeleton className=\"h-12 w-12 rounded-full shrink-0\" />\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-system-xxs)]\">\n <Skeleton className=\"h-5 w-32\" />\n <Skeleton className=\"h-4 w-24\" />\n </div>\n </div>\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Duration</p>\n <div className=\"flex gap-[var(--spacing-system-xs)]\">\n <Skeleton className=\"h-9 w-20\" />\n <Skeleton className=\"h-9 w-20\" />\n </div>\n </div>\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Timezone</p>\n <Skeleton className=\"h-12 w-full\" />\n </div>\n </div>\n )\n}\n\nexport function SchedulerContextPanel({\n hosts,\n title,\n description,\n durationsMs,\n selectedDurationMs,\n onSelectDuration,\n timezone,\n onTimezoneChange,\n locked = false,\n className,\n}: SchedulerContextPanelProps) {\n // All IANA zones with live GMT offsets — computed once, client-only (the\n // panel renders the picker only after the parent resolves a zone).\n const zoneOptions = useMemo(() => {\n if (!timezone) return []\n let zones: string[]\n try {\n // Older lib targets don't type supportedValuesOf (ES2022) — runtime-guarded.\n const intl = Intl as typeof Intl & { supportedValuesOf?: (key: string) => string[] }\n zones = intl.supportedValuesOf ? intl.supportedValuesOf('timeZone') : [timezone]\n } catch {\n zones = [timezone]\n }\n if (!zones.includes(timezone)) zones = [timezone, ...zones]\n return zones.map((tz) => ({ value: tz, label: zoneLabel(tz) }))\n }, [timezone])\n\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-mf)]', className)}>\n {/* Host identity: full rows up to 3 hosts; larger teams (round-robin\n links can carry many members) collapse to a stacked-avatar cluster\n + count so the panel can never overflow. */}\n {hosts.length > 0 && hosts.length <= 3 && (\n <div className=\"flex flex-col gap-[var(--spacing-system-s)]\">\n {hosts.map((host) => (\n <div key={host.name} className=\"flex items-center gap-[var(--spacing-system-s)]\">\n <SquareAvatar variant=\"round\" size=\"lg\" src={host.avatarUrl ?? undefined} alt={host.name} fallback={host.name} />\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-h4 text-ods-text-primary truncate\">{host.name}</p>\n {host.title && <p className=\"text-h6 text-ods-text-secondary truncate\">{host.title}</p>}\n </div>\n </div>\n ))}\n </div>\n )}\n {hosts.length > 3 && (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <AvatarStack people={hosts} max={4} size=\"lg\" />\n <p className=\"text-h6 text-ods-text-secondary\">{hosts.length} hosts on this calendar</p>\n </div>\n )}\n\n {title && <p className=\"text-h3 text-ods-text-primary\">{title}</p>}\n {description && <p className=\"text-h6 text-ods-text-secondary\">{description}</p>}\n\n {locked ? (\n // Post-selection: selectors are GONE, not disabled — the step summary\n // owns the chosen time/zone; the panel keeps a single static line.\n selectedDurationMs != null && (\n <p className=\"text-h6 text-ods-text-secondary\">{formatDurationCompact(selectedDurationMs / 1000)} call</p>\n )\n ) : (\n <>\n {durationsMs.length > 1 ? (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Duration</p>\n <div className=\"flex flex-wrap gap-[var(--spacing-system-xs)]\">\n {durationsMs.map((ms) => (\n <Button\n key={ms}\n variant={selectedDurationMs === ms ? undefined : 'outline'}\n size=\"small-legacy\"\n onClick={() => onSelectDuration(ms)}\n >\n {formatDurationCompact(ms / 1000)}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n selectedDurationMs != null && (\n <p className=\"text-h6 text-ods-text-secondary\">{formatDurationCompact(selectedDurationMs / 1000)} call</p>\n )\n )}\n\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Timezone</p>\n {timezone ? (\n <Autocomplete\n value={timezone}\n onChange={(tz) => {\n if (tz) onTimezoneChange?.(tz)\n }}\n options={zoneOptions}\n placeholder=\"Search timezone…\"\n startAdornment={<ClockIcon className=\"size-4 shrink-0 text-ods-text-secondary\" />}\n noOptionsText=\"No matching timezone\"\n showClearAll={false}\n />\n ) : (\n <Skeleton className=\"h-12 w-full\" />\n )}\n </div>\n </>\n )}\n </div>\n )\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { Calendar } from '../calendar'\nimport { Button, Skeleton } from '../ui'\nimport { cn } from '../../utils/cn'\nimport { MAX_MONTH_OFFSET } from '../../utils/hubspot-meetings-convention'\n\n/**\n * SlotPicker — day layer + time-chip grid for the meeting scheduler.\n *\n * Day layer is the lib's ODS-styled `Calendar` (react-day-picker v9\n * wrapper) — NOT a bespoke day grid. `startMonth`/`endMonth` pin navigation\n * to [current month, current + MAX_MONTH_OFFSET] so the past is unreachable;\n * days without bookable slots are disabled (visually muted via the v9\n * `disabled` classNames). Month navigation drives the host-proxy\n * `monthOffset`. Time chips are plain `Button`s whose variant flips on\n * selection — no bespoke pill.\n *\n * LAZY STATES (no jumps, ever): each region owns a same-footprint skeleton —\n * `CalendarSkeleton` (caption + weekday row + 6×7 day grid) and\n * `TimeChipsSkeleton` (day heading + chip grid). During an availability\n * refetch (month change) BOTH regions swap to their skeletons; the region\n * containers carry fixed min-heights so loading ⇄ loaded never shifts layout.\n *\n * All slot instants are epoch-ms; every label is rendered in `timezone`\n * (resolved by the parent AFTER mount — this component never reads Intl\n * itself, keeping SSR output deterministic).\n */\n\nexport interface SlotPickerProps {\n /** Bookable slot start times (epoch ms) for the selected duration. */\n slots: number[]\n /** IANA zone every label renders in (parent resolves post-mount). */\n timezone: string\n monthOffset: number\n onMonthOffsetChange: (offset: number) => void\n selectedSlot: number | null\n onSelectSlot: (startMs: number) => void\n selectedDay: string | null\n onSelectDay: (dayKey: string) => void\n /** Availability refetch in flight (month change) → per-region skeletons. */\n isLoading?: boolean\n}\n\n/** Stable per-zone day key for an instant, e.g. \"2026-08-14\" (exported — the\n * parent uses it to auto-select the first available day). */\nexport function dayKeyInZone(ms: number, timeZone: string): string {\n return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(\n new Date(ms),\n )\n}\n\n/**\n * Calendar-side day-key anchoring: react-day-picker exchanges PLAIN calendar\n * dates (local Date objects whose y/m/d ARE the cell). Convert via local\n * date parts — never through an Intl zone — or a `displayTimezone` far from\n * the browser zone shifts cells by a day.\n */\nconst pad2 = (n: number) => String(n).padStart(2, '0')\nfunction calendarDayKey(day: Date): string {\n return `${day.getFullYear()}-${pad2(day.getMonth() + 1)}-${pad2(day.getDate())}`\n}\nfunction dateFromDayKey(key: string): Date {\n const [y, m, d] = key.split('-').map(Number)\n return new Date(y, m - 1, d, 12)\n}\n\nfunction timeLabelInZone(ms: number, timeZone: string): string {\n return new Intl.DateTimeFormat(undefined, { timeZone, hour: 'numeric', minute: '2-digit' }).format(new Date(ms))\n}\n\n/** Calendar region min-height: caption (32) + gap (≤12) + table with 6 week\n * rows (8 outer border-spacing + 7×36 rows + 6×4 row gaps = 284) = 328px.\n * Sized to the TALLEST month so 5-row months, 6-row months, skeletons and\n * loaded states all occupy identical space — measured live, not estimated. */\nconst CALENDAR_MIN_H = 'min-h-[20.5rem]'\n\nconst WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']\n\n/** Deterministic month caption for a given offset (fixed en-US locale so\n * the SSR and client first-paint markup agree byte-for-byte). */\nexport function monthLabelFor(offset: number): string {\n const now = new Date()\n const month = new Date(now.getFullYear(), now.getMonth() + offset, 1)\n return new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(month)\n}\n\n/**\n * Same-footprint skeleton for the day-selection calendar. STATIC chrome —\n * month caption, BOXED nav chevrons, weekday labels — renders REAL (it needs\n * no data); only the day buttons are skeleton cells.\n *\n * GEOMETRY IS EXACT, not approximate — it mirrors the v9 Calendar's real\n * classes: caption row h-8 with h-8/w-8 bordered nav boxes; the grid has NO\n * horizontal gaps (the real table is `border-separate border-spacing-y-1` —\n * vertical spacing only, so cells touch horizontally) and `py-1` reproduces\n * the table's outer border-spacing edges. Any deviation here shifts the\n * times column on skeleton ⇄ loaded swap.\n */\nexport function CalendarSkeleton({ monthOffset = 0 }: { monthOffset?: number }) {\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-s)]', CALENDAR_MIN_H)}>\n <div className=\"relative h-8\">\n <div className=\"absolute inset-x-0 top-0 flex h-8 items-center justify-between\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60\">\n <ChevronLeft className=\"h-4 w-4 text-ods-text-muted\" />\n </span>\n <span className=\"flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60\">\n <ChevronRight className=\"h-4 w-4 text-ods-text-muted\" />\n </span>\n </div>\n <div className=\"flex h-8 items-center justify-center\">\n <p className=\"text-h6 text-ods-text-primary\">{monthLabelFor(monthOffset)}</p>\n </div>\n </div>\n {/* Cells are NOT fixed-width: the real table stretches to its\n container (columns share the extra width, buttons centered) — the\n skeleton's grid tracks do exactly the same. */}\n <div className=\"flex flex-col gap-y-1 py-1\">\n <div className=\"grid grid-cols-7\">\n {WEEKDAY_LABELS.map((d) => (\n <div key={d} className=\"flex h-9 items-center justify-center text-h6 font-normal text-ods-text-secondary\">\n {d}\n </div>\n ))}\n </div>\n {Array.from({ length: 6 }, (_, row) => (\n <div key={`w-${row}`} className=\"grid grid-cols-7\">\n {Array.from({ length: 7 }, (_, col) => (\n <div key={col} className=\"flex h-9 items-center justify-center\">\n <Skeleton className=\"h-9 w-9 rounded-md\" />\n </div>\n ))}\n </div>\n ))}\n </div>\n </div>\n )\n}\n\n/**\n * The full slot-area skeleton — calendar + time-chip regions side by side in\n * the SAME flex wrappers as the loaded `SlotPicker`. This is the ONLY\n * loading visual for the slot area, used by every path that has nothing to\n * show yet (initial availability fetch, the pre-mount timezone-resolution\n * window, month-change refetch) — never a bare rectangle.\n */\nexport function SlotPickerSkeleton({ monthOffset = 0 }: { monthOffset?: number }) {\n return (\n <div className=\"flex flex-col md:flex-row gap-[var(--spacing-system-lf)]\">\n <div className={cn('shrink-0', CALENDAR_MIN_H)}>\n <CalendarSkeleton monthOffset={monthOffset} />\n </div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]\">\n <TimeChipsSkeleton />\n </div>\n </div>\n )\n}\n\n/** Same-footprint skeleton for the time-chip column. */\nexport function TimeChipsSkeleton() {\n return (\n <>\n {/* Day-heading line: text-h6 is 16px tall on mobile, 20px from md up. */}\n <Skeleton className=\"h-4 md:h-5 w-40\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start\">\n {Array.from({ length: 9 }, (_, i) => (\n <Skeleton key={i} className=\"h-10 w-full\" />\n ))}\n </div>\n </>\n )\n}\n\nexport function SlotPicker({\n slots,\n timezone,\n monthOffset,\n onMonthOffsetChange,\n selectedSlot,\n onSelectSlot,\n selectedDay,\n onSelectDay,\n isLoading = false,\n}: SlotPickerProps) {\n const slotsByDay = useMemo(() => {\n const map = new Map<string, number[]>()\n for (const ms of slots) {\n const key = dayKeyInZone(ms, timezone)\n const list = map.get(key) ?? []\n list.push(ms)\n map.set(key, list)\n }\n for (const list of map.values()) list.sort((a, b) => a - b)\n return map\n }, [slots, timezone])\n\n const now = new Date()\n const startMonth = new Date(now.getFullYear(), now.getMonth(), 1)\n const endMonth = new Date(now.getFullYear(), now.getMonth() + MAX_MONTH_OFFSET, 1)\n const visibleMonth = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1)\n\n const daySlots = selectedDay ? (slotsByDay.get(selectedDay) ?? []) : []\n\n return (\n <div className=\"flex flex-col md:flex-row gap-[var(--spacing-system-lf)]\">\n <div className={cn('shrink-0', CALENDAR_MIN_H)}>\n {isLoading ? (\n <CalendarSkeleton monthOffset={monthOffset} />\n ) : (\n <Calendar\n month={visibleMonth}\n startMonth={startMonth}\n endMonth={endMonth}\n onMonthChange={(month) => {\n const offset = (month.getFullYear() - now.getFullYear()) * 12 + (month.getMonth() - now.getMonth())\n onMonthOffsetChange(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)))\n }}\n mode=\"single\"\n required\n // CONTROLLED selection: v9 needs BOTH `selected` AND `onSelect` —\n // with `selected` alone it goes uncontrolled (internal state\n // seeded at mount), silently ignoring the parent's auto-selected\n // first day. `required` disables click-to-deselect.\n selected={selectedDay ? dateFromDayKey(selectedDay) : undefined}\n onSelect={(day) => {\n if (!day) return\n const key = calendarDayKey(day)\n if (slotsByDay.has(key)) onSelectDay(key)\n }}\n disabled={(day) => !slotsByDay.has(calendarDayKey(day))}\n className=\"p-0\"\n />\n )}\n </div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]\">\n {isLoading ? (\n <TimeChipsSkeleton />\n ) : selectedDay ? (\n <>\n <p className=\"text-h6 text-ods-text-primary\">\n {/* Format the DAY-KEY itself (plain calendar date) — re-zoning a\n local instant could label an adjacent day. */}\n {new Intl.DateTimeFormat(undefined, {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n }).format(dateFromDayKey(selectedDay))}\n </p>\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start max-h-80 overflow-y-auto\">\n {daySlots.map((ms) => {\n const isSelected = selectedSlot === ms\n return (\n <Button\n key={ms}\n variant={isSelected ? undefined : 'outline'}\n size=\"small-legacy\"\n onClick={() => onSelectSlot(ms)}\n >\n {timeLabelInZone(ms, timezone)}\n </Button>\n )\n })}\n </div>\n </>\n ) : (\n <p className=\"text-h6 text-ods-text-secondary\">\n {[...slotsByDay.keys()].some((k) =>\n k.startsWith(\n `${visibleMonth.getFullYear()}-${String(visibleMonth.getMonth() + 1).padStart(2, '0')}`,\n ),\n )\n ? 'Pick a day to see available times.'\n : 'No available times this month — try the next one.'}\n </p>\n )}\n </div>\n </div>\n )\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { useForm, Controller } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n makeBookingSchema,\n isSupportedFormField,\n type MeetingAvailability,\n type MeetingBookingPayload,\n} from '../../schemas/meeting-booking-schema'\nimport {\n Button,\n Input,\n Textarea,\n Label,\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n Checkbox,\n RadioGroup,\n RadioGroupItem,\n} from '../ui'\nimport { HoneypotField } from '../ui/honeypot-field'\n\n/**\n * BookingForm — attendee details + the link's declared custom questions +\n * verbatim legal-consent copy. ContactForm's scaffolding (react-hook-form +\n * zodResolver + lib field primitives); a sibling rather than a `<ContactForm>`\n * configuration because of the dynamic HubSpot `formFields`, the per-checkbox\n * consent model, and the first/last-name field model — none expressible via\n * `hideFields`/`extraTopField`.\n *\n * Bot protection is LOAD-BEARING: without the humanity signals in the body,\n * the host's `verifyHuman` degrades to first-party-only BotID (fails open for\n * external embedders). Honeypot + elapsed-ms are merged into the POST at\n * submit; the parent calls `resetSignals()` after a SLOT_TAKEN refetch so a\n * legitimate retry isn't flagged too-fast.\n */\n\nexport interface BookingFormProps {\n availability: MeetingAvailability\n meetingId: string\n startTimeMs: number\n durationMs: number\n /** IANA zone the confirmation/invite should render in (parent-resolved). */\n timezone: string\n isSubmitting: boolean\n onSubmit: (payload: Record<string, unknown>) => Promise<void>\n /** From useHumanitySignals — parent owns the instance so it can resetSignals(). */\n honeypotInputProps: { ref: React.Ref<HTMLInputElement>; name: string }\n getSignals: () => Record<string, string | number>\n}\n\nexport function BookingForm({\n availability,\n meetingId,\n startTimeMs,\n durationMs,\n timezone,\n isSubmitting,\n onSubmit,\n honeypotInputProps,\n getSignals,\n}: BookingFormProps) {\n const { formFields, legalConsent } = availability\n const supportedFields = useMemo(() => formFields.filter(isSupportedFormField), [formFields])\n const schema = useMemo(() => makeBookingSchema(supportedFields, legalConsent), [supportedFields, legalConsent])\n\n const {\n register,\n control,\n handleSubmit,\n formState: { errors },\n } = useForm<MeetingBookingPayload>({\n resolver: zodResolver(schema),\n defaultValues: {\n meetingId,\n startTimeMs,\n durationMs,\n timezone,\n locale: typeof navigator !== 'undefined' ? navigator.language : undefined,\n firstName: '',\n lastName: '',\n email: '',\n formFields: {},\n legalConsentResponses: (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({\n communicationTypeId: c.communicationTypeId,\n consented: false,\n })),\n },\n })\n\n const submit = handleSubmit(async (data) => {\n await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() })\n })\n\n const fieldError = (name: string): string | undefined => {\n const err = (errors.formFields as Record<string, { message?: string }> | undefined)?.[name]\n return err?.message\n }\n\n // Field chrome mirrors ContactForm 1:1 (`contact/contact-form.tsx`) — the\n // booking form must be indistinguishable from every other form in the app.\n const inputClass = 'bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12'\n\n return (\n <form onSubmit={submit} className=\"flex flex-col space-y-4 md:space-y-6\" noValidate>\n <HoneypotField {...honeypotInputProps} />\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6\">\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-first-name\">\n First name<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-first-name\"\n autoComplete=\"given-name\"\n placeholder=\"Jane\"\n aria-invalid={!!errors.firstName}\n className={inputClass}\n {...register('firstName')}\n />\n {errors.firstName && <span className=\"text-ods-error text-h6 mt-1\">{errors.firstName.message}</span>}\n </div>\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-last-name\">\n Last name<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-last-name\"\n autoComplete=\"family-name\"\n placeholder=\"Doe\"\n aria-invalid={!!errors.lastName}\n className={inputClass}\n {...register('lastName')}\n />\n {errors.lastName && <span className=\"text-ods-error text-h6 mt-1\">{errors.lastName.message}</span>}\n </div>\n </div>\n\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-email\">\n Email<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"jane@company.com\"\n aria-invalid={!!errors.email}\n className={inputClass}\n {...register('email')}\n />\n {errors.email && <span className=\"text-ods-error text-h6 mt-1\">{errors.email.message}</span>}\n </div>\n\n {supportedFields.map((field) => (\n <div key={field.name} className=\"flex flex-col\">\n <Label htmlFor={`ms-q-${field.name}`}>\n {field.label}\n {field.required && <span className=\"text-ods-accent\">*</span>}\n </Label>\n {field.type === 'textarea' && (\n <Textarea\n id={`ms-q-${field.name}`}\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3\"\n {...register(`formFields.${field.name}` as never)}\n />\n )}\n {field.type === 'text' && (\n <Input\n id={`ms-q-${field.name}`}\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12\"\n {...register(`formFields.${field.name}` as never)}\n />\n )}\n {(field.type === 'select' || field.type === 'radio') && (\n <Controller\n control={control}\n name={`formFields.${field.name}` as never}\n render={({ field: rhf }) =>\n field.type === 'select' ? (\n <Select value={(rhf.value as string) ?? ''} onValueChange={rhf.onChange}>\n <SelectTrigger id={`ms-q-${field.name}`}>\n <SelectValue placeholder=\"Select…\" />\n </SelectTrigger>\n <SelectContent>\n {(field.options ?? []).map((opt) => (\n <SelectItem key={opt} value={opt}>\n {opt}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <RadioGroup value={(rhf.value as string) ?? ''} onValueChange={rhf.onChange}>\n {(field.options ?? []).map((opt) => (\n <div key={opt} className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <RadioGroupItem id={`ms-q-${field.name}-${opt}`} value={opt} />\n <Label htmlFor={`ms-q-${field.name}-${opt}`}>{opt}</Label>\n </div>\n ))}\n </RadioGroup>\n )\n }\n />\n )}\n {field.type === 'checkbox' && (\n <Controller\n control={control}\n name={`formFields.${field.name}` as never}\n render={({ field: rhf }) => (\n // The question's Label above already carries the text and\n // binds to this id — a second inline label would double the\n // visible text AND the accessible name.\n <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <Checkbox\n id={`ms-q-${field.name}`}\n checked={Boolean(rhf.value)}\n onCheckedChange={(v) => rhf.onChange(v === true)}\n />\n </div>\n )}\n />\n )}\n {fieldError(field.name) && (\n <span className=\"text-ods-error text-h6 mt-1\">{fieldError(field.name)}</span>\n )}\n </div>\n ))}\n\n {legalConsent && (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-md)]\">\n {/* GDPR surface — HubSpot's consent copy rendered VERBATIM, never edited. */}\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.processingConsentText}</p>\n {legalConsent.communicationConsentText && (\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.communicationConsentText}</p>\n )}\n <Controller\n control={control}\n name=\"legalConsentResponses\"\n render={({ field: rhf }) => (\n <>\n {legalConsent.communicationConsentCheckboxes.map((box) => {\n const responses = (rhf.value ?? []) as Array<{ communicationTypeId: string; consented: boolean }>\n const current = responses.find((r) => r.communicationTypeId === box.communicationTypeId)\n return (\n <div key={box.communicationTypeId} className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <Checkbox\n id={`ms-consent-${box.communicationTypeId}`}\n checked={current?.consented ?? false}\n onCheckedChange={(v) =>\n rhf.onChange(\n responses.map((r) =>\n r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v === true } : r,\n ),\n )\n }\n />\n <Label htmlFor={`ms-consent-${box.communicationTypeId}`}>\n {box.label}\n {box.required ? ' *' : ''}\n </Label>\n </div>\n )\n })}\n </>\n )}\n />\n {legalConsent.privacyPolicyText && (\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.privacyPolicyText}</p>\n )}\n {errors.legalConsentResponses && (\n <p className=\"text-h6 text-ods-error\">{errors.legalConsentResponses.message as string}</p>\n )}\n </div>\n )}\n\n {/* Step navigation back to the calendar lives in the step header (the\n app-standard BackButton, rendered by the parent) — the form ships\n only its submit. */}\n <div className=\"flex\">\n <Button type=\"submit\" loading={isSubmitting} disabled={isSubmitting} className=\"w-full md:w-auto\">\n Confirm booking\n </Button>\n </div>\n </form>\n )\n}\n","'use client'\n\nimport { formatDurationCompact } from '../../utils/format'\nimport type { BookingConfirmation } from '../../schemas/meeting-booking-schema'\n\n/**\n * Confirmation — terminal success state after a completed booking. Renders\n * the booked instant in the visitor's resolved zone. Deliberately NO\n * follow-up CTA: the job is done, the calendar invite is the next touchpoint\n * (a \"book another\" button here tested as noise).\n */\n\nexport interface ConfirmationProps {\n confirmation: BookingConfirmation\n timezone: string\n}\n\nexport function Confirmation({ confirmation, timezone }: ConfirmationProps) {\n const when = new Intl.DateTimeFormat(undefined, {\n timeZone: timezone,\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n }).format(new Date(confirmation.startTimeMs))\n\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-md)] text-center\">\n <p className=\"text-h1\" role=\"img\" aria-label=\"Celebration\">\n 🎉\n </p>\n <h3 className=\"text-h3 text-ods-text-primary\">You're booked</h3>\n <p className=\"text-h5 text-ods-text-primary\">{confirmation.title}</p>\n <p className=\"text-h6 text-ods-text-secondary\">\n {when} · {formatDurationCompact(confirmation.durationMs / 1000)}\n </p>\n <p className=\"text-h6 text-ods-text-secondary\">A calendar invite is on its way to your email.</p>\n </div>\n )\n}\n","'use client'\n\n/**\n * `<MeetingSchedulerDirectory />` — the embeddable scheduling DIRECTORY\n * block: clickable row cards (host avatars, title/description, For-<audience>\n * chip, next available time, boxed chevron) + the house\n * `PersistentPaginationWrapper`, fed by a host proxy's `GET\n * {apiBaseUrl}/api/meetings` (see `docs/EMBEDDING_HUBSPOT_MEETINGS.md`).\n * Rows navigate to `{bookingBasePath}/{slug}` — pair with\n * `<HubSpotMeetingScheduler>` on that page for the full white-label flow.\n *\n * Layout stability is engineered, not incidental: the rows area reserves one\n * FULL page (pageSize × 80px rows + gaps), the pagination slot holds its\n * measured 72px even when hidden, and the loading state renders same-shell\n * skeleton rows — so nothing below the block ever jumps across\n * loading ⇄ loaded ⇄ page changes ⇄ short last pages.\n *\n * SSR/host mode: pass `initialData` (host-fetched) to skip the client fetch.\n * Client/embed mode: omit it — skeleton page + self-fetch via `contentFetch`\n * (embed-auth adapters inherited for free).\n */\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { ChevronRight } from 'lucide-react'\nimport Link from '../../embed-shims/next-link'\nimport { contentFetch } from '../../utils/embed-content-fetch'\nimport { formatDurationCompact } from '../../utils/format'\nimport { cn } from '../../utils/cn'\nimport { AvatarStack, StatusBadge, Skeleton } from '../ui'\nimport { EmptyState } from '../empty-state'\nimport { PersistentPaginationWrapper } from '../persistent-pagination'\nimport type { SchedulingLink, SchedulingLinksPayload } from '../../schemas/meeting-booking-schema'\n\nexport interface MeetingSchedulerDirectoryProps {\n /** Endpoints prefix, default '' (same-origin `/api/meetings`). */\n apiBaseUrl?: string\n /** Host-fetched seed — skips the client fetch when provided. */\n initialData?: SchedulingLinksPayload | null\n /** `scope=all` — full-portal view (non-conforming links under \"other\"). */\n includeAll?: boolean\n /** Rows per page; also the skeleton row count and the reserved rows-area height. */\n pageSize?: number\n /** Rows link to `${bookingBasePath}/${slug}`. */\n bookingBasePath?: string\n /** EmptyState CTA target for error/empty states (omit → no CTA). */\n contactHref?: string\n className?: string\n}\n\n/** Zone-aware \"next available\" label — resolved POST-mount (SSR renders the\n * static caption only, so server and client first paint agree). */\nfunction useNextAvailableLabel(ms: number | null): string | null {\n const [label, setLabel] = useState<string | null>(null)\n useEffect(() => {\n if (ms == null) return\n try {\n setLabel(\n new Intl.DateTimeFormat(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }).format(new Date(ms)),\n )\n } catch {\n setLabel(null)\n }\n }, [ms])\n return label\n}\n\nfunction DirectoryRow({\n link,\n audienceLabel,\n bookingBasePath,\n}: {\n link: SchedulingLink\n audienceLabel: string\n bookingBasePath: string\n}) {\n const nextLabel = useNextAvailableLabel(link.nextAvailableMs)\n\n const subtitle = [\n link.description,\n link.durationsMinutes.length > 0 ? link.durationsMinutes.map((m) => formatDurationCompact(m * 60)).join(' / ') : null,\n link.kind === 'team' ? 'Team' : null,\n ]\n .filter(Boolean)\n .join(' • ')\n\n return (\n <Link\n href={`${bookingBasePath}/${link.slug}`}\n prefetch={false}\n className=\"block rounded-lg no-underline text-inherit focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent\"\n >\n <div className=\"flex w-full items-center gap-4 px-4 py-3 min-h-20 rounded-lg border border-ods-border bg-ods-card transition-colors duration-150 hover:bg-ods-bg-hover\">\n {/* Title leads at a fixed X; hosts live in the right meta cluster. */}\n <div className=\"flex min-w-0 flex-1 flex-col justify-center text-left\">\n <p className=\"text-h3 text-ods-text-primary truncate\">{link.title}</p>\n {subtitle && <p className=\"text-h6 text-ods-text-secondary truncate\">{subtitle}</p>}\n </div>\n\n {/* No audience entity (scope=all \"other\" group) → no chip. */}\n {audienceLabel && (\n <StatusBadge text={`For ${audienceLabel}`} singleLine className=\"shrink-0 hidden sm:inline-flex\" />\n )}\n\n {/* Hosts stay visible on mobile too — the facepile is the row's \"who\n you're booking with\" signal; only the chip and next-available meta\n collapse on narrow widths. */}\n <AvatarStack people={link.hosts} max={3} size=\"md\" className=\"flex shrink-0\" />\n\n <div className=\"hidden md:flex w-44 shrink-0 flex-col items-end justify-center\">\n <p className=\"text-h6 text-ods-text-secondary\">Next available</p>\n <p className=\"text-h6 text-ods-text-primary min-h-5\">\n {link.nextAvailableMs == null ? 'No times published' : (nextLabel ?? ' ')}\n </p>\n </div>\n\n {/* House boxed chevron (ChatTicketItem). */}\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-card\">\n <ChevronRight className=\"size-6 text-ods-text-secondary\" />\n </span>\n </div>\n </Link>\n )\n}\n\n/** Same-shell loading row (co-located-skeleton convention). */\nexport function MeetingSchedulerDirectoryRowSkeleton() {\n return (\n <div className=\"flex w-full items-center gap-4 px-4 py-3 min-h-20 rounded-lg border border-ods-border bg-ods-card\">\n <div className=\"flex min-w-0 flex-1 flex-col justify-center gap-1\">\n <Skeleton className=\"h-5 w-1/2\" />\n <Skeleton className=\"h-4 w-1/3\" />\n </div>\n <Skeleton className=\"hidden sm:block h-6 w-28 shrink-0\" />\n <Skeleton className=\"h-10 w-10 shrink-0 rounded-full\" />\n <div className=\"hidden md:flex w-44 shrink-0 flex-col items-end gap-1\">\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-5 w-32\" />\n </div>\n <Skeleton className=\"size-12 shrink-0 rounded-md\" />\n </div>\n )\n}\n\n/** Measured pagination-slot height (PersistentPaginationWrapper = 72px). */\nconst PAGINATION_SLOT_H = 'h-[4.5rem]'\n\nexport function MeetingSchedulerDirectory({\n apiBaseUrl = '',\n initialData = null,\n includeAll = false,\n pageSize = 6,\n bookingBasePath = '/schedule-a-call',\n contactHref,\n className,\n}: MeetingSchedulerDirectoryProps) {\n const [data, setData] = useState<SchedulingLinksPayload | null>(initialData)\n const [isLoading, setIsLoading] = useState(initialData === null)\n const [error, setError] = useState<string | null>(null)\n const [page, setPage] = useState(1)\n\n useEffect(() => {\n if (initialData !== null) return // host-seeded — skip the client fetch\n const controller = new AbortController()\n let active = true\n const load = async () => {\n setIsLoading(true)\n setError(null)\n try {\n const qs = includeAll ? '?scope=all' : ''\n const res = await contentFetch(`${apiBaseUrl}/api/meetings${qs}`, { signal: controller.signal })\n if (!res.ok) throw new Error(`meetings ${res.status}`)\n const payload = (await res.json()) as SchedulingLinksPayload\n if (active) setData(payload)\n } catch (err) {\n if (!active || (err instanceof DOMException && err.name === 'AbortError')) return\n setError(err instanceof Error ? err.message : 'Failed to fetch scheduling links')\n } finally {\n if (active) setIsLoading(false)\n }\n }\n void load()\n return () => {\n active = false\n controller.abort()\n }\n }, [initialData, includeAll, apiBaseUrl])\n\n const rows = useMemo(() => {\n const purposes = data?.purposes ?? []\n return purposes.flatMap((p) => p.links.map((link) => ({ link, audienceLabel: p.label })))\n }, [data])\n\n const totalPages = Math.max(1, Math.ceil(rows.length / pageSize))\n const pageRows = rows.slice((page - 1) * pageSize, page * pageSize)\n\n // Reserved rows-area height: pageSize × 80px rows + (pageSize−1) × 16px gaps.\n const rowsMinHeight = pageSize * 80 + (pageSize - 1) * 16\n\n if (isLoading && !data) {\n return (\n <div className={cn('flex flex-col', className)}>\n <div className=\"flex w-full flex-col gap-4\" style={{ minHeight: rowsMinHeight }}>\n {Array.from({ length: pageSize }, (_, i) => (\n <MeetingSchedulerDirectoryRowSkeleton key={i} />\n ))}\n </div>\n <div className={cn('mt-8 flex w-full items-center justify-center', PAGINATION_SLOT_H)}>\n <Skeleton className=\"h-10 w-64\" />\n </div>\n </div>\n )\n }\n\n if (error) {\n return (\n <div className={cn('flex flex-col justify-center', className)} style={{ minHeight: rowsMinHeight + 104 }}>\n <EmptyState\n type=\"generic\"\n title=\"We couldn't load available call times\"\n description=\"Something went wrong on our side. Try again in a minute, or reach us directly.\"\n {...(contactHref ? { ctaText: 'Contact us', ctaHref: contactHref } : {})}\n />\n </div>\n )\n }\n\n if (rows.length === 0) {\n return (\n <div className={cn('flex flex-col justify-center', className)} style={{ minHeight: rowsMinHeight + 104 }}>\n <EmptyState\n type=\"generic\"\n title=\"No call times are published right now\"\n description=\"We're updating our calendars. Reach us directly and we'll set something up.\"\n {...(contactHref ? { ctaText: 'Contact us', ctaHref: contactHref } : {})}\n />\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n <div className=\"flex w-full flex-col gap-4\" style={{ minHeight: rowsMinHeight }}>\n {pageRows.map(({ link, audienceLabel }) => (\n <DirectoryRow key={link.id} link={link} audienceLabel={audienceLabel} bookingBasePath={bookingBasePath} />\n ))}\n </div>\n {/* House pagination block — dims in place during refetches; the slot\n holds its measured height even at ≤1 page (where the inner\n UnifiedPagination returns null). */}\n <div className={cn('mt-8 flex w-full items-center justify-center', PAGINATION_SLOT_H)}>\n {totalPages > 1 && (\n <PersistentPaginationWrapper\n isLoading={isLoading}\n currentPage={page}\n totalPages={totalPages}\n onPageChange={setPage}\n variant=\"blog\"\n />\n )}\n </div>\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/meeting-scheduler/index.cjs","../../../src/components/meeting-scheduler/index.tsx","../../../src/hooks/use-meeting-booking.ts","../../../src/schemas/meeting-booking-schema.ts","../../../src/components/meeting-scheduler/context-panel.tsx","../../../src/components/meeting-scheduler/slot-picker.tsx","../../../src/components/meeting-scheduler/booking-form.tsx","../../../src/components/meeting-scheduler/confirmation.tsx","../../../src/components/meeting-scheduler/directory.tsx"],"names":["jsx","Fragment","jsxs","ChevronRight","useState","useCallback"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/meeting-scheduler/index.cjs","sourcesContent":[null,"'use client'\n\n/**\n * `<HubSpotMeetingScheduler />` — natively-branded booking flow over the\n * HubSpot Meetings scheduler, rendered entirely with ODS primitives (no\n * iframe, no third-party chrome, no scripts). Data flows exclusively through\n * a HOST PROXY (`GET {apiBaseUrl}/api/meetings/availability`,\n * `POST {apiBaseUrl}/api/meetings/book`) — the widget has zero HubSpot\n * knowledge and no secrets; see `docs/EMBEDDING_HUBSPOT_MEETINGS.md` for the\n * endpoint contract a host must serve (external embedders proxy through\n * their own backend — the book route is IP-rate-limited and bot-gated).\n *\n * LAYOUT (Calendly-anatomy, conversion-first): one bordered card, two\n * panels. Left/top — the CONTEXT panel: host identity (avatar + name +\n * title from `availability.hosts`), meeting title/description, duration\n * chips (the duration choice lives here, NOT as a wizard step — the visitor\n * always lands straight on the calendar), and the resolved timezone.\n * Right/bottom — the ACTION panel: calendar + time slots (first available\n * day auto-selected so slots are visible immediately), then the details\n * form, then the confirmation. Panels stack on mobile.\n *\n * Two modes, one component:\n * - SSR mode: pass `initialAvailability` (host-fetched) — the\n * zone-independent shell (context panel, calendar) renders server-side;\n * slot TIME labels wait for the client zone resolution (deterministic\n * first paint, no hydration mismatch), and availability is refetched\n * unconditionally after mount (the seed is scaffolding for a 60s-volatile\n * resource, not truth).\n * - Client mode: omit the seed — skeleton + self-fetch on mount.\n *\n * State machine: slot → details → confirmed, with a back edge, a \"book\n * another\" reset, and a `submitting` lock as the double-booking guard.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { cn } from '../../utils/cn'\nimport { formatDurationCompact } from '../../utils/format'\nimport { Button } from '../ui'\nimport { BackButton } from '../layout/back-button'\nimport { useHumanitySignals } from '../../hooks/use-humanity-signals'\nimport { useMeetingBooking } from '../../hooks/use-meeting-booking'\nimport {\n isSupportedFormField,\n type BookingConfirmation,\n type MeetingAvailability,\n type MeetingBookingErrorCode,\n type MeetingHost,\n} from '../../schemas/meeting-booking-schema'\nimport { SchedulerContextPanel, ContextPanelSkeleton } from './context-panel'\nimport { SlotPicker, SlotPickerSkeleton, dayKeyInZone } from './slot-picker'\nimport { BookingForm } from './booking-form'\nimport { Confirmation } from './confirmation'\n\nexport interface HubSpotMeetingSchedulerProps {\n /** Directory id of the meeting link (from the host's `/api/meetings` payload). */\n meetingId: string\n /** Endpoints prefix, default '' (same-origin `/api/meetings/*`) — FaqSection precedent. */\n apiBaseUrl?: string\n /** SSR-mode seed (host-fetched). Omitted → client mode (self-fetch on mount). */\n initialAvailability?: MeetingAvailability\n /** Meeting title shown in the context panel (host page owns the h1). */\n title?: string\n /** Short description under the title in the context panel. */\n description?: string | null\n /** Override the hosts shown (defaults to `availability.hosts`). */\n hosts?: MeetingHost[]\n /** Pin the DISPLAY zone (rendering only — never sent upstream, never a cache key). */\n displayTimezone?: string\n /** The link's public HubSpot booking URL — the \"Open in HubSpot\" escape hatch target. */\n fallbackUrl?: string\n onBooked?: (b: BookingConfirmation) => void\n className?: string\n}\n\ntype Step = 'slot' | 'details' | 'confirmed'\n\n/**\n * Fail-closed gate: a link whose declared questions include an unsupported\n * type, or whose consent block is malformed, must NOT render a half-working\n * native form (a silently dropped required question or missing consent copy\n * is worse than no native form) — the escape hatch takes over.\n */\nfunction isNativelyBookable(availability: MeetingAvailability): boolean {\n if (!availability.formFields.every(isSupportedFormField)) return false\n const consent = availability.legalConsent\n if (consent) {\n if (typeof consent.processingConsentText !== 'string') return false\n if (!Array.isArray(consent.communicationConsentCheckboxes)) return false\n }\n return true\n}\n\nexport function HubSpotMeetingScheduler({\n meetingId,\n apiBaseUrl = '',\n initialAvailability,\n title,\n description,\n hosts,\n displayTimezone,\n fallbackUrl,\n onBooked,\n className,\n}: HubSpotMeetingSchedulerProps) {\n const {\n availability,\n isLoadingAvailability,\n availabilityError,\n monthOffset,\n setMonthOffset,\n refetchAvailability,\n book,\n isSubmitting,\n } = useMeetingBooking({ meetingId, apiBaseUrl, initialAvailability })\n\n // Zone resolution happens POST-mount (Intl) unless the embedder pins one —\n // the initial (server) render stays deterministic; time labels appear once\n // a zone exists.\n const [timezone, setTimezone] = useState<string | null>(displayTimezone ?? null)\n useEffect(() => {\n if (displayTimezone) return\n try {\n setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC')\n } catch {\n setTimezone('UTC')\n }\n }, [displayTimezone])\n\n const [step, setStep] = useState<Step>('slot')\n const [durationMs, setDurationMs] = useState<number | null>(null)\n const [selectedDay, setSelectedDay] = useState<string | null>(null)\n const [selectedSlot, setSelectedSlot] = useState<number | null>(null)\n const [confirmation, setConfirmation] = useState<BookingConfirmation | null>(null)\n const [bookingError, setBookingError] = useState<MeetingBookingErrorCode | null>(null)\n\n const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()\n\n // Reset the machine when the host switches links.\n useEffect(() => {\n setStep('slot')\n setDurationMs(null)\n setSelectedDay(null)\n setSelectedSlot(null)\n setConfirmation(null)\n setBookingError(null)\n autoAdvanceCount.current = 0\n }, [meetingId])\n\n const durations = availability?.durationsMs ?? []\n const shownHosts = hosts ?? availability?.hosts ?? []\n\n // Default to the first offered duration — the visitor lands straight on\n // the calendar; duration is a context-panel chip, not a wizard step.\n useEffect(() => {\n if (durationMs == null && durations.length > 0) setDurationMs(durations[0])\n }, [durationMs, durations])\n\n const slots = useMemo(() => {\n if (!availability || durationMs == null) return []\n return availability.slotsByDurationMs[String(durationMs)] ?? []\n }, [availability, durationMs])\n\n // Auto-select the first day WITH SLOTS IN THE VISIBLE MONTH (once the zone\n // is known) so the visitor sees concrete times immediately — never a dead\n // \"pick a day\" state, and NEVER a day from another month than the calendar\n // shows (HubSpot's monthOffset payloads can carry near-term slots outside\n // the requested month — the visible grid is the authority).\n useEffect(() => {\n if (step !== 'slot' || !timezone) return\n const now = new Date()\n const visible = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1)\n const monthPrefix = `${visible.getFullYear()}-${String(visible.getMonth() + 1).padStart(2, '0')}`\n const inMonth = slots.filter((ms) => dayKeyInZone(ms, timezone).startsWith(monthPrefix))\n if (inMonth.length === 0) {\n if (selectedDay && !selectedDay.startsWith(monthPrefix)) setSelectedDay(null)\n return\n }\n const dayKeys = new Set(inMonth.map((ms) => dayKeyInZone(ms, timezone)))\n if (selectedDay && dayKeys.has(selectedDay)) return\n setSelectedDay(dayKeyInZone(inMonth[0], timezone))\n }, [step, timezone, slots, selectedDay, monthOffset])\n\n // Fully-booked month with more ahead → auto-advance (bounded, ≤3 hops)\n // before showing the empty state.\n const autoAdvanceCount = useRef(0)\n useEffect(() => {\n if (\n step === 'slot' &&\n availability &&\n !isLoadingAvailability &&\n durationMs != null &&\n slots.length === 0 &&\n availability.hasMore &&\n autoAdvanceCount.current < 3\n ) {\n autoAdvanceCount.current += 1\n setMonthOffset(monthOffset + 1)\n }\n }, [step, availability, isLoadingAvailability, durationMs, slots, monthOffset, setMonthOffset])\n\n const handleSubmit = useCallback(\n async (payload: Record<string, unknown>) => {\n setBookingError(null)\n const result = await book(payload)\n if (result.ok && result.confirmation) {\n setConfirmation(result.confirmation)\n setStep('confirmed')\n onBooked?.(result.confirmation)\n return\n }\n const code = result.code ?? 'TEMPORARILY_UNAVAILABLE'\n setBookingError(code)\n if (code === 'SLOT_TAKEN') {\n // Recover: refresh the grid (the slot vanishes) and reset the timing\n // signal so the retry isn't flagged too-fast.\n setSelectedSlot(null)\n setStep('slot')\n resetSignals()\n void refetchAvailability()\n }\n },\n [book, onBooked, refetchAvailability, resetSignals],\n )\n\n const escapeHatch = fallbackUrl ? (\n <Button variant=\"outline\" size=\"small-legacy\" href={fallbackUrl} openInNewTab>\n Open in HubSpot\n </Button>\n ) : null\n\n // ---- terminal / degraded states -----------------------------------------\n\n if (isLoadingAvailability && !availability) {\n // Per-region skeletons, composed inside the SAME card wrappers as the\n // loaded state: `ContextPanelSkeleton` (profile side), `CalendarSkeleton`\n // (day buttons), `TimeChipsSkeleton` (hour options). Identical footprints\n // by construction — loading ⇄ loaded never shifts layout.\n return (\n <div className={cn('rounded-md border border-ods-border bg-ods-card overflow-hidden', className)}>\n <div className=\"flex flex-col md:flex-row\">\n <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\" />\n <div className=\"flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col gap-[var(--spacing-system-md)]\">\n {/* Static heading renders REAL — it needs no data. */}\n <p className=\"text-h5 text-ods-text-secondary\">Select a date & time</p>\n <SlotPickerSkeleton />\n </div>\n </div>\n </div>\n )\n }\n\n if (availabilityError || !availability) {\n return (\n <div\n className={cn(\n 'rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]',\n className,\n )}\n >\n <p className=\"text-h6 text-ods-text-secondary\">\n We couldn't load available call times. Please try again shortly.\n </p>\n {escapeHatch}\n </div>\n )\n }\n\n if (!isNativelyBookable(availability)) {\n // Fail closed — never render a half-working native form on a link with\n // questions or consent we can't faithfully reproduce.\n return (\n <div\n className={cn(\n 'rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-lf)] flex flex-col items-start gap-[var(--spacing-system-md)]',\n className,\n )}\n >\n <p className=\"text-h6 text-ods-text-secondary\">This meeting type is booked directly on HubSpot.</p>\n {escapeHatch}\n </div>\n )\n }\n\n // ---- the card ------------------------------------------------------------\n\n return (\n <div className={cn('rounded-md border border-ods-border bg-ods-card overflow-hidden', className)}>\n <div className=\"flex flex-col md:flex-row\">\n <SchedulerContextPanel\n hosts={shownHosts}\n title={title}\n description={description}\n durationsMs={durations}\n selectedDurationMs={durationMs}\n onSelectDuration={(ms) => {\n setDurationMs(ms)\n setSelectedDay(null)\n setSelectedSlot(null)\n if (step === 'details') setStep('slot')\n }}\n timezone={timezone}\n onTimezoneChange={setTimezone}\n locked={step !== 'slot'}\n className=\"p-[var(--spacing-system-lf)] md:w-80 md:shrink-0 border-b md:border-b-0 md:border-r border-ods-border\"\n />\n\n {/* min-height pins the card so slot ⇄ details ⇄ confirmed transitions\n never jump the page (sized to the slot step, the tallest one);\n flex column so shorter steps can center vertically. */}\n <div className=\"flex-1 min-w-0 p-[var(--spacing-system-lf)] md:min-h-[32rem] flex flex-col\">\n {step === 'confirmed' && confirmation && timezone ? (\n <Confirmation confirmation={confirmation} timezone={timezone} />\n ) : step === 'details' && durationMs != null && selectedSlot != null && timezone ? (\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-system-md)]\">\n {/* App-standard BackButton — pinned at the TOP of the panel,\n exactly where the slot step's \"Select a date & time\" header\n sits; the form centers in the remaining space. */}\n <BackButton\n label=\"Back\"\n onClick={() => {\n setStep('slot')\n setBookingError(null)\n }}\n />\n <div className=\"flex flex-1 flex-col justify-center gap-[var(--spacing-system-md)]\">\n <p className=\"text-h4 text-ods-text-primary\">\n {new Intl.DateTimeFormat(undefined, {\n timeZone: timezone,\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n }).format(new Date(selectedSlot))}{' '}\n · {formatDurationCompact(durationMs / 1000)}\n </p>\n {bookingError && bookingError !== 'SLOT_TAKEN' && (\n <div className=\"flex flex-col items-start gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h6 text-ods-error\">\n {bookingError === 'TEMPORARILY_UNAVAILABLE'\n ? 'Scheduling is briefly unavailable — please try again in a minute.'\n : bookingError === 'MEETING_UNAVAILABLE'\n ? 'This meeting type has reached its booking limit for today — try another time or contact us.'\n : bookingError === 'LINK_GONE'\n ? 'This meeting type is no longer available.'\n : 'Please double-check your details and try again.'}\n </p>\n {escapeHatch}\n </div>\n )}\n <BookingForm\n availability={availability}\n meetingId={meetingId}\n startTimeMs={selectedSlot}\n durationMs={durationMs}\n timezone={timezone}\n isSubmitting={isSubmitting}\n onSubmit={handleSubmit}\n honeypotInputProps={honeypotInputProps}\n getSignals={getSignals}\n />\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-[var(--spacing-system-md)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Select a date & time</p>\n {bookingError === 'SLOT_TAKEN' && (\n <p className=\"text-h6 text-ods-error\">\n That time is no longer available — if you just submitted, check your email for a confirmation\n before rebooking.\n </p>\n )}\n {timezone && durationMs != null ? (\n slots.length > 0 || isLoadingAvailability ? (\n <SlotPicker\n slots={slots}\n timezone={timezone}\n monthOffset={monthOffset}\n onMonthOffsetChange={(o) => {\n setSelectedDay(null)\n setSelectedSlot(null)\n setMonthOffset(o)\n }}\n selectedSlot={selectedSlot}\n onSelectSlot={(ms) => {\n setSelectedSlot(ms)\n setStep('details')\n }}\n selectedDay={selectedDay}\n onSelectDay={setSelectedDay}\n isLoading={isLoadingAvailability}\n />\n ) : (\n <div className=\"flex flex-col items-start gap-[var(--spacing-system-md)]\">\n <p className=\"text-h6 text-ods-text-secondary\">\n {isLoadingAvailability ? 'Checking more dates…' : 'No call times are published right now.'}\n </p>\n {!isLoadingAvailability && escapeHatch}\n </div>\n )\n ) : (\n // Pre-mount timezone-resolution window (every SSR load hits\n // this for one frame-batch) — SAME per-region skeletons, no\n // bare rectangle, no shift when the real picker swaps in.\n <SlotPickerSkeleton />\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n}\n\nexport {\n MeetingSchedulerDirectory,\n MeetingSchedulerDirectoryRowSkeleton,\n type MeetingSchedulerDirectoryProps,\n} from './directory'\nexport type { MeetingAvailability, BookingConfirmation, MeetingBookingErrorCode, MeetingHost }\nexport type { SchedulingLink, SchedulingLinksPayload } from '../../schemas/meeting-booking-schema'\n","'use client'\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { contentFetch } from '../utils/embed-content-fetch'\nimport { MAX_MONTH_OFFSET } from '../utils/hubspot-meetings-convention'\nimport type {\n BookingConfirmation,\n MeetingAvailability,\n MeetingBookingErrorCode,\n} from '../schemas/meeting-booking-schema'\n\n/**\n * useMeetingBooking — data layer for `<HubSpotMeetingScheduler>` (the widget\n * files never inline fetch; same split as `useContactSubmission`/ContactForm).\n *\n * Availability: plain `useState` + `useEffect` + `AbortController` self-fetch\n * (NO client cache layer). An `initialAvailability` seed (SSR mode) renders\n * immediately, but the hook REFETCHES UNCONDITIONALLY after mount — the seed\n * is first-paint scaffolding for a 60-second-volatile resource, not truth.\n *\n * Booking: single POST guarded by `isSubmitting` (the only double-booking\n * guard — the transport never retries POST). Errors surface as typed\n * `MeetingBookingErrorCode`s the widget keys its recovery UI off.\n *\n * All requests go through the host proxy (`apiBaseUrl` prefix, FaqSection\n * precedent) via `contentFetch`, so embedded hosts with an embed-auth adapter\n * inherit auth with zero extra wiring.\n */\n\nexport interface BookingResult {\n ok: boolean\n confirmation?: BookingConfirmation\n code?: MeetingBookingErrorCode\n message?: string\n}\n\nconst KNOWN_CODES: MeetingBookingErrorCode[] = [\n 'SLOT_TAKEN',\n 'VALIDATION',\n 'LINK_GONE',\n 'TEMPORARILY_UNAVAILABLE',\n 'MEETING_UNAVAILABLE',\n]\n\nexport function useMeetingBooking(options: {\n meetingId: string\n apiBaseUrl?: string\n initialAvailability?: MeetingAvailability\n}) {\n const { meetingId, apiBaseUrl = '', initialAvailability } = options\n const seededFor = initialAvailability?.meetingId === meetingId ? initialAvailability : undefined\n\n const [availability, setAvailability] = useState<MeetingAvailability | null>(seededFor ?? null)\n const [monthOffset, setMonthOffsetState] = useState(seededFor?.monthOffset ?? 0)\n const [isLoadingAvailability, setIsLoadingAvailability] = useState(!seededFor)\n const [availabilityError, setAvailabilityError] = useState<string | null>(null)\n const [isSubmitting, setIsSubmitting] = useState(false)\n const submittingRef = useRef(false)\n\n const setMonthOffset = useCallback((offset: number) => {\n setMonthOffsetState(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)))\n }, [])\n\n // Link switch resets paging — without this, the new link is fetched at the\n // PREVIOUS link's month. Layout effect so the fetch effect below sees the\n // reset offset in the same commit.\n const prevMeetingIdRef = useRef(meetingId)\n useLayoutEffect(() => {\n if (prevMeetingIdRef.current === meetingId) return\n prevMeetingIdRef.current = meetingId\n setMonthOffsetState(0)\n }, [meetingId])\n\n // Unconditional (re)fetch on mount and on (meetingId, monthOffset) change —\n // even when SSR-seeded (the seed may already be stale).\n useEffect(() => {\n const controller = new AbortController()\n let active = true\n const load = async () => {\n // Same-month refresh (the unconditional post-mount refetch of an SSR\n // seed) keeps the grid on screen; a DIFFERENT month or link has nothing\n // truthful to show — loading turns on so the widget renders its\n // per-region skeletons instead of a stale grid under a new caption.\n setAvailabilityError(null)\n if (!availability || availability.meetingId !== meetingId || availability.monthOffset !== monthOffset) {\n setIsLoadingAvailability(true)\n }\n try {\n const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) })\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, {\n signal: controller.signal,\n })\n if (!res.ok) throw new Error(`availability ${res.status}`)\n const data = (await res.json()) as MeetingAvailability\n if (active) setAvailability(data)\n } catch (err) {\n if (!active || (err instanceof DOMException && err.name === 'AbortError')) return\n setAvailabilityError(err instanceof Error ? err.message : 'Failed to load availability')\n } finally {\n if (active) setIsLoadingAvailability(false)\n }\n }\n void load()\n return () => {\n active = false\n controller.abort()\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- refetch is keyed on (meetingId, monthOffset) only; `availability` is read for skeleton gating, not as a trigger\n }, [meetingId, monthOffset, apiBaseUrl])\n\n const refetchAvailability = useCallback(async () => {\n try {\n const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) })\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`)\n if (res.ok) setAvailability((await res.json()) as MeetingAvailability)\n } catch {\n /* recovery refetch is best-effort — the stale grid stays visible */\n }\n }, [meetingId, monthOffset, apiBaseUrl])\n\n const book = useCallback(\n async (payload: Record<string, unknown>): Promise<BookingResult> => {\n if (submittingRef.current) return { ok: false, code: 'VALIDATION', message: 'Already submitting' }\n submittingRef.current = true\n setIsSubmitting(true)\n try {\n const res = await contentFetch(`${apiBaseUrl}/api/meetings/book`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const data = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (res.ok) return { ok: true, confirmation: data as unknown as BookingConfirmation }\n const rawCode = typeof data.code === 'string' ? data.code : ''\n const code = (KNOWN_CODES as string[]).includes(rawCode)\n ? (rawCode as MeetingBookingErrorCode)\n : 'TEMPORARILY_UNAVAILABLE'\n return { ok: false, code, message: typeof data.error === 'string' ? data.error : undefined }\n } catch {\n return { ok: false, code: 'TEMPORARILY_UNAVAILABLE' }\n } finally {\n submittingRef.current = false\n setIsSubmitting(false)\n }\n },\n [apiBaseUrl],\n )\n\n return {\n availability,\n isLoadingAvailability,\n availabilityError,\n monthOffset,\n setMonthOffset,\n refetchAvailability,\n book,\n isSubmitting,\n } as const\n}\n","import { z } from 'zod'\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n meetingId: string\n monthOffset: number\n hasMore: boolean\n /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n durationsMs: number[]\n /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n slotsByDurationMs: Record<string, number[]>\n formFields: MeetingFormField[]\n /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n legalConsent: MeetingLegalConsent | null\n /**\n * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n * builds from its own people data (e.g. a profiles table matched\n * server-side). NEVER carries emails or busy-time data; optional so\n * existing hosts stay wire-compatible.\n */\n hosts?: MeetingHost[]\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n name: string\n avatarUrl: string | null\n /** Job title / role line under the name (null → omitted). */\n title: string | null\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n id: string\n /** The link's public HubSpot booking URL — escape-hatch target only. */\n link: string\n /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n slug: string\n /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n purpose: string\n title: string\n description: string | null\n kind: 'personal' | 'team'\n /** Display-only minutes projection (booking stays ms end-to-end). */\n durationsMinutes: number[]\n hosts: MeetingHost[]\n /** Earliest bookable slot (epoch ms) from the current-month payload. */\n nextAvailableMs: number | null\n}\n\nexport interface SchedulingLinksPayload {\n purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>\n fetchedAt: string\n}\n\nexport interface MeetingFormField {\n name: string\n label: string\n type: string\n required: boolean\n options?: string[]\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n processingConsentText: string\n processingConsentCheckboxLabel: string | null\n communicationConsentText: string | null\n communicationConsentCheckboxes: Array<{\n communicationTypeId: string\n label: string\n required: boolean\n }>\n privacyPolicyText: string | null\n isLegitimateInterest: boolean\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n meetingId: string\n title: string\n startTimeMs: number\n durationMs: number\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream.\n */\nexport type MeetingBookingErrorCode =\n | 'SLOT_TAKEN'\n | 'VALIDATION'\n | 'LINK_GONE'\n | 'TEMPORARILY_UNAVAILABLE'\n | 'MEETING_UNAVAILABLE'\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * HubSpot custom-question types the native form supports. The widget's\n * renderer switches over THIS set and `makeBookingSchema` maps over THIS set;\n * fail-closed = a field whose `type` is not in the set (the widget then\n * renders the \"Open in HubSpot\" escape hatch for that link instead of a\n * half-working native form). Exact upstream type strings are pinned against\n * the rollout fixture link — extend here (renderer + validator move together).\n */\nexport const SUPPORTED_FORM_FIELD_TYPES = ['text', 'textarea', 'select', 'radio', 'checkbox'] as const\nexport type SupportedFormFieldType = (typeof SUPPORTED_FORM_FIELD_TYPES)[number]\n\nexport function isSupportedFormField(field: MeetingFormField): boolean {\n return (SUPPORTED_FORM_FIELD_TYPES as readonly string[]).includes(field.type)\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+\\-]+)+)$/\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n if (!IANA_TZ_RE.test(tz)) return false\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz })\n return true\n } catch {\n return false\n }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n try {\n return Intl.getCanonicalLocales(locale).length > 0\n } catch {\n return false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nexport function makeBookingSchema(\n formFields: MeetingFormField[],\n legalConsent: MeetingLegalConsent | null,\n) {\n const answers: Record<string, z.ZodTypeAny> = {}\n for (const field of formFields) {\n if (!isSupportedFormField(field)) continue // unsupported types are fail-closed at render time\n let validator: z.ZodTypeAny\n switch (field.type as SupportedFormFieldType) {\n case 'checkbox':\n validator = z.boolean()\n break\n case 'select':\n case 'radio':\n validator = z\n .string()\n .refine((v) => !v || (field.options ?? []).includes(v), {\n message: `Please choose a valid option for ${field.label}`,\n })\n break\n case 'textarea':\n validator = z.string().max(5000, { message: `${field.label} is too long` })\n break\n case 'text':\n default:\n validator = z.string().max(1000, { message: `${field.label} is too long` })\n break\n }\n if (field.required) {\n validator =\n field.type === 'checkbox'\n ? z.literal(true, { message: `${field.label} is required` })\n : (validator as z.ZodString).min(1, { message: `${field.label} is required` })\n } else if (field.type !== 'checkbox') {\n validator = (validator as z.ZodString).optional().or(z.literal(''))\n } else {\n validator = z.boolean().optional()\n }\n answers[field.name] = validator\n }\n\n const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n .filter((c) => c.required)\n .map((c) => c.communicationTypeId)\n\n // A required answer cannot be enforced by an `.optional()` parent object —\n // omitting the `formFields` key entirely would skip every per-question\n // rule. When the link declares at least one required supported question,\n // the object itself is required.\n const hasRequiredAnswers = formFields.some((f) => isSupportedFormField(f) && f.required)\n const answersObject = z.object(answers)\n\n return z\n .object({\n meetingId: z.string().min(1),\n startTimeMs: z.number().int().positive(),\n durationMs: z.number().int().positive(),\n firstName: z.string().min(1, { message: 'First name is required' }).max(255),\n lastName: z.string().min(1, { message: 'Last name is required' }).max(255),\n email: z.string().email({ message: 'Please enter a valid email address' }).max(255),\n timezone: z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' }),\n locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n // Plain `.optional()` (no `.default()`) so zod's input and output types\n // match — react-hook-form's zodResolver needs them identical, and the\n // server handles `undefined` explicitly anyway. REQUIRED when the link\n // declares required questions (see `hasRequiredAnswers` above).\n formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n legalConsentResponses: z\n .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n .optional(),\n })\n // Object-level rule: `.refine` on an `.optional()` field is skipped when\n // the field is absent — required consents must reject even on a payload\n // that omits the array entirely.\n .superRefine((data, ctx) => {\n const responses = data.legalConsentResponses ?? []\n const ok = requiredConsentIds.every((id) =>\n responses.some((r) => r.communicationTypeId === id && r.consented === true),\n )\n if (!ok) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['legalConsentResponses'],\n message: 'Required consent checkboxes must be accepted',\n })\n }\n })\n}\n\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>\n","'use client'\n\nimport { useMemo } from 'react'\nimport { SquareAvatar, AvatarStack, Button, Skeleton, Autocomplete } from '../ui'\nimport { ClockIcon } from '../icons-v2-generated'\nimport { cn } from '../../utils/cn'\nimport { formatDurationCompact } from '../../utils/format'\nimport type { MeetingHost } from '../../schemas/meeting-booking-schema'\n\n/**\n * ContextPanel — the \"who / what / how long\" side of the scheduler card\n * (Calendly-anatomy left panel; stacks on top on mobile). Everything here is\n * trust surface: host identity (avatar + name + title), the meeting's own\n * title/description, the duration (chips when the link offers several — the\n * duration choice lives HERE, not as a separate wizard step), and a\n * SEARCHABLE timezone picker (all IANA zones with live GMT offsets;\n * rendering-only — the wire is always epoch-ms).\n *\n * The picker renders only once the parent resolves the zone post-mount — SSR\n * output stays deterministic; a same-footprint skeleton holds the space.\n */\n\nexport interface SchedulerContextPanelProps {\n hosts: MeetingHost[]\n title?: string\n description?: string | null\n durationsMs: number[]\n selectedDurationMs: number | null\n onSelectDuration: (ms: number) => void\n /** Resolved IANA zone (null until the client resolves it). */\n timezone: string | null\n onTimezoneChange?: (tz: string) => void\n /** True once a slot is chosen (details/confirmed steps) — the duration and\n * timezone SELECTORS disappear entirely (the step summary carries both; a\n * disabled picker next to a form just reads as noise) and a static\n * duration line takes their place. Going Back restores them. */\n locked?: boolean\n className?: string\n}\n\nfunction zoneLabel(tz: string): string {\n try {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone: tz,\n timeZoneName: 'shortOffset',\n }).formatToParts(new Date())\n const offset = parts.find((p) => p.type === 'timeZoneName')?.value ?? ''\n return offset ? `${tz.replace(/_/g, ' ')} (${offset})` : tz.replace(/_/g, ' ')\n } catch {\n return tz.replace(/_/g, ' ')\n }\n}\n\n/**\n * Same-footprint skeleton — swaps with the loaded panel with zero shift.\n * STATIC labels (\"Duration\", \"Timezone\") render REAL; only data-driven\n * content (host identity, duration chips, the zone value) is skeleton.\n */\nexport function ContextPanelSkeleton({ className }: { className?: string }) {\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-mf)]', className)}>\n <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n <Skeleton className=\"h-12 w-12 rounded-full shrink-0\" />\n <div className=\"flex flex-1 flex-col gap-[var(--spacing-system-xxs)]\">\n <Skeleton className=\"h-5 w-32\" />\n <Skeleton className=\"h-4 w-24\" />\n </div>\n </div>\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Duration</p>\n <div className=\"flex gap-[var(--spacing-system-xs)]\">\n <Skeleton className=\"h-9 w-20\" />\n <Skeleton className=\"h-9 w-20\" />\n </div>\n </div>\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Timezone</p>\n <Skeleton className=\"h-12 w-full\" />\n </div>\n </div>\n )\n}\n\nexport function SchedulerContextPanel({\n hosts,\n title,\n description,\n durationsMs,\n selectedDurationMs,\n onSelectDuration,\n timezone,\n onTimezoneChange,\n locked = false,\n className,\n}: SchedulerContextPanelProps) {\n // All IANA zones with live GMT offsets — computed once, client-only (the\n // panel renders the picker only after the parent resolves a zone).\n const zoneOptions = useMemo(() => {\n if (!timezone) return []\n let zones: string[]\n try {\n // Older lib targets don't type supportedValuesOf (ES2022) — runtime-guarded.\n const intl = Intl as typeof Intl & { supportedValuesOf?: (key: string) => string[] }\n zones = intl.supportedValuesOf ? intl.supportedValuesOf('timeZone') : [timezone]\n } catch {\n zones = [timezone]\n }\n if (!zones.includes(timezone)) zones = [timezone, ...zones]\n return zones.map((tz) => ({ value: tz, label: zoneLabel(tz) }))\n }, [timezone])\n\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-mf)]', className)}>\n {/* Host identity: full rows up to 3 hosts; larger teams (round-robin\n links can carry many members) collapse to a stacked-avatar cluster\n + count so the panel can never overflow. */}\n {hosts.length > 0 && hosts.length <= 3 && (\n <div className=\"flex flex-col gap-[var(--spacing-system-s)]\">\n {hosts.map((host) => (\n <div key={host.name} className=\"flex items-center gap-[var(--spacing-system-s)]\">\n <SquareAvatar variant=\"round\" size=\"lg\" src={host.avatarUrl ?? undefined} alt={host.name} fallback={host.name} />\n <div className=\"flex flex-col min-w-0\">\n <p className=\"text-h4 text-ods-text-primary truncate\">{host.name}</p>\n {host.title && <p className=\"text-h6 text-ods-text-secondary truncate\">{host.title}</p>}\n </div>\n </div>\n ))}\n </div>\n )}\n {hosts.length > 3 && (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <AvatarStack people={hosts} max={4} size=\"lg\" />\n <p className=\"text-h6 text-ods-text-secondary\">{hosts.length} hosts on this calendar</p>\n </div>\n )}\n\n {title && <p className=\"text-h3 text-ods-text-primary\">{title}</p>}\n {description && <p className=\"text-h6 text-ods-text-secondary\">{description}</p>}\n\n {locked ? (\n // Post-selection: selectors are GONE, not disabled — the step summary\n // owns the chosen time/zone; the panel keeps a single static line.\n selectedDurationMs != null && (\n <p className=\"text-h6 text-ods-text-secondary\">{formatDurationCompact(selectedDurationMs / 1000)} call</p>\n )\n ) : (\n <>\n {durationsMs.length > 1 ? (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Duration</p>\n <div className=\"flex flex-wrap gap-[var(--spacing-system-xs)]\">\n {durationsMs.map((ms) => (\n <Button\n key={ms}\n variant={selectedDurationMs === ms ? undefined : 'outline'}\n size=\"small-legacy\"\n onClick={() => onSelectDuration(ms)}\n >\n {formatDurationCompact(ms / 1000)}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n selectedDurationMs != null && (\n <p className=\"text-h6 text-ods-text-secondary\">{formatDurationCompact(selectedDurationMs / 1000)} call</p>\n )\n )}\n\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n <p className=\"text-h5 text-ods-text-secondary\">Timezone</p>\n {timezone ? (\n <Autocomplete\n value={timezone}\n onChange={(tz) => {\n if (tz) onTimezoneChange?.(tz)\n }}\n options={zoneOptions}\n placeholder=\"Search timezone…\"\n startAdornment={<ClockIcon className=\"size-4 shrink-0 text-ods-text-secondary\" />}\n noOptionsText=\"No matching timezone\"\n showClearAll={false}\n />\n ) : (\n <Skeleton className=\"h-12 w-full\" />\n )}\n </div>\n </>\n )}\n </div>\n )\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { Calendar } from '../calendar'\nimport { Button, Skeleton } from '../ui'\nimport { cn } from '../../utils/cn'\nimport { MAX_MONTH_OFFSET } from '../../utils/hubspot-meetings-convention'\n\n/**\n * SlotPicker — day layer + time-chip grid for the meeting scheduler.\n *\n * Day layer is the lib's ODS-styled `Calendar` (react-day-picker v9\n * wrapper) — NOT a bespoke day grid. `startMonth`/`endMonth` pin navigation\n * to [current month, current + MAX_MONTH_OFFSET] so the past is unreachable;\n * days without bookable slots are disabled (visually muted via the v9\n * `disabled` classNames). Month navigation drives the host-proxy\n * `monthOffset`. Time chips are plain `Button`s whose variant flips on\n * selection — no bespoke pill.\n *\n * LAZY STATES (no jumps, ever): each region owns a same-footprint skeleton —\n * `CalendarSkeleton` (caption + weekday row + 6×7 day grid) and\n * `TimeChipsSkeleton` (day heading + chip grid). During an availability\n * refetch (month change) BOTH regions swap to their skeletons; the region\n * containers carry fixed min-heights so loading ⇄ loaded never shifts layout.\n *\n * All slot instants are epoch-ms; every label is rendered in `timezone`\n * (resolved by the parent AFTER mount — this component never reads Intl\n * itself, keeping SSR output deterministic).\n */\n\nexport interface SlotPickerProps {\n /** Bookable slot start times (epoch ms) for the selected duration. */\n slots: number[]\n /** IANA zone every label renders in (parent resolves post-mount). */\n timezone: string\n monthOffset: number\n onMonthOffsetChange: (offset: number) => void\n selectedSlot: number | null\n onSelectSlot: (startMs: number) => void\n selectedDay: string | null\n onSelectDay: (dayKey: string) => void\n /** Availability refetch in flight (month change) → per-region skeletons. */\n isLoading?: boolean\n}\n\n/** Stable per-zone day key for an instant, e.g. \"2026-08-14\" (exported — the\n * parent uses it to auto-select the first available day). */\nexport function dayKeyInZone(ms: number, timeZone: string): string {\n return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(\n new Date(ms),\n )\n}\n\n/**\n * Calendar-side day-key anchoring: react-day-picker exchanges PLAIN calendar\n * dates (local Date objects whose y/m/d ARE the cell). Convert via local\n * date parts — never through an Intl zone — or a `displayTimezone` far from\n * the browser zone shifts cells by a day.\n */\nconst pad2 = (n: number) => String(n).padStart(2, '0')\nfunction calendarDayKey(day: Date): string {\n return `${day.getFullYear()}-${pad2(day.getMonth() + 1)}-${pad2(day.getDate())}`\n}\nfunction dateFromDayKey(key: string): Date {\n const [y, m, d] = key.split('-').map(Number)\n return new Date(y, m - 1, d, 12)\n}\n\nfunction timeLabelInZone(ms: number, timeZone: string): string {\n return new Intl.DateTimeFormat(undefined, { timeZone, hour: 'numeric', minute: '2-digit' }).format(new Date(ms))\n}\n\n/** Calendar region min-height: caption (32) + gap (≤12) + table with 6 week\n * rows (8 outer border-spacing + 7×36 rows + 6×4 row gaps = 284) = 328px.\n * Sized to the TALLEST month so 5-row months, 6-row months, skeletons and\n * loaded states all occupy identical space — measured live, not estimated. */\nconst CALENDAR_MIN_H = 'min-h-[20.5rem]'\n\nconst WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']\n\n/** Deterministic month caption for a given offset (fixed en-US locale so\n * the SSR and client first-paint markup agree byte-for-byte). */\nexport function monthLabelFor(offset: number): string {\n const now = new Date()\n const month = new Date(now.getFullYear(), now.getMonth() + offset, 1)\n return new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(month)\n}\n\n/**\n * Same-footprint skeleton for the day-selection calendar. STATIC chrome —\n * month caption, BOXED nav chevrons, weekday labels — renders REAL (it needs\n * no data); only the day buttons are skeleton cells.\n *\n * GEOMETRY IS EXACT, not approximate — it mirrors the v9 Calendar's real\n * classes: caption row h-8 with h-8/w-8 bordered nav boxes; the grid has NO\n * horizontal gaps (the real table is `border-separate border-spacing-y-1` —\n * vertical spacing only, so cells touch horizontally) and `py-1` reproduces\n * the table's outer border-spacing edges. Any deviation here shifts the\n * times column on skeleton ⇄ loaded swap.\n */\nexport function CalendarSkeleton({ monthOffset = 0 }: { monthOffset?: number }) {\n return (\n <div className={cn('flex flex-col gap-[var(--spacing-system-s)]', CALENDAR_MIN_H)}>\n <div className=\"relative h-8\">\n <div className=\"absolute inset-x-0 top-0 flex h-8 items-center justify-between\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60\">\n <ChevronLeft className=\"h-4 w-4 text-ods-text-muted\" />\n </span>\n <span className=\"flex h-8 w-8 items-center justify-center rounded-md border border-ods-border opacity-60\">\n <ChevronRight className=\"h-4 w-4 text-ods-text-muted\" />\n </span>\n </div>\n <div className=\"flex h-8 items-center justify-center\">\n <p className=\"text-h6 text-ods-text-primary\">{monthLabelFor(monthOffset)}</p>\n </div>\n </div>\n {/* Cells are NOT fixed-width: the real table stretches to its\n container (columns share the extra width, buttons centered) — the\n skeleton's grid tracks do exactly the same. */}\n <div className=\"flex flex-col gap-y-1 py-1\">\n <div className=\"grid grid-cols-7\">\n {WEEKDAY_LABELS.map((d) => (\n <div key={d} className=\"flex h-9 items-center justify-center text-h6 font-normal text-ods-text-secondary\">\n {d}\n </div>\n ))}\n </div>\n {Array.from({ length: 6 }, (_, row) => (\n <div key={`w-${row}`} className=\"grid grid-cols-7\">\n {Array.from({ length: 7 }, (_, col) => (\n <div key={col} className=\"flex h-9 items-center justify-center\">\n <Skeleton className=\"h-9 w-9 rounded-md\" />\n </div>\n ))}\n </div>\n ))}\n </div>\n </div>\n )\n}\n\n/**\n * The full slot-area skeleton — calendar + time-chip regions side by side in\n * the SAME flex wrappers as the loaded `SlotPicker`. This is the ONLY\n * loading visual for the slot area, used by every path that has nothing to\n * show yet (initial availability fetch, the pre-mount timezone-resolution\n * window, month-change refetch) — never a bare rectangle.\n */\nexport function SlotPickerSkeleton({ monthOffset = 0 }: { monthOffset?: number }) {\n return (\n <div className=\"flex flex-col md:flex-row gap-[var(--spacing-system-lf)]\">\n <div className={cn('shrink-0', CALENDAR_MIN_H)}>\n <CalendarSkeleton monthOffset={monthOffset} />\n </div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]\">\n <TimeChipsSkeleton />\n </div>\n </div>\n )\n}\n\n/** Same-footprint skeleton for the time-chip column. */\nexport function TimeChipsSkeleton() {\n return (\n <>\n {/* Day-heading line: text-h6 is 16px tall on mobile, 20px from md up. */}\n <Skeleton className=\"h-4 md:h-5 w-40\" />\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start\">\n {Array.from({ length: 9 }, (_, i) => (\n <Skeleton key={i} className=\"h-10 w-full\" />\n ))}\n </div>\n </>\n )\n}\n\nexport function SlotPicker({\n slots,\n timezone,\n monthOffset,\n onMonthOffsetChange,\n selectedSlot,\n onSelectSlot,\n selectedDay,\n onSelectDay,\n isLoading = false,\n}: SlotPickerProps) {\n const slotsByDay = useMemo(() => {\n const map = new Map<string, number[]>()\n for (const ms of slots) {\n const key = dayKeyInZone(ms, timezone)\n const list = map.get(key) ?? []\n list.push(ms)\n map.set(key, list)\n }\n for (const list of map.values()) list.sort((a, b) => a - b)\n return map\n }, [slots, timezone])\n\n const now = new Date()\n const startMonth = new Date(now.getFullYear(), now.getMonth(), 1)\n const endMonth = new Date(now.getFullYear(), now.getMonth() + MAX_MONTH_OFFSET, 1)\n const visibleMonth = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1)\n\n const daySlots = selectedDay ? (slotsByDay.get(selectedDay) ?? []) : []\n\n return (\n <div className=\"flex flex-col md:flex-row gap-[var(--spacing-system-lf)]\">\n <div className={cn('shrink-0', CALENDAR_MIN_H)}>\n {isLoading ? (\n <CalendarSkeleton monthOffset={monthOffset} />\n ) : (\n <Calendar\n month={visibleMonth}\n startMonth={startMonth}\n endMonth={endMonth}\n onMonthChange={(month) => {\n const offset = (month.getFullYear() - now.getFullYear()) * 12 + (month.getMonth() - now.getMonth())\n onMonthOffsetChange(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)))\n }}\n mode=\"single\"\n required\n // CONTROLLED selection: v9 needs BOTH `selected` AND `onSelect` —\n // with `selected` alone it goes uncontrolled (internal state\n // seeded at mount), silently ignoring the parent's auto-selected\n // first day. `required` disables click-to-deselect.\n selected={selectedDay ? dateFromDayKey(selectedDay) : undefined}\n onSelect={(day) => {\n if (!day) return\n const key = calendarDayKey(day)\n if (slotsByDay.has(key)) onSelectDay(key)\n }}\n disabled={(day) => !slotsByDay.has(calendarDayKey(day))}\n className=\"p-0\"\n />\n )}\n </div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-system-s)]\">\n {isLoading ? (\n <TimeChipsSkeleton />\n ) : selectedDay ? (\n <>\n <p className=\"text-h6 text-ods-text-primary\">\n {/* Format the DAY-KEY itself (plain calendar date) — re-zoning a\n local instant could label an adjacent day. */}\n {new Intl.DateTimeFormat(undefined, {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n }).format(dateFromDayKey(selectedDay))}\n </p>\n <div className=\"grid grid-cols-2 sm:grid-cols-3 gap-[var(--spacing-system-xs)] content-start max-h-80 overflow-y-auto\">\n {daySlots.map((ms) => {\n const isSelected = selectedSlot === ms\n return (\n <Button\n key={ms}\n variant={isSelected ? undefined : 'outline'}\n size=\"small-legacy\"\n onClick={() => onSelectSlot(ms)}\n >\n {timeLabelInZone(ms, timezone)}\n </Button>\n )\n })}\n </div>\n </>\n ) : (\n <p className=\"text-h6 text-ods-text-secondary\">\n {[...slotsByDay.keys()].some((k) =>\n k.startsWith(\n `${visibleMonth.getFullYear()}-${String(visibleMonth.getMonth() + 1).padStart(2, '0')}`,\n ),\n )\n ? 'Pick a day to see available times.'\n : 'No available times this month — try the next one.'}\n </p>\n )}\n </div>\n </div>\n )\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { useForm, Controller } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport {\n makeBookingSchema,\n isSupportedFormField,\n type MeetingAvailability,\n type MeetingBookingPayload,\n} from '../../schemas/meeting-booking-schema'\nimport {\n Button,\n Input,\n Textarea,\n Label,\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n Checkbox,\n RadioGroup,\n RadioGroupItem,\n} from '../ui'\nimport { HoneypotField } from '../ui/honeypot-field'\n\n/**\n * BookingForm — attendee details + the link's declared custom questions +\n * verbatim legal-consent copy. ContactForm's scaffolding (react-hook-form +\n * zodResolver + lib field primitives); a sibling rather than a `<ContactForm>`\n * configuration because of the dynamic HubSpot `formFields`, the per-checkbox\n * consent model, and the first/last-name field model — none expressible via\n * `hideFields`/`extraTopField`.\n *\n * Bot protection is LOAD-BEARING: without the humanity signals in the body,\n * the host's `verifyHuman` degrades to first-party-only BotID (fails open for\n * external embedders). Honeypot + elapsed-ms are merged into the POST at\n * submit; the parent calls `resetSignals()` after a SLOT_TAKEN refetch so a\n * legitimate retry isn't flagged too-fast.\n */\n\nexport interface BookingFormProps {\n availability: MeetingAvailability\n meetingId: string\n startTimeMs: number\n durationMs: number\n /** IANA zone the confirmation/invite should render in (parent-resolved). */\n timezone: string\n isSubmitting: boolean\n onSubmit: (payload: Record<string, unknown>) => Promise<void>\n /** From useHumanitySignals — parent owns the instance so it can resetSignals(). */\n honeypotInputProps: { ref: React.Ref<HTMLInputElement>; name: string }\n getSignals: () => Record<string, string | number>\n}\n\nexport function BookingForm({\n availability,\n meetingId,\n startTimeMs,\n durationMs,\n timezone,\n isSubmitting,\n onSubmit,\n honeypotInputProps,\n getSignals,\n}: BookingFormProps) {\n const { formFields, legalConsent } = availability\n const supportedFields = useMemo(() => formFields.filter(isSupportedFormField), [formFields])\n const schema = useMemo(() => makeBookingSchema(supportedFields, legalConsent), [supportedFields, legalConsent])\n\n const {\n register,\n control,\n handleSubmit,\n formState: { errors },\n } = useForm<MeetingBookingPayload>({\n resolver: zodResolver(schema),\n defaultValues: {\n meetingId,\n startTimeMs,\n durationMs,\n timezone,\n locale: typeof navigator !== 'undefined' ? navigator.language : undefined,\n firstName: '',\n lastName: '',\n email: '',\n formFields: {},\n legalConsentResponses: (legalConsent?.communicationConsentCheckboxes ?? []).map((c) => ({\n communicationTypeId: c.communicationTypeId,\n consented: false,\n })),\n },\n })\n\n const submit = handleSubmit(async (data) => {\n await onSubmit({ ...data, meetingId, startTimeMs, durationMs, timezone, ...getSignals() })\n })\n\n const fieldError = (name: string): string | undefined => {\n const err = (errors.formFields as Record<string, { message?: string }> | undefined)?.[name]\n return err?.message\n }\n\n // Field chrome mirrors ContactForm 1:1 (`contact/contact-form.tsx`) — the\n // booking form must be indistinguishable from every other form in the app.\n const inputClass = 'bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12'\n\n return (\n <form onSubmit={submit} className=\"flex flex-col space-y-4 md:space-y-6\" noValidate>\n <HoneypotField {...honeypotInputProps} />\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6\">\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-first-name\">\n First name<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-first-name\"\n autoComplete=\"given-name\"\n placeholder=\"Jane\"\n aria-invalid={!!errors.firstName}\n className={inputClass}\n {...register('firstName')}\n />\n {errors.firstName && <span className=\"text-ods-error text-h6 mt-1\">{errors.firstName.message}</span>}\n </div>\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-last-name\">\n Last name<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-last-name\"\n autoComplete=\"family-name\"\n placeholder=\"Doe\"\n aria-invalid={!!errors.lastName}\n className={inputClass}\n {...register('lastName')}\n />\n {errors.lastName && <span className=\"text-ods-error text-h6 mt-1\">{errors.lastName.message}</span>}\n </div>\n </div>\n\n <div className=\"flex flex-col\">\n <Label htmlFor=\"ms-email\">\n Email<span className=\"text-ods-accent\">*</span>\n </Label>\n <Input\n id=\"ms-email\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"jane@company.com\"\n aria-invalid={!!errors.email}\n className={inputClass}\n {...register('email')}\n />\n {errors.email && <span className=\"text-ods-error text-h6 mt-1\">{errors.email.message}</span>}\n </div>\n\n {supportedFields.map((field) => (\n <div key={field.name} className=\"flex flex-col\">\n <Label htmlFor={`ms-q-${field.name}`}>\n {field.label}\n {field.required && <span className=\"text-ods-accent\">*</span>}\n </Label>\n {field.type === 'textarea' && (\n <Textarea\n id={`ms-q-${field.name}`}\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3\"\n {...register(`formFields.${field.name}` as never)}\n />\n )}\n {field.type === 'text' && (\n <Input\n id={`ms-q-${field.name}`}\n className=\"bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12\"\n {...register(`formFields.${field.name}` as never)}\n />\n )}\n {(field.type === 'select' || field.type === 'radio') && (\n <Controller\n control={control}\n name={`formFields.${field.name}` as never}\n render={({ field: rhf }) =>\n field.type === 'select' ? (\n <Select value={(rhf.value as string) ?? ''} onValueChange={rhf.onChange}>\n <SelectTrigger id={`ms-q-${field.name}`}>\n <SelectValue placeholder=\"Select…\" />\n </SelectTrigger>\n <SelectContent>\n {(field.options ?? []).map((opt) => (\n <SelectItem key={opt} value={opt}>\n {opt}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <RadioGroup value={(rhf.value as string) ?? ''} onValueChange={rhf.onChange}>\n {(field.options ?? []).map((opt) => (\n <div key={opt} className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <RadioGroupItem id={`ms-q-${field.name}-${opt}`} value={opt} />\n <Label htmlFor={`ms-q-${field.name}-${opt}`}>{opt}</Label>\n </div>\n ))}\n </RadioGroup>\n )\n }\n />\n )}\n {field.type === 'checkbox' && (\n <Controller\n control={control}\n name={`formFields.${field.name}` as never}\n render={({ field: rhf }) => (\n // The question's Label above already carries the text and\n // binds to this id — a second inline label would double the\n // visible text AND the accessible name.\n <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <Checkbox\n id={`ms-q-${field.name}`}\n checked={Boolean(rhf.value)}\n onCheckedChange={(v) => rhf.onChange(v === true)}\n />\n </div>\n )}\n />\n )}\n {fieldError(field.name) && (\n <span className=\"text-ods-error text-h6 mt-1\">{fieldError(field.name)}</span>\n )}\n </div>\n ))}\n\n {legalConsent && (\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-md)]\">\n {/* GDPR surface — HubSpot's consent copy rendered VERBATIM, never edited. */}\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.processingConsentText}</p>\n {legalConsent.communicationConsentText && (\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.communicationConsentText}</p>\n )}\n <Controller\n control={control}\n name=\"legalConsentResponses\"\n render={({ field: rhf }) => (\n <>\n {legalConsent.communicationConsentCheckboxes.map((box) => {\n const responses = (rhf.value ?? []) as Array<{ communicationTypeId: string; consented: boolean }>\n const current = responses.find((r) => r.communicationTypeId === box.communicationTypeId)\n return (\n <div key={box.communicationTypeId} className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n <Checkbox\n id={`ms-consent-${box.communicationTypeId}`}\n checked={current?.consented ?? false}\n onCheckedChange={(v) =>\n rhf.onChange(\n responses.map((r) =>\n r.communicationTypeId === box.communicationTypeId ? { ...r, consented: v === true } : r,\n ),\n )\n }\n />\n <Label htmlFor={`ms-consent-${box.communicationTypeId}`}>\n {box.label}\n {box.required ? ' *' : ''}\n </Label>\n </div>\n )\n })}\n </>\n )}\n />\n {legalConsent.privacyPolicyText && (\n <p className=\"text-h6 text-ods-text-secondary\">{legalConsent.privacyPolicyText}</p>\n )}\n {errors.legalConsentResponses && (\n <p className=\"text-h6 text-ods-error\">{errors.legalConsentResponses.message as string}</p>\n )}\n </div>\n )}\n\n {/* Step navigation back to the calendar lives in the step header (the\n app-standard BackButton, rendered by the parent) — the form ships\n only its submit. */}\n <div className=\"flex\">\n <Button type=\"submit\" loading={isSubmitting} disabled={isSubmitting} className=\"w-full md:w-auto\">\n Confirm booking\n </Button>\n </div>\n </form>\n )\n}\n","'use client'\n\nimport { formatDurationCompact } from '../../utils/format'\nimport type { BookingConfirmation } from '../../schemas/meeting-booking-schema'\n\n/**\n * Confirmation — terminal success state after a completed booking. Renders\n * the booked instant in the visitor's resolved zone. Deliberately NO\n * follow-up CTA: the job is done, the calendar invite is the next touchpoint\n * (a \"book another\" button here tested as noise).\n */\n\nexport interface ConfirmationProps {\n confirmation: BookingConfirmation\n timezone: string\n}\n\nexport function Confirmation({ confirmation, timezone }: ConfirmationProps) {\n const when = new Intl.DateTimeFormat(undefined, {\n timeZone: timezone,\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n }).format(new Date(confirmation.startTimeMs))\n\n return (\n <div className=\"flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-md)] text-center\">\n <p className=\"text-h1\" role=\"img\" aria-label=\"Celebration\">\n 🎉\n </p>\n <h3 className=\"text-h3 text-ods-text-primary\">You're booked</h3>\n <p className=\"text-h5 text-ods-text-primary\">{confirmation.title}</p>\n <p className=\"text-h6 text-ods-text-secondary\">\n {when} · {formatDurationCompact(confirmation.durationMs / 1000)}\n </p>\n <p className=\"text-h6 text-ods-text-secondary\">A calendar invite is on its way to your email.</p>\n </div>\n )\n}\n","'use client'\n\n/**\n * `<MeetingSchedulerDirectory />` — the embeddable scheduling DIRECTORY\n * block: clickable row cards (host avatars, title/description, For-<audience>\n * chip, next available time, boxed chevron) + the house\n * `PersistentPaginationWrapper`, fed by a host proxy's `GET\n * {apiBaseUrl}/api/meetings` (see `docs/EMBEDDING_HUBSPOT_MEETINGS.md`).\n * Rows navigate to `{bookingBasePath}/{slug}` — pair with\n * `<HubSpotMeetingScheduler>` on that page for the full white-label flow.\n *\n * Layout stability is engineered, not incidental: the rows area reserves one\n * FULL page (pageSize × 80px rows + gaps), the pagination slot holds its\n * measured 72px even when hidden, and the loading state renders same-shell\n * skeleton rows — so nothing below the block ever jumps across\n * loading ⇄ loaded ⇄ page changes ⇄ short last pages.\n *\n * SSR/host mode: pass `initialData` (host-fetched) to skip the client fetch.\n * Client/embed mode: omit it — skeleton page + self-fetch via `contentFetch`\n * (embed-auth adapters inherited for free).\n */\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { ChevronRight } from 'lucide-react'\nimport Link from '../../embed-shims/next-link'\nimport { contentFetch } from '../../utils/embed-content-fetch'\nimport { formatDurationCompact } from '../../utils/format'\nimport { cn } from '../../utils/cn'\nimport { AvatarStack, StatusBadge, Skeleton } from '../ui'\nimport { EmptyState } from '../empty-state'\nimport { PersistentPaginationWrapper } from '../persistent-pagination'\nimport type { SchedulingLink, SchedulingLinksPayload } from '../../schemas/meeting-booking-schema'\n\nexport interface MeetingSchedulerDirectoryProps {\n /** Endpoints prefix, default '' (same-origin `/api/meetings`). */\n apiBaseUrl?: string\n /** Host-fetched seed — skips the client fetch when provided. */\n initialData?: SchedulingLinksPayload | null\n /** `scope=all` — full-portal view (non-conforming links under \"other\"). */\n includeAll?: boolean\n /** Rows per page; also the skeleton row count and the reserved rows-area height. */\n pageSize?: number\n /** Rows link to `${bookingBasePath}/${slug}`. */\n bookingBasePath?: string\n /** EmptyState CTA target for error/empty states (omit → no CTA). */\n contactHref?: string\n className?: string\n}\n\n/** Zone-aware \"next available\" label — resolved POST-mount (SSR renders the\n * static caption only, so server and client first paint agree). */\nfunction useNextAvailableLabel(ms: number | null): string | null {\n const [label, setLabel] = useState<string | null>(null)\n useEffect(() => {\n if (ms == null) return\n try {\n setLabel(\n new Intl.DateTimeFormat(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }).format(new Date(ms)),\n )\n } catch {\n setLabel(null)\n }\n }, [ms])\n return label\n}\n\nfunction DirectoryRow({\n link,\n audienceLabel,\n bookingBasePath,\n}: {\n link: SchedulingLink\n audienceLabel: string\n bookingBasePath: string\n}) {\n const nextLabel = useNextAvailableLabel(link.nextAvailableMs)\n\n const subtitle = [\n link.description,\n link.durationsMinutes.length > 0 ? link.durationsMinutes.map((m) => formatDurationCompact(m * 60)).join(' / ') : null,\n link.kind === 'team' ? 'Team' : null,\n ]\n .filter(Boolean)\n .join(' • ')\n\n return (\n <Link\n href={`${bookingBasePath}/${link.slug}`}\n prefetch={false}\n className=\"block rounded-lg no-underline text-inherit focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent\"\n >\n <div className=\"flex w-full items-center gap-4 px-4 py-3 min-h-20 rounded-lg border border-ods-border bg-ods-card transition-colors duration-150 hover:bg-ods-bg-hover\">\n {/* Title leads at a fixed X; hosts live in the right meta cluster. */}\n <div className=\"flex min-w-0 flex-1 flex-col justify-center text-left\">\n <p className=\"text-h3 text-ods-text-primary truncate\">{link.title}</p>\n {subtitle && <p className=\"text-h6 text-ods-text-secondary truncate\">{subtitle}</p>}\n </div>\n\n {/* No audience entity (scope=all \"other\" group) → no chip. */}\n {audienceLabel && (\n <StatusBadge text={`For ${audienceLabel}`} singleLine className=\"shrink-0 hidden sm:inline-flex\" />\n )}\n\n {/* Hosts stay visible on mobile too — the facepile is the row's \"who\n you're booking with\" signal; only the chip and next-available meta\n collapse on narrow widths. */}\n <AvatarStack people={link.hosts} max={3} size=\"md\" className=\"flex shrink-0\" />\n\n <div className=\"hidden md:flex w-44 shrink-0 flex-col items-end justify-center\">\n <p className=\"text-h6 text-ods-text-secondary\">Next available</p>\n <p className=\"text-h6 text-ods-text-primary min-h-5\">\n {link.nextAvailableMs == null ? 'No times published' : (nextLabel ?? ' ')}\n </p>\n </div>\n\n {/* House boxed chevron (ChatTicketItem). */}\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-card\">\n <ChevronRight className=\"size-6 text-ods-text-secondary\" />\n </span>\n </div>\n </Link>\n )\n}\n\n/** Same-shell loading row (co-located-skeleton convention). */\nexport function MeetingSchedulerDirectoryRowSkeleton() {\n return (\n <div className=\"flex w-full items-center gap-4 px-4 py-3 min-h-20 rounded-lg border border-ods-border bg-ods-card\">\n <div className=\"flex min-w-0 flex-1 flex-col justify-center gap-1\">\n <Skeleton className=\"h-5 w-1/2\" />\n <Skeleton className=\"h-4 w-1/3\" />\n </div>\n <Skeleton className=\"hidden sm:block h-6 w-28 shrink-0\" />\n <Skeleton className=\"h-10 w-10 shrink-0 rounded-full\" />\n <div className=\"hidden md:flex w-44 shrink-0 flex-col items-end gap-1\">\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-5 w-32\" />\n </div>\n <Skeleton className=\"size-12 shrink-0 rounded-md\" />\n </div>\n )\n}\n\n/** Measured pagination-slot height (PersistentPaginationWrapper = 72px). */\nconst PAGINATION_SLOT_H = 'h-[4.5rem]'\n\nexport function MeetingSchedulerDirectory({\n apiBaseUrl = '',\n initialData = null,\n includeAll = false,\n pageSize = 6,\n bookingBasePath = '/schedule-a-call',\n contactHref,\n className,\n}: MeetingSchedulerDirectoryProps) {\n const [data, setData] = useState<SchedulingLinksPayload | null>(initialData)\n const [isLoading, setIsLoading] = useState(initialData === null)\n const [error, setError] = useState<string | null>(null)\n const [page, setPage] = useState(1)\n\n useEffect(() => {\n if (initialData !== null) return // host-seeded — skip the client fetch\n const controller = new AbortController()\n let active = true\n const load = async () => {\n setIsLoading(true)\n setError(null)\n try {\n const qs = includeAll ? '?scope=all' : ''\n const res = await contentFetch(`${apiBaseUrl}/api/meetings${qs}`, { signal: controller.signal })\n if (!res.ok) throw new Error(`meetings ${res.status}`)\n const payload = (await res.json()) as SchedulingLinksPayload\n if (active) setData(payload)\n } catch (err) {\n if (!active || (err instanceof DOMException && err.name === 'AbortError')) return\n setError(err instanceof Error ? err.message : 'Failed to fetch scheduling links')\n } finally {\n if (active) setIsLoading(false)\n }\n }\n void load()\n return () => {\n active = false\n controller.abort()\n }\n }, [initialData, includeAll, apiBaseUrl])\n\n const rows = useMemo(() => {\n const purposes = data?.purposes ?? []\n return purposes.flatMap((p) => p.links.map((link) => ({ link, audienceLabel: p.label })))\n }, [data])\n\n const totalPages = Math.max(1, Math.ceil(rows.length / pageSize))\n const pageRows = rows.slice((page - 1) * pageSize, page * pageSize)\n\n // Reserved rows-area height: pageSize × 80px rows + (pageSize−1) × 16px gaps.\n const rowsMinHeight = pageSize * 80 + (pageSize - 1) * 16\n\n if (isLoading && !data) {\n return (\n <div className={cn('flex flex-col', className)}>\n <div className=\"flex w-full flex-col gap-4\" style={{ minHeight: rowsMinHeight }}>\n {Array.from({ length: pageSize }, (_, i) => (\n <MeetingSchedulerDirectoryRowSkeleton key={i} />\n ))}\n </div>\n <div className={cn('mt-8 flex w-full items-center justify-center', PAGINATION_SLOT_H)}>\n <Skeleton className=\"h-10 w-64\" />\n </div>\n </div>\n )\n }\n\n if (error) {\n return (\n <div className={cn('flex flex-col justify-center', className)} style={{ minHeight: rowsMinHeight + 104 }}>\n <EmptyState\n type=\"generic\"\n title=\"We couldn't load available call times\"\n description=\"Something went wrong on our side. Try again in a minute, or reach us directly.\"\n {...(contactHref ? { ctaText: 'Contact us', ctaHref: contactHref } : {})}\n />\n </div>\n )\n }\n\n if (rows.length === 0) {\n return (\n <div className={cn('flex flex-col justify-center', className)} style={{ minHeight: rowsMinHeight + 104 }}>\n <EmptyState\n type=\"generic\"\n title=\"No call times are published right now\"\n description=\"We're updating our calendars. Reach us directly and we'll set something up.\"\n {...(contactHref ? { ctaText: 'Contact us', ctaHref: contactHref } : {})}\n />\n </div>\n )\n }\n\n return (\n <div className={cn('flex flex-col', className)}>\n <div className=\"flex w-full flex-col gap-4\" style={{ minHeight: rowsMinHeight }}>\n {pageRows.map(({ link, audienceLabel }) => (\n <DirectoryRow key={link.id} link={link} audienceLabel={audienceLabel} bookingBasePath={bookingBasePath} />\n ))}\n </div>\n {/* House pagination block — dims in place during refetches; the slot\n holds its measured height even at ≤1 page (where the inner\n UnifiedPagination returns null). */}\n <div className={cn('mt-8 flex w-full items-center justify-center', PAGINATION_SLOT_H)}>\n {totalPages > 1 && (\n <PersistentPaginationWrapper\n isLoading={isLoading}\n currentPage={page}\n totalPages={totalPages}\n onPageChange={setPage}\n variant=\"blog\"\n />\n )}\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -5,16 +5,15 @@ import {
|
|
|
5
5
|
} from "../../chunk-7J4DGFHZ.js";
|
|
6
6
|
import {
|
|
7
7
|
PersistentPaginationWrapper
|
|
8
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-6RTSYEBE.js";
|
|
9
9
|
import {
|
|
10
10
|
EmptyState
|
|
11
|
-
} from "../../chunk-
|
|
11
|
+
} from "../../chunk-XO4YU5GN.js";
|
|
12
12
|
import {
|
|
13
13
|
Autocomplete,
|
|
14
14
|
AvatarStack,
|
|
15
15
|
BackButton,
|
|
16
16
|
HoneypotField,
|
|
17
|
-
Label,
|
|
18
17
|
MAX_MONTH_OFFSET,
|
|
19
18
|
RadioGroup,
|
|
20
19
|
RadioGroupItem,
|
|
@@ -27,7 +26,7 @@ import {
|
|
|
27
26
|
StatusBadge,
|
|
28
27
|
Textarea,
|
|
29
28
|
formatDurationCompact
|
|
30
|
-
} from "../../chunk-
|
|
29
|
+
} from "../../chunk-JS7FDV27.js";
|
|
31
30
|
import "../../chunk-7EUIW46X.js";
|
|
32
31
|
import "../../chunk-LXC6P2EO.js";
|
|
33
32
|
import "../../chunk-EL5YVPD5.js";
|
|
@@ -45,8 +44,9 @@ import "../../chunk-Y2HKHGEW.js";
|
|
|
45
44
|
import {
|
|
46
45
|
Checkbox,
|
|
47
46
|
Input,
|
|
47
|
+
Label,
|
|
48
48
|
Skeleton
|
|
49
|
-
} from "../../chunk-
|
|
49
|
+
} from "../../chunk-RSZXB6J2.js";
|
|
50
50
|
import "../../chunk-6U3IUD57.js";
|
|
51
51
|
import {
|
|
52
52
|
Button
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
var
|
|
38
|
+
var _chunkLDS54O26cjs = require('../../chunk-LDS54O26.cjs');
|
|
39
39
|
require('../../chunk-C5RS3QLV.cjs');
|
|
40
40
|
require('../../chunk-XL4V2PYG.cjs');
|
|
41
41
|
require('../../chunk-BZFW3FOF.cjs');
|
|
@@ -45,7 +45,7 @@ require('../../chunk-3M2MFP2Y.cjs');
|
|
|
45
45
|
require('../../chunk-UTVOBFXR.cjs');
|
|
46
46
|
require('../../chunk-XQFFGR6U.cjs');
|
|
47
47
|
require('../../chunk-7OPWSWEJ.cjs');
|
|
48
|
-
require('../../chunk-
|
|
48
|
+
require('../../chunk-DCB4EBA5.cjs');
|
|
49
49
|
require('../../chunk-WZW7C7TF.cjs');
|
|
50
50
|
require('../../chunk-7INXXQVK.cjs');
|
|
51
51
|
require('../../chunk-WBR7H6E3.cjs');
|
|
@@ -87,5 +87,5 @@ require('../../chunk-VRHGVLSL.cjs');
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
exports.AppHeader =
|
|
90
|
+
exports.AppHeader = _chunkLDS54O26cjs.AppHeader; exports.AppLayout = _chunkLDS54O26cjs.AppLayout; exports.AppLayoutDrawer = _chunkLDS54O26cjs.AppLayoutDrawerRoot; exports.AppLayoutDrawerBody = _chunkLDS54O26cjs.AppLayoutDrawerBody; exports.AppLayoutDrawerClose = _chunkLDS54O26cjs.AppLayoutDrawerClose; exports.AppLayoutDrawerContent = _chunkLDS54O26cjs.AppLayoutDrawerContent; exports.AppLayoutDrawerDescription = _chunkLDS54O26cjs.AppLayoutDrawerDescription; exports.AppLayoutDrawerFooter = _chunkLDS54O26cjs.AppLayoutDrawerFooter; exports.AppLayoutDrawerHeader = _chunkLDS54O26cjs.AppLayoutDrawerHeader; exports.AppLayoutDrawerTitle = _chunkLDS54O26cjs.AppLayoutDrawerTitle; exports.AppLayoutDrawerTrigger = _chunkLDS54O26cjs.AppLayoutDrawerTrigger; exports.ClientOnlyHeader = _chunkLDS54O26cjs.ClientOnlyHeader; exports.Header = _chunkLDS54O26cjs.Header; exports.HeaderButton = _chunkLDS54O26cjs.HeaderButton; exports.HeaderGlobalSearch = _chunkLDS54O26cjs.HeaderGlobalSearch; exports.HeaderMingoButton = _chunkLDS54O26cjs.HeaderMingoButton; exports.HeaderOrganizationFilter = _chunkLDS54O26cjs.HeaderOrganizationFilter; exports.HeaderSkeleton = _chunkLDS54O26cjs.HeaderSkeleton; exports.MOBILE_NAV_PANEL_ID = _chunkLDS54O26cjs.MOBILE_NAV_PANEL_ID; exports.MingoAiButton = _chunkLDS54O26cjs.MingoAiButton; exports.MobileBurgerMenu = _chunkLDS54O26cjs.MobileBurgerMenu; exports.MobileNavPanel = _chunkLDS54O26cjs.MobileNavPanel; exports.MobileNavigationDropdown = _chunkLDS54O26cjs.MobileNavigationDropdown; exports.MultiLevelNavigation = _chunkLDS54O26cjs.MultiLevelNavigation; exports.NAVIGATION_SIDEBAR_WIDTH_VAR = _chunkLDS54O26cjs.NAVIGATION_SIDEBAR_WIDTH_VAR; exports.NavigationSidebar = _chunkLDS54O26cjs.NavigationSidebar; exports.SlidingSidebar = _chunkLDS54O26cjs.SlidingSidebar; exports.StickySectionNav = _chunkLDS54O26cjs.StickySectionNav; exports.TicketAlertsButton = _chunkLDS54O26cjs.TicketAlertsButton; exports.TopNavigation = _chunkLDS54O26cjs.TopNavigation; exports.UnreadCountBadge = _chunkLDS54O26cjs.UnreadCountBadge; exports.UnreadDot = _chunkLDS54O26cjs.UnreadDot; exports.useAppLayoutDrawerContainer = _chunkLDS54O26cjs.useAppLayoutDrawerContainer; exports.useSectionNavigation = _chunkLDS54O26cjs.useSectionNavigation;
|
|
91
91
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
UnreadDot,
|
|
36
36
|
useAppLayoutDrawerContainer,
|
|
37
37
|
useSectionNavigation
|
|
38
|
-
} from "../../chunk-
|
|
38
|
+
} from "../../chunk-JS7FDV27.js";
|
|
39
39
|
import "../../chunk-7EUIW46X.js";
|
|
40
40
|
import "../../chunk-LXC6P2EO.js";
|
|
41
41
|
import "../../chunk-EL5YVPD5.js";
|
|
@@ -45,7 +45,7 @@ import "../../chunk-GJAHN7LH.js";
|
|
|
45
45
|
import "../../chunk-JMZH4KMJ.js";
|
|
46
46
|
import "../../chunk-W72U7OU7.js";
|
|
47
47
|
import "../../chunk-Y2HKHGEW.js";
|
|
48
|
-
import "../../chunk-
|
|
48
|
+
import "../../chunk-RSZXB6J2.js";
|
|
49
49
|
import "../../chunk-6U3IUD57.js";
|
|
50
50
|
import "../../chunk-5OGHZNY5.js";
|
|
51
51
|
import "../../chunk-OHPI2HRK.js";
|