@flamingo-stack/openframe-frontend-core 0.0.560 → 0.0.561-1846.5000.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/{chunk-F3F3PVJ3.js → chunk-44U25GD5.js} +2 -2
  2. package/dist/{chunk-DCB4EBA5.cjs → chunk-4LHEQMOV.cjs} +3 -3
  3. package/dist/chunk-4LHEQMOV.cjs.map +1 -0
  4. package/dist/{chunk-IGO6GNG7.js → chunk-5AEHCGQV.js} +6 -6
  5. package/dist/{chunk-QUYIRF6U.cjs → chunk-AGQ4VTUX.cjs} +29 -29
  6. package/dist/{chunk-QUYIRF6U.cjs.map → chunk-AGQ4VTUX.cjs.map} +1 -1
  7. package/dist/{chunk-GMDPQ5FQ.js → chunk-B6TM3QAL.js} +4 -4
  8. package/dist/{chunk-GZSUDIJB.cjs → chunk-CR2CMLTQ.cjs} +4 -4
  9. package/dist/{chunk-GZSUDIJB.cjs.map → chunk-CR2CMLTQ.cjs.map} +1 -1
  10. package/dist/{chunk-MAX76H7U.cjs → chunk-D3OY4CJU.cjs} +3 -3
  11. package/dist/{chunk-MAX76H7U.cjs.map → chunk-D3OY4CJU.cjs.map} +1 -1
  12. package/dist/{chunk-WXJZULAB.cjs → chunk-DAVVVBNL.cjs} +26 -26
  13. package/dist/{chunk-WXJZULAB.cjs.map → chunk-DAVVVBNL.cjs.map} +1 -1
  14. package/dist/{chunk-XRP6VHOS.cjs → chunk-DHRJFUBR.cjs} +11 -11
  15. package/dist/{chunk-XRP6VHOS.cjs.map → chunk-DHRJFUBR.cjs.map} +1 -1
  16. package/dist/{chunk-N66ANBF6.cjs → chunk-EK346HD7.cjs} +39 -39
  17. package/dist/{chunk-N66ANBF6.cjs.map → chunk-EK346HD7.cjs.map} +1 -1
  18. package/dist/{chunk-UR2JKFIO.cjs → chunk-FSWJCGRJ.cjs} +7 -7
  19. package/dist/{chunk-UR2JKFIO.cjs.map → chunk-FSWJCGRJ.cjs.map} +1 -1
  20. package/dist/{chunk-RDFMBOSI.js → chunk-GKGQ4DWE.js} +120 -48
  21. package/dist/chunk-GKGQ4DWE.js.map +1 -0
  22. package/dist/{chunk-FEI4KQYW.cjs → chunk-GWRQ5EEI.cjs} +953 -881
  23. package/dist/chunk-GWRQ5EEI.cjs.map +1 -0
  24. package/dist/{chunk-NCU45OV4.cjs → chunk-GYC3OTBI.cjs} +9 -9
  25. package/dist/{chunk-NCU45OV4.cjs.map → chunk-GYC3OTBI.cjs.map} +1 -1
  26. package/dist/{chunk-WIPZ65KU.js → chunk-JCQUWIPT.js} +3 -3
  27. package/dist/{chunk-2IL7SPBP.js → chunk-KRYLVEXU.js} +2 -2
  28. package/dist/{chunk-YNRK3N2D.js → chunk-KWNVQZ5Y.js} +4 -4
  29. package/dist/{chunk-QXDEXQDI.js → chunk-MLSDBNKT.js} +2 -2
  30. package/dist/{chunk-65ZXR4YJ.js → chunk-NGY4KET4.js} +2 -2
  31. package/dist/{chunk-QKGNSXF2.js → chunk-OCJG3X4U.js} +2 -2
  32. package/dist/{chunk-VAAQZZHT.js → chunk-ONDI64M3.js} +2 -2
  33. package/dist/{chunk-BKQWCJP5.js → chunk-QGQ26NEE.js} +2 -2
  34. package/dist/{chunk-RSZXB6J2.js → chunk-RDKJCZHY.js} +3 -3
  35. package/dist/chunk-RDKJCZHY.js.map +1 -0
  36. package/dist/{chunk-CSXEOFJ4.cjs → chunk-SUPBFRUK.cjs} +5 -5
  37. package/dist/{chunk-CSXEOFJ4.cjs.map → chunk-SUPBFRUK.cjs.map} +1 -1
  38. package/dist/{chunk-I7RGBCG6.cjs → chunk-UJ7MSY6F.cjs} +31 -31
  39. package/dist/{chunk-I7RGBCG6.cjs.map → chunk-UJ7MSY6F.cjs.map} +1 -1
  40. package/dist/{chunk-UW42K54L.cjs → chunk-UV53Z64O.cjs} +14 -14
  41. package/dist/{chunk-UW42K54L.cjs.map → chunk-UV53Z64O.cjs.map} +1 -1
  42. package/dist/{chunk-NFMYNXFH.cjs → chunk-WCM4BGHO.cjs} +97 -97
  43. package/dist/{chunk-NFMYNXFH.cjs.map → chunk-WCM4BGHO.cjs.map} +1 -1
  44. package/dist/{chunk-57G6CG2E.js → chunk-YUAKZNK5.js} +6 -6
  45. package/dist/{chunk-XQJPFAA7.cjs → chunk-ZENRHE55.cjs} +62 -62
  46. package/dist/{chunk-XQJPFAA7.cjs.map → chunk-ZENRHE55.cjs.map} +1 -1
  47. package/dist/{chunk-PHIRRCPY.js → chunk-ZJ7RI62F.js} +2 -2
  48. package/dist/components/calendar.cjs +81 -4
  49. package/dist/components/calendar.cjs.map +1 -1
  50. package/dist/components/calendar.js +81 -4
  51. package/dist/components/calendar.js.map +1 -1
  52. package/dist/components/case-studies/index.cjs +9 -9
  53. package/dist/components/case-studies/index.js +3 -3
  54. package/dist/components/chat/index.cjs +3 -3
  55. package/dist/components/chat/index.js +2 -2
  56. package/dist/components/contact/index.cjs +4 -4
  57. package/dist/components/contact/index.js +3 -3
  58. package/dist/components/docs/index.cjs +6 -6
  59. package/dist/components/docs/index.js +5 -5
  60. package/dist/components/embeds/index.cjs +4 -4
  61. package/dist/components/embeds/index.js +3 -3
  62. package/dist/components/faq/index.cjs +4 -4
  63. package/dist/components/faq/index.js +3 -3
  64. package/dist/components/features/index.cjs +5 -3
  65. package/dist/components/features/index.cjs.map +1 -1
  66. package/dist/components/features/index.d.ts +1 -1
  67. package/dist/components/features/index.d.ts.map +1 -1
  68. package/dist/components/features/index.js +4 -2
  69. package/dist/components/features/use-walkthrough-video.d.ts +1 -1
  70. package/dist/components/features/use-walkthrough-video.d.ts.map +1 -1
  71. package/dist/components/features/{floating-walkthrough-video.d.ts → walkthrough-video.d.ts} +61 -7
  72. package/dist/components/features/walkthrough-video.d.ts.map +1 -0
  73. package/dist/components/help-center-pages/index.cjs +23 -23
  74. package/dist/components/help-center-pages/index.js +14 -14
  75. package/dist/components/index.cjs +144 -140
  76. package/dist/components/index.cjs.map +1 -1
  77. package/dist/components/index.js +16 -12
  78. package/dist/components/index.js.map +1 -1
  79. package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
  80. package/dist/components/meeting-scheduler/context-panel.d.ts +24 -6
  81. package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
  82. package/dist/components/meeting-scheduler/index.cjs +529 -425
  83. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  84. package/dist/components/meeting-scheduler/index.d.ts +54 -1
  85. package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
  86. package/dist/components/meeting-scheduler/index.js +507 -403
  87. package/dist/components/meeting-scheduler/index.js.map +1 -1
  88. package/dist/components/meeting-scheduler/slot-picker.d.ts +35 -31
  89. package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
  90. package/dist/components/navigation/index.cjs +3 -3
  91. package/dist/components/navigation/index.js +2 -2
  92. package/dist/components/onboarding-guides/index.cjs +6 -6
  93. package/dist/components/onboarding-guides/index.js +5 -5
  94. package/dist/components/related-content/index.cjs +4 -4
  95. package/dist/components/related-content/index.js +3 -3
  96. package/dist/components/tickets/index.cjs +7 -7
  97. package/dist/components/tickets/index.js +6 -6
  98. package/dist/components/ui/date-picker.d.ts +62 -1
  99. package/dist/components/ui/date-picker.d.ts.map +1 -1
  100. package/dist/components/ui/field-wrapper.d.ts +7 -0
  101. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  102. package/dist/components/ui/file-manager/index.cjs +36 -36
  103. package/dist/components/ui/file-manager/index.js +1 -1
  104. package/dist/components/ui/index.cjs +5 -3
  105. package/dist/components/ui/index.cjs.map +1 -1
  106. package/dist/components/ui/index.js +4 -2
  107. package/dist/hooks/use-meeting-booking.d.ts +31 -8
  108. package/dist/hooks/use-meeting-booking.d.ts.map +1 -1
  109. package/dist/index.cjs +7 -3
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.js +6 -2
  112. package/package.json +1 -1
  113. package/src/components/features/{.floating-walkthrough-video.md → .walkthrough-video.md} +8 -5
  114. package/src/components/features/index.ts +1 -1
  115. package/src/components/features/use-walkthrough-video.ts +1 -1
  116. package/src/components/features/{floating-walkthrough-video.tsx → walkthrough-video.tsx} +108 -22
  117. package/src/components/meeting-scheduler/booking-form.tsx +60 -51
  118. package/src/components/meeting-scheduler/confirmation.tsx +1 -1
  119. package/src/components/meeting-scheduler/context-panel.tsx +166 -68
  120. package/src/components/meeting-scheduler/index.tsx +227 -81
  121. package/src/components/meeting-scheduler/slot-picker.tsx +275 -127
  122. package/src/components/ui/date-picker.tsx +200 -47
  123. package/src/components/ui/field-wrapper.tsx +9 -2
  124. package/src/hooks/use-meeting-booking.ts +97 -73
  125. package/dist/chunk-7J4DGFHZ.js +0 -88
  126. package/dist/chunk-7J4DGFHZ.js.map +0 -1
  127. package/dist/chunk-DCB4EBA5.cjs.map +0 -1
  128. package/dist/chunk-FEI4KQYW.cjs.map +0 -1
  129. package/dist/chunk-MKPJYEOR.cjs +0 -88
  130. package/dist/chunk-MKPJYEOR.cjs.map +0 -1
  131. package/dist/chunk-RDFMBOSI.js.map +0 -1
  132. package/dist/chunk-RSZXB6J2.js.map +0 -1
  133. package/dist/components/features/floating-walkthrough-video.d.ts.map +0 -1
  134. /package/dist/{chunk-F3F3PVJ3.js.map → chunk-44U25GD5.js.map} +0 -0
  135. /package/dist/{chunk-IGO6GNG7.js.map → chunk-5AEHCGQV.js.map} +0 -0
  136. /package/dist/{chunk-GMDPQ5FQ.js.map → chunk-B6TM3QAL.js.map} +0 -0
  137. /package/dist/{chunk-WIPZ65KU.js.map → chunk-JCQUWIPT.js.map} +0 -0
  138. /package/dist/{chunk-2IL7SPBP.js.map → chunk-KRYLVEXU.js.map} +0 -0
  139. /package/dist/{chunk-YNRK3N2D.js.map → chunk-KWNVQZ5Y.js.map} +0 -0
  140. /package/dist/{chunk-QXDEXQDI.js.map → chunk-MLSDBNKT.js.map} +0 -0
  141. /package/dist/{chunk-65ZXR4YJ.js.map → chunk-NGY4KET4.js.map} +0 -0
  142. /package/dist/{chunk-QKGNSXF2.js.map → chunk-OCJG3X4U.js.map} +0 -0
  143. /package/dist/{chunk-VAAQZZHT.js.map → chunk-ONDI64M3.js.map} +0 -0
  144. /package/dist/{chunk-BKQWCJP5.js.map → chunk-QGQ26NEE.js.map} +0 -0
  145. /package/dist/{chunk-57G6CG2E.js.map → chunk-YUAKZNK5.js.map} +0 -0
  146. /package/dist/{chunk-PHIRRCPY.js.map → chunk-ZJ7RI62F.js.map} +0 -0
@@ -68,7 +68,7 @@ export * from './video-bites-display'
68
68
  export * from './card-hit-layer'
69
69
  export * from './video-hover-preview'
70
70
  export * from './entity-video-section'
71
- export * from './floating-walkthrough-video'
71
+ export * from './walkthrough-video'
72
72
  export * from './use-walkthrough-video'
73
73
  export * from './use-video-warmup'
74
74
  export * from './captions-url'
@@ -17,8 +17,8 @@
17
17
  */
18
18
 
19
19
  import { useQuery } from '@tanstack/react-query';
20
- import type { WalkthroughVideoData } from './floating-walkthrough-video';
21
20
  import { useCaptions } from './use-captions';
21
+ import type { WalkthroughVideoData } from './walkthrough-video';
22
22
 
23
23
  export interface UseWalkthroughVideoOptions {
24
24
  /** Absolute or proxied URL of the public GET route. */
@@ -1,12 +1,35 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * <FloatingWalkthroughVideo> — THE generic, embeddable per-platform demo-video
5
- * widget. A collapsed card pinned to a bottom corner — left unless the video
6
- * data says `position: 'right'` — (bite-identical hover grammar via
4
+ * THE generic, embeddable per-platform demo-video widget, in the two placements
5
+ * hosts actually mount: <FloatingWalkthroughVideo> and <InlineWalkthroughVideo>.
6
+ *
7
+ * SHARED GRAMMAR. A collapsed card (bite-identical hover grammar via
7
8
  * <VideoHoverPreviewSurface>) that opens a large in-page theater (Radix Dialog
8
9
  * primitives, NOT native fullscreen) showing ONLY the video: a bare 16:9 stage
9
10
  * with the player's own controls + captions. No card chrome, no summary.
11
+ * - FLOATING pins that card to a bottom corner over the page — left unless
12
+ * the video data says `position: 'right'`.
13
+ * - INLINE lays it out in the host's own flow, filling its container at 16:9.
14
+ * Every overlay-only behaviour drops out with the pinning: no fixed
15
+ * positioning, no z-layer, no shadow, no appear delay (nothing to stagger
16
+ * against an already-painted page), no footer fade (no footer to collide
17
+ * with) and no dismissal (an X is an overlay's "get out of my way"; in a
18
+ * host's own layout it just leaves a hole, and the cookie is per-platform,
19
+ * so one dismissal in the corner would silently empty the in-page block).
20
+ *
21
+ * ONE ENGINE, TWO PRESETS. <WalkthroughVideo> below takes `placement` and is
22
+ * deliberately NOT exported: placement is a constant at every call site, so the
23
+ * public API is two components that say what they are instead of one that takes
24
+ * a mode — a <FloatingWalkthroughVideo placement="inline"> reads as a
25
+ * contradiction, and naming a family after one of its variants is what produced
26
+ * it. The presets are one line each; everything below the wrapper — the player
27
+ * mode machine, the audio invariant, the corner map, dismissal — stays shared,
28
+ * because forking THAT is how the two placements would drift.
29
+ *
30
+ * The split pays off in the types: the overlay-only knobs are absent from
31
+ * `InlineWalkthroughVideoProps` outright, so "floating only" is a compile error
32
+ * rather than a line of prose each caller has to read.
10
33
  *
11
34
  * All UI lives here in the lib so every platform site AND the react-embedding
12
35
  * example mount the same component; the host supplies only the video data.
@@ -78,7 +101,8 @@ export interface WalkthroughVideoData {
78
101
  position?: 'left' | 'right' | null;
79
102
  }
80
103
 
81
- export interface FloatingWalkthroughVideoProps {
104
+ /** Everything the two placements share — the widget's actual contract. */
105
+ interface WalkthroughVideoBaseProps {
82
106
  video: WalkthroughVideoData | null | undefined;
83
107
  open?: boolean;
84
108
  onOpenChange?: (open: boolean) => void;
@@ -101,17 +125,35 @@ export interface FloatingWalkthroughVideoProps {
101
125
  * so the hydrated (non-portal) markup is identical either way. */
102
126
  deepLinkParam?: string;
103
127
  label?: string;
128
+ className?: string;
129
+ }
130
+
131
+ /**
132
+ * <FloatingWalkthroughVideo>: the shared contract plus the knobs that only mean
133
+ * something for an OVERLAY. Each of these is inert inline — the appear delay
134
+ * staggers against a page the visitor hasn't seen yet, dismissal is an overlay
135
+ * affordance, and the footer fade (with the `pathname` that re-queries its
136
+ * target) exists to keep a pinned card off the page chrome. So they live here,
137
+ * not in the base.
138
+ */
139
+ export interface FloatingWalkthroughVideoProps extends WalkthroughVideoBaseProps {
104
140
  appearDelayMs?: number;
105
141
  /** Cookie-based dismissal (id-match, mirrors the announcement bar). `false`
106
142
  * disables the X entirely. `storageKey` is the per-platform cookie name. */
107
143
  dismissal?: { storageKey?: string } | false;
144
+ /** Fades the card out while `hideNearSelector` is in view. */
108
145
  hideNearSelector?: string;
109
146
  /** Route identity from the host (the lib can't observe navigation). Changing
110
147
  * it re-queries the footer IO target. */
111
148
  pathname?: string;
112
- className?: string;
113
149
  }
114
150
 
151
+ /** <InlineWalkthroughVideo>: the shared contract and nothing else — see above. */
152
+ export type InlineWalkthroughVideoProps = WalkthroughVideoBaseProps;
153
+
154
+ /** Engine props. Internal: `placement` never reaches a call site. */
155
+ type WalkthroughVideoProps = FloatingWalkthroughVideoProps & { placement: 'floating' | 'inline' };
156
+
115
157
  /** "Paused at the end" — used by BOTH the close snapshot and the reopen seed,
116
158
  * which must agree or a finished video reopens at its own last frame. */
117
159
  const isAtEnd = (duration: number, time: number) => duration > 0 && duration - time < 1;
@@ -124,7 +166,7 @@ interface Handoff {
124
166
  playing: boolean;
125
167
  }
126
168
 
127
- export function FloatingWalkthroughVideo({
169
+ function WalkthroughVideo({
128
170
  video,
129
171
  open: openProp,
130
172
  onOpenChange,
@@ -132,12 +174,17 @@ export function FloatingWalkthroughVideo({
132
174
  defaultOpenPaused,
133
175
  deepLinkParam = WALKTHROUGH_OPEN_QUERY_PARAM,
134
176
  label = 'Play Demo Video',
177
+ placement,
178
+ // Keeps the FLOATING card off the critical path. The inline preset passes 0:
179
+ // an in-page block is part of a page the visitor is already looking at, so
180
+ // staggering it in would only jump the layout.
135
181
  appearDelayMs = 3000,
136
182
  dismissal = {},
137
183
  hideNearSelector = 'footer',
138
184
  pathname,
139
185
  className,
140
- }: FloatingWalkthroughVideoProps): React.ReactElement | null {
186
+ }: WalkthroughVideoProps): React.ReactElement | null {
187
+ const inline = placement === 'inline';
141
188
  const dismissEnabled = dismissal !== false;
142
189
  const storageKey = (dismissEnabled && dismissal.storageKey) || WALKTHROUGH_VIDEO_DISMISS_KEY;
143
190
  // id-match dismissal key: a new video (new id) re-shows the card. No id
@@ -257,6 +304,12 @@ export function FloatingWalkthroughVideo({
257
304
  // --- footer fade (re-query on pathname change; null-tolerant) ---
258
305
  useEffect(() => {
259
306
  if (!mounted) return;
307
+ // An inline card scrolls WITH the page — fading it out over the footer
308
+ // would blank the very block the visitor scrolled down to.
309
+ if (inline) {
310
+ setFooterHidden(false);
311
+ return;
312
+ }
260
313
  const el = typeof document !== 'undefined' ? document.querySelector(hideNearSelector) : null;
261
314
  if (!el || typeof IntersectionObserver === 'undefined') {
262
315
  setFooterHidden(false);
@@ -268,7 +321,7 @@ export function FloatingWalkthroughVideo({
268
321
  );
269
322
  io.observe(el);
270
323
  return () => io.disconnect();
271
- }, [mounted, hideNearSelector, pathname]);
324
+ }, [mounted, inline, hideNearSelector, pathname]);
272
325
 
273
326
  // --- tab-hidden: pause resume-mode playback, keep the timestamp ---
274
327
  useEffect(() => {
@@ -697,7 +750,10 @@ export function FloatingWalkthroughVideo({
697
750
  // badge accents off it. It must live on an element that actually
698
751
  // receives pointer events — the media layer is pointer-events-none, so
699
752
  // a group inside it can never match :hover.
700
- 'group/card pointer-events-auto relative overflow-hidden rounded-lg border border-ods-border bg-ods-card shadow-2xl',
753
+ 'group/card pointer-events-auto relative overflow-hidden rounded-md border border-ods-border bg-ods-card',
754
+ // The lift is what separates a FLOATING card from the page beneath it;
755
+ // an in-flow block sitting inside a host card needs no shadow at all.
756
+ !inline && 'shadow-2xl',
701
757
  // MOBILE = PiP dimensions, not a shrunken desktop card. This is an
702
758
  // UNINVITED overlay on the smallest screens, so it is sized against
703
759
  // the corner-PiP conventions, NOT against system PiP (which is
@@ -713,7 +769,9 @@ export function FloatingWalkthroughVideo({
713
769
  // Do NOT go below ~150px: the corner chrome (32px transport pair +
714
770
  // 32px dismiss + the title pill) stops fitting, and the card's own
715
771
  // controls — not the poster — are what set the real floor here.
716
- 'aspect-video w-[min(52vw,192px)] sm:w-80 transition-opacity duration-200',
772
+ 'aspect-video transition-opacity duration-200',
773
+ // Inline: the HOST owns the width — the card only keeps 16:9 within it.
774
+ inline ? 'w-full' : 'w-[min(52vw,192px)] sm:w-80',
717
775
  footerHidden ? 'opacity-0 pointer-events-none' : 'opacity-100',
718
776
  className,
719
777
  )}
@@ -906,18 +964,23 @@ export function FloatingWalkthroughVideo({
906
964
 
907
965
  return (
908
966
  <>
909
- {showCard && (
910
- <div
911
- className={cn(
912
- 'pointer-events-none fixed bottom-0 p-[var(--spacing-system-mf)]',
913
- video.position === 'right' ? 'right-0' : 'left-0',
914
- WALKTHROUGH_Z,
915
- )}
916
- style={{ paddingBottom: 'max(var(--spacing-system-mf), env(safe-area-inset-bottom))' }}
917
- >
918
- {collapsed}
919
- </div>
920
- )}
967
+ {showCard &&
968
+ (inline ? (
969
+ // No wrapper: a positioning / z-index / safe-area shell around an
970
+ // in-flow block is exactly what stops it sizing to its container.
971
+ collapsed
972
+ ) : (
973
+ <div
974
+ className={cn(
975
+ 'pointer-events-none fixed bottom-0 p-[var(--spacing-system-mf)]',
976
+ video.position === 'right' ? 'right-0' : 'left-0',
977
+ WALKTHROUGH_Z,
978
+ )}
979
+ style={{ paddingBottom: 'max(var(--spacing-system-mf), env(safe-area-inset-bottom))' }}
980
+ >
981
+ {collapsed}
982
+ </div>
983
+ ))}
921
984
 
922
985
  <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange}>
923
986
  <DialogPortal>
@@ -993,3 +1056,26 @@ export function FloatingWalkthroughVideo({
993
1056
  </>
994
1057
  );
995
1058
  }
1059
+
1060
+ /**
1061
+ * The widget pinned into a bottom corner over the page — the app-shell / site
1062
+ * placement. Every host that wants "a demo video somewhere on the page, out of
1063
+ * the way" mounts this.
1064
+ */
1065
+ export function FloatingWalkthroughVideo(props: FloatingWalkthroughVideoProps): React.ReactElement {
1066
+ return <WalkthroughVideo {...props} placement="floating" />;
1067
+ }
1068
+
1069
+ /**
1070
+ * The same widget as an in-flow block, sized by whatever the host puts it in —
1071
+ * for a page that gives the video a slot of its own rather than a corner.
1072
+ *
1073
+ * The two overlay defaults are fixed here rather than left to the caller: an
1074
+ * in-page block has nothing to stagger against (`appearDelayMs={0}`), and a
1075
+ * dismissable one would leave the host's layout with a hole while sharing the
1076
+ * floating card's per-platform cookie (`dismissal={false}`). Both are stated
1077
+ * once here instead of at every call site, where forgetting either is silent.
1078
+ */
1079
+ export function InlineWalkthroughVideo(props: InlineWalkthroughVideoProps): React.ReactElement {
1080
+ return <WalkthroughVideo {...props} placement="inline" appearDelayMs={0} dismissal={false} />;
1081
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../schemas/meeting-booking-schema'
12
12
  import {
13
13
  Button,
14
+ FieldWrapper,
14
15
  Input,
15
16
  Textarea,
16
17
  Label,
@@ -104,65 +105,76 @@ export function BookingForm({
104
105
 
105
106
  // Field chrome mirrors ContactForm 1:1 (`contact/contact-form.tsx`) — the
106
107
  // booking form must be indistinguishable from every other form in the app.
107
- const inputClass = 'bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12'
108
+ const inputClass = 'bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12'
109
+ // One step ABOVE the `spacing system/m` the design names (16/24 instead of
110
+ // 12/16), because the field messages hang out of flow: they need ~16px on a
111
+ // phone and ~20 on desktop of clear space under the control, and `m` leaves
112
+ // 12/16 — four pixels short at both ends, so an error would print over the
113
+ // next field's label. The design has no error state drawn; this is the
114
+ // smallest ODS step that houses it.
115
+ const FORM_STACK = 'flex flex-col gap-[var(--spacing-system-l)]'
108
116
 
109
117
  return (
110
- <form onSubmit={submit} className="flex flex-col space-y-4 md:space-y-6" noValidate>
118
+ <form onSubmit={submit} className={FORM_STACK} noValidate>
111
119
  <HoneypotField {...honeypotInputProps} />
112
120
 
113
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
114
- <div className="flex flex-col">
115
- <Label htmlFor="ms-first-name">
116
- First name<span className="text-ods-accent">*</span>
117
- </Label>
121
+ {/* Email first, name pair below — the order the desktop and mobile mocks
122
+ both draw. The pair stays TWO columns even on a phone (164px each at
123
+ 375): two short fields side by side cost one line instead of two on
124
+ the layout that can least afford them.
125
+
126
+ Every field goes through `FieldWrapper`, which hangs its message OUT
127
+ OF FLOW below the control. That is the whole reason it is here: a
128
+ message rendered in flow grows its field, which pushes everything
129
+ under it down and — inside a card that states its height — walks the
130
+ submit button off the bottom the moment validation fails. Required-ness
131
+ is carried by `required` on the control (read out by assistive tech),
132
+ not by an asterisk the design does not draw. */}
133
+ <FieldWrapper label="Email" htmlFor="ms-email" error={errors.email?.message}>
134
+ <Input
135
+ id="ms-email"
136
+ type="email"
137
+ required
138
+ autoComplete="email"
139
+ placeholder="jane@company.com"
140
+ aria-invalid={!!errors.email}
141
+ className={inputClass}
142
+ {...register('email')}
143
+ />
144
+ </FieldWrapper>
145
+
146
+ <div className="grid grid-cols-2 gap-[var(--spacing-system-m)]">
147
+ <FieldWrapper label="First Name" htmlFor="ms-first-name" error={errors.firstName?.message}>
118
148
  <Input
119
149
  id="ms-first-name"
150
+ required
120
151
  autoComplete="given-name"
121
152
  placeholder="Jane"
122
153
  aria-invalid={!!errors.firstName}
123
154
  className={inputClass}
124
155
  {...register('firstName')}
125
156
  />
126
- {errors.firstName && <span className="text-ods-error text-h6 mt-1">{errors.firstName.message}</span>}
127
- </div>
128
- <div className="flex flex-col">
129
- <Label htmlFor="ms-last-name">
130
- Last name<span className="text-ods-accent">*</span>
131
- </Label>
157
+ </FieldWrapper>
158
+ <FieldWrapper label="Last Name" htmlFor="ms-last-name" error={errors.lastName?.message}>
132
159
  <Input
133
160
  id="ms-last-name"
161
+ required
134
162
  autoComplete="family-name"
135
163
  placeholder="Doe"
136
164
  aria-invalid={!!errors.lastName}
137
165
  className={inputClass}
138
166
  {...register('lastName')}
139
167
  />
140
- {errors.lastName && <span className="text-ods-error text-h6 mt-1">{errors.lastName.message}</span>}
141
- </div>
142
- </div>
143
-
144
- <div className="flex flex-col">
145
- <Label htmlFor="ms-email">
146
- Email<span className="text-ods-accent">*</span>
147
- </Label>
148
- <Input
149
- id="ms-email"
150
- type="email"
151
- autoComplete="email"
152
- placeholder="jane@company.com"
153
- aria-invalid={!!errors.email}
154
- className={inputClass}
155
- {...register('email')}
156
- />
157
- {errors.email && <span className="text-ods-error text-h6 mt-1">{errors.email.message}</span>}
168
+ </FieldWrapper>
158
169
  </div>
159
170
 
160
171
  {supportedFields.map((field) => (
161
- <div key={field.name} className="flex flex-col">
162
- <Label htmlFor={`ms-q-${field.name}`}>
163
- {field.label}
164
- {field.required && <span className="text-ods-accent">*</span>}
165
- </Label>
172
+ <FieldWrapper
173
+ key={field.name}
174
+ label={field.label}
175
+ htmlFor={`ms-q-${field.name}`}
176
+ error={fieldError(field.name)}
177
+ >
166
178
  {field.type === 'textarea' && (
167
179
  <Textarea
168
180
  id={`ms-q-${field.name}`}
@@ -173,7 +185,7 @@ export function BookingForm({
173
185
  {field.type === 'text' && (
174
186
  <Input
175
187
  id={`ms-q-${field.name}`}
176
- className="bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
188
+ className="bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-11 md:h-12"
177
189
  {...register(`formFields.${field.name}` as never)}
178
190
  />
179
191
  )}
@@ -226,14 +238,16 @@ export function BookingForm({
226
238
  )}
227
239
  />
228
240
  )}
229
- {fieldError(field.name) && (
230
- <span className="text-ods-error text-h6 mt-1">{fieldError(field.name)}</span>
231
- )}
232
- </div>
241
+ </FieldWrapper>
233
242
  ))}
234
243
 
235
244
  {legalConsent && (
236
- <div className="flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-md)]">
245
+ // Same out-of-flow message as the fields above, so a missed consent
246
+ // box doesn't shove the submit button down the card. With no error the
247
+ // wrapper is `display:contents` and the panel below is the flex item,
248
+ // exactly as it was.
249
+ <FieldWrapper error={errors.legalConsentResponses?.message as string | undefined}>
250
+ <div className="flex flex-col gap-[var(--spacing-system-xs)] border border-ods-border rounded-md p-[var(--spacing-system-m)]">
237
251
  {/* GDPR surface — HubSpot's consent copy rendered VERBATIM, never edited. */}
238
252
  <p className="text-h6 text-ods-text-secondary">{legalConsent.processingConsentText}</p>
239
253
  {legalConsent.communicationConsentText && (
@@ -260,10 +274,7 @@ export function BookingForm({
260
274
  )
261
275
  }
262
276
  />
263
- <Label htmlFor={`ms-consent-${box.communicationTypeId}`}>
264
- {box.label}
265
- {box.required ? ' *' : ''}
266
- </Label>
277
+ <Label htmlFor={`ms-consent-${box.communicationTypeId}`}>{box.label}</Label>
267
278
  </div>
268
279
  )
269
280
  })}
@@ -273,18 +284,16 @@ export function BookingForm({
273
284
  {legalConsent.privacyPolicyText && (
274
285
  <p className="text-h6 text-ods-text-secondary">{legalConsent.privacyPolicyText}</p>
275
286
  )}
276
- {errors.legalConsentResponses && (
277
- <p className="text-h6 text-ods-error">{errors.legalConsentResponses.message as string}</p>
278
- )}
279
287
  </div>
288
+ </FieldWrapper>
280
289
  )}
281
290
 
282
291
  {/* Step navigation back to the calendar lives in the step header (the
283
292
  app-standard BackButton, rendered by the parent) — the form ships
284
293
  only its submit. */}
285
294
  <div className="flex">
286
- <Button type="submit" loading={isSubmitting} disabled={isSubmitting} className="w-full md:w-auto">
287
- Confirm booking
295
+ <Button type="submit" loading={isSubmitting} disabled={isSubmitting}>
296
+ Confirm Booking
288
297
  </Button>
289
298
  </div>
290
299
  </form>
@@ -27,7 +27,7 @@ export function Confirmation({ confirmation, timezone }: ConfirmationProps) {
27
27
  }).format(new Date(confirmation.startTimeMs))
28
28
 
29
29
  return (
30
- <div className="flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-md)] text-center">
30
+ <div className="flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center">
31
31
  <p className="text-h1" role="img" aria-label="Celebration">
32
32
  🎉
33
33
  </p>