@ticketboothapp/booking 1.2.101 → 1.2.102

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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/BookingDialog.module.css +9 -0
  3. package/src/components/booking/BookingProductGrid.module.css +11 -0
  4. package/src/components/booking/BookingProductGrid.tsx +54 -28
  5. package/src/components/booking/CancellationPolicySelector.tsx +4 -1
  6. package/src/components/booking/CheckoutForm.module.css +108 -3
  7. package/src/components/booking/CheckoutForm.tsx +13 -1
  8. package/src/components/booking/CheckoutOptionalPhoneFields.tsx +58 -0
  9. package/src/components/booking/DapTourDescription.tsx +9 -7
  10. package/src/components/booking/DependentAddOnBookingDialog.tsx +42 -7
  11. package/src/components/booking/NewBookingFlow.tsx +137 -55
  12. package/src/components/booking/PrivateShuttleBookingFlow.module.css +7 -0
  13. package/src/components/booking/PrivateShuttleBookingFlow.tsx +21 -0
  14. package/src/components/booking/booking-flow-types.ts +2 -0
  15. package/src/components/booking/booking-flow-ui.ts +2 -0
  16. package/src/components/booking/booking-flow.css +72 -4
  17. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -3
  18. package/src/data/dap-descriptions/session-elopements.en.json +12 -12
  19. package/src/data/dap-descriptions/session-proposals.en.json +6 -9
  20. package/src/data/products-config.json +20 -0
  21. package/src/lib/booking/checkout-contact.ts +8 -0
  22. package/src/lib/booking/i18n/messages/en.json +6 -0
  23. package/src/lib/booking/i18n/messages/fr.json +6 -0
  24. package/src/lib/booking/phone.ts +18 -0
  25. package/src/lib/booking-api.ts +131 -2
  26. package/src/lib/booking-types.ts +5 -0
  27. package/src/lib/dap-descriptions.ts +6 -0
  28. package/src/lib/dependent-add-on-api.ts +6 -0
  29. package/src/lib/photo-dap-config.ts +92 -15
  30. package/src/providers/dependent-add-on-dialog-provider.tsx +6 -0
  31. package/src/runtime/types.ts +2 -0
  32. package/src/strings/en.json +6 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.101",
3
+ "version": "1.2.102",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -363,6 +363,15 @@
363
363
  padding: var(--spacing-small);
364
364
  }
365
365
 
366
+ .dapCheckoutCheckboxQuestion label {
367
+ align-items: center;
368
+ }
369
+
370
+ .dapCheckoutCheckboxQuestion input[type="checkbox"] {
371
+ flex: 0 0 auto;
372
+ margin-top: 0;
373
+ }
374
+
366
375
  .dapField {
367
376
  display: flex;
368
377
  flex-direction: column;
@@ -357,3 +357,14 @@
357
357
  left: 0.75rem;
358
358
  }
359
359
  }
360
+
361
+ /* Embedded pickers (photo-first dialog): 2 columns only — default tile sizes */
362
+ .compactRoot .grid {
363
+ grid-template-columns: 1fr 1fr;
364
+ }
365
+
366
+ @media (min-width: 768px) {
367
+ .compactRoot .grid {
368
+ grid-template-columns: 1fr 1fr;
369
+ }
370
+ }
@@ -137,6 +137,7 @@ function BookingProductTileExpanded({
137
137
  useLayoutId,
138
138
  suppressLayoutAnimation,
139
139
  isPartialLaunch,
140
+ selectProductLabel,
140
141
  }: {
141
142
  product: Product;
142
143
  onBook: () => void;
@@ -145,6 +146,7 @@ function BookingProductTileExpanded({
145
146
  useLayoutId: boolean;
146
147
  suppressLayoutAnimation?: boolean;
147
148
  isPartialLaunch: boolean;
149
+ selectProductLabel?: string;
148
150
  }) {
149
151
  const [isClosing, setIsClosing] = useState(false);
150
152
  const { locale } = useLocale();
@@ -257,7 +259,10 @@ function BookingProductTileExpanded({
257
259
  ))}
258
260
  </div>
259
261
  <div className={styles.expandedActions}>
260
- <Button className={styles.bookButton} hoverColor={ButtonHoverColor.Turquoise} onClick={onBook}>{isPartialLaunch ? defaultStrings.common.moreInfo : defaultStrings.common.bookNow}</Button>
262
+ <Button className={styles.bookButton} hoverColor={ButtonHoverColor.Turquoise} onClick={onBook}>
263
+ {selectProductLabel ??
264
+ (isPartialLaunch ? defaultStrings.common.moreInfo : defaultStrings.common.bookNow)}
265
+ </Button>
261
266
  </div>
262
267
  </div>
263
268
  </div>
@@ -271,6 +276,8 @@ interface BookingProductGridProps {
271
276
  onRestoreApplied?: () => void;
272
277
  /** Pre-select a filter when opening the grid (e.g. from a theme page). */
273
278
  initialFilterId?: FilterId;
279
+ /** When true, hides the theme filter pills (e.g. photo-first shuttle picker). */
280
+ hideFilterPills?: boolean;
274
281
  /** When true, shows "More Info" instead of "Book Now" (pre-launch state). */
275
282
  isPartialLaunch?: boolean;
276
283
  /**
@@ -283,6 +290,12 @@ interface BookingProductGridProps {
283
290
  * Default false keeps expand-in-place behavior for the main booking dialog.
284
291
  */
285
292
  bookOnTileClick?: boolean;
293
+ /** Extra filter applied after theme filter (e.g. DAP-eligible shuttles only). */
294
+ productFilter?: (product: Product) => boolean;
295
+ /** Expanded tile CTA label when `onBookProduct` is set (default: book now / more info). */
296
+ selectProductLabel?: string;
297
+ /** Force a 2-column grid (e.g. photo-first add-on dialog); uses default tile sizing. */
298
+ compact?: boolean;
286
299
  }
287
300
 
288
301
  function filterProductsByFilterId(products: Product[], filterId: FilterId): Product[] {
@@ -297,9 +310,13 @@ export default function BookingProductGrid({
297
310
  restoreState,
298
311
  onRestoreApplied,
299
312
  initialFilterId,
313
+ hideFilterPills = false,
300
314
  isPartialLaunch = false,
301
315
  onBookProduct,
302
316
  bookOnTileClick = false,
317
+ productFilter,
318
+ selectProductLabel,
319
+ compact = false,
303
320
  }: BookingProductGridProps = {}) {
304
321
  const { catalog, strings: defaultStrings } = useBookingHost();
305
322
  const { push } = useBookingDialog();
@@ -340,10 +357,10 @@ export default function BookingProductGrid({
340
357
  [catalog, defaultStrings]
341
358
  );
342
359
 
343
- const products = useMemo(
344
- () => filterProductsByFilterId(allProducts, selectedFilterId),
345
- [allProducts, selectedFilterId]
346
- );
360
+ const products = useMemo(() => {
361
+ const filtered = filterProductsByFilterId(allProducts, selectedFilterId);
362
+ return productFilter ? filtered.filter(productFilter) : filtered;
363
+ }, [allProducts, selectedFilterId, productFilter]);
347
364
 
348
365
  // Collapse expanded tile if it's no longer in the filtered list
349
366
  useEffect(() => {
@@ -358,15 +375,21 @@ export default function BookingProductGrid({
358
375
  const expandedProduct =
359
376
  expandedIndex >= 0 ? products[expandedIndex] : null;
360
377
 
361
- // Column count for expand-in-place logic (2 mobile, 3 desktop)
362
- const [cols, setCols] = useState(3);
378
+ // Column count for expand-in-place logic (2 mobile, 3 desktop; compact always 2)
379
+ const [cols, setCols] = useState(compact ? 2 : 3);
363
380
  useEffect(() => {
381
+ if (compact) {
382
+ setCols(2);
383
+ return;
384
+ }
364
385
  const mq = window.matchMedia('(min-width: 768px)');
365
386
  const update = () => setCols(mq.matches ? 3 : 2);
366
387
  update();
367
388
  mq.addEventListener('change', update);
368
389
  return () => mq.removeEventListener('change', update);
369
- }, []);
390
+ }, [compact]);
391
+
392
+ const useLayoutAnimations = !compact && cols === 3;
370
393
 
371
394
  // Expand in place: keep expanded tile in its row, move same-row siblings below.
372
395
  // This avoids holes and prevents the expanded tile from appearing far from the click.
@@ -487,24 +510,26 @@ export default function BookingProductGrid({
487
510
  };
488
511
 
489
512
  return (
490
- <div>
491
- <div className={styles.filterPillsScroll}>
492
- <div className={styles.filterPills}>
493
- {FILTER_IDS.map((filterId) => {
494
- const isApplied = selectedFilterId === filterId;
495
- return (
496
- <button
497
- key={filterId}
498
- type="button"
499
- onClick={() => setSelectedFilterId(filterId)}
500
- className={`${styles.filterPill} ${isApplied ? styles.filterPillApplied : ''}`}
501
- >
502
- {filterLabels[filterId]}
503
- </button>
504
- );
505
- })}
513
+ <div className={compact ? styles.compactRoot : undefined}>
514
+ {hideFilterPills ? null : (
515
+ <div className={styles.filterPillsScroll}>
516
+ <div className={styles.filterPills}>
517
+ {FILTER_IDS.map((filterId) => {
518
+ const isApplied = selectedFilterId === filterId;
519
+ return (
520
+ <button
521
+ key={filterId}
522
+ type="button"
523
+ onClick={() => setSelectedFilterId(filterId)}
524
+ className={`${styles.filterPill} ${isApplied ? styles.filterPillApplied : ''}`}
525
+ >
526
+ {filterLabels[filterId]}
527
+ </button>
528
+ );
529
+ })}
530
+ </div>
506
531
  </div>
507
- </div>
532
+ )}
508
533
  <div className={styles.grid}>
509
534
  <AnimatePresence mode={isRestoring ? 'sync' : 'popLayout'}>
510
535
  {productsAbove.map((product) => (
@@ -512,7 +537,7 @@ export default function BookingProductGrid({
512
537
  key={product.id}
513
538
  product={product}
514
539
  onClick={() => onCollapsedTileClick(product)}
515
- useLayoutId={cols === 3}
540
+ useLayoutId={useLayoutAnimations}
516
541
  suppressLayoutAnimation={isRestoring}
517
542
  />
518
543
  ))}
@@ -524,9 +549,10 @@ export default function BookingProductGrid({
524
549
  onBook={() => handleBook(expandedProduct)}
525
550
  onCollapse={() => setExpandedId(null)}
526
551
  onMount={scrollExpandedToTop}
527
- useLayoutId={cols === 3}
552
+ useLayoutId={useLayoutAnimations}
528
553
  suppressLayoutAnimation={isRestoring}
529
554
  isPartialLaunch={isPartialLaunch}
555
+ selectProductLabel={onBookProduct ? selectProductLabel : undefined}
530
556
  />
531
557
  )}
532
558
 
@@ -535,7 +561,7 @@ export default function BookingProductGrid({
535
561
  key={product.id}
536
562
  product={product}
537
563
  onClick={() => onCollapsedTileClick(product)}
538
- useLayoutId={cols === 3}
564
+ useLayoutId={useLayoutAnimations}
539
565
  suppressLayoutAnimation={isRestoring}
540
566
  />
541
567
  ))}
@@ -50,6 +50,8 @@ interface CancellationPolicySelectorProps {
50
50
  rowSubtitle?: React.ReactNode;
51
51
  /** Hide +fee amounts (customer change flow until server-backed pricing). */
52
52
  suppressFees?: boolean;
53
+ /** Override default `booking.cancellationPolicy` section heading. */
54
+ sectionLabel?: string;
53
55
  }
54
56
 
55
57
  export function CancellationPolicySelector({
@@ -62,11 +64,12 @@ export function CancellationPolicySelector({
62
64
  forcedPolicy,
63
65
  rowSubtitle,
64
66
  suppressFees = false,
67
+ sectionLabel,
65
68
  }: CancellationPolicySelectorProps) {
66
69
  return (
67
70
  <div className={styles.wrapper}>
68
71
  <div className={styles.label}>
69
- {t('booking.cancellationPolicy')}
72
+ {sectionLabel ?? t('booking.cancellationPolicy')}
70
73
  </div>
71
74
  <div className={styles.list}>
72
75
  {forcedPolicy && (
@@ -45,22 +45,127 @@
45
45
 
46
46
  .input {
47
47
  width: 100%;
48
- padding: 0.875rem 1.25rem;
48
+ padding: 0.5rem 0.625rem;
49
49
  border-radius: 0.5rem;
50
50
  border: 1px solid var(--booking-stone-300, #d6d3d1);
51
- font-size: 1rem;
51
+ font-size: 0.875rem;
52
+ line-height: 1.25rem;
52
53
  color: var(--booking-stone-900, #1c1917);
54
+ background: var(--booking-stone-50, #fafaf9);
55
+ box-sizing: border-box;
53
56
  }
54
57
 
55
58
  .input:focus {
56
59
  outline: none;
57
- border-color: var(--booking-stone-500, #78716c);
60
+ border-color: var(--booking-emerald-600, #059669);
61
+ background: #fff;
62
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.2);
58
63
  }
59
64
 
60
65
  .inputError {
61
66
  font-size: 0.75rem;
62
67
  color: #b91c1c;
63
68
  margin-top: 0.375rem;
69
+ border: none;
70
+ background: transparent;
71
+ padding: 0;
72
+ }
73
+
74
+ .optionalPhoneField {
75
+ display: flex;
76
+ flex-direction: column;
77
+ }
78
+
79
+ .contactFieldHint {
80
+ margin: 0.625rem 0 0;
81
+ font-size: 0.75rem;
82
+ line-height: 1.35;
83
+ color: var(--booking-stone-500, #78716c);
84
+ }
85
+
86
+ .optionalPhoneFieldHint {
87
+ margin: 0;
88
+ padding: 0.375rem 0.5rem 0;
89
+ font-size: 0.75rem;
90
+ line-height: 1.4;
91
+ color: var(--booking-stone-500, #78716c);
92
+ }
93
+
94
+ .checkoutPhoneInput {
95
+ width: 100%;
96
+ --react-international-phone-height: auto;
97
+ --react-international-phone-border-radius: 0.5rem;
98
+ --react-international-phone-background-color: var(--booking-stone-50, #fafaf9);
99
+ --react-international-phone-border-color: var(--booking-stone-300, #d6d3d1);
100
+ --react-international-phone-font-size: 0.875rem;
101
+ }
102
+
103
+ /* Override site PhoneInputWithCountry (orange 2px border) — match promo / contact fields */
104
+ .checkoutPhoneInput :global(.react-international-phone-input-container) {
105
+ display: flex;
106
+ align-items: center;
107
+ width: 100%;
108
+ min-height: 0;
109
+ border: 1px solid var(--booking-stone-300, #d6d3d1) !important;
110
+ border-radius: 0.5rem !important;
111
+ background: var(--booking-stone-50, #fafaf9) !important;
112
+ box-shadow: none !important;
113
+ overflow: visible;
114
+ transition: border-color 0.15s, background-color 0.15s;
115
+ }
116
+
117
+ .checkoutPhoneInput:focus-within :global(.react-international-phone-input-container) {
118
+ border-color: var(--booking-emerald-600, #059669) !important;
119
+ border-width: 1px !important;
120
+ background: #fff !important;
121
+ outline: none !important;
122
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.2) !important;
123
+ }
124
+
125
+ .checkoutPhoneInput:focus-within :global(.react-international-phone-country-selector-button) {
126
+ background: transparent !important;
127
+ }
128
+
129
+ .checkoutPhoneInput :global(.react-international-phone-country-selector-button) {
130
+ border: none !important;
131
+ background: transparent !important;
132
+ padding: 0 0.375rem 0 0.5rem !important;
133
+ height: auto !important;
134
+ align-self: stretch;
135
+ border-radius: 0 !important;
136
+ box-shadow: none !important;
137
+ }
138
+
139
+ .checkoutPhoneInput :global(.react-international-phone-country-selector-button:hover) {
140
+ background: rgba(0, 0, 0, 0.03) !important;
141
+ }
142
+
143
+ .checkoutPhoneInput :global(.react-international-phone-input) {
144
+ flex: 1;
145
+ min-width: 0;
146
+ border: none !important;
147
+ padding: 0.5rem 0.625rem 0.5rem 0.25rem !important;
148
+ font-size: 0.875rem !important;
149
+ line-height: 1.25rem !important;
150
+ color: var(--booking-stone-900, #1c1917) !important;
151
+ background: transparent !important;
152
+ }
153
+
154
+ .checkoutPhoneInput :global(.react-international-phone-input:focus),
155
+ .checkoutPhoneInput :global(.react-international-phone-input:focus-visible) {
156
+ outline: none !important;
157
+ border: none !important;
158
+ box-shadow: none !important;
159
+ }
160
+
161
+ .checkoutPhoneInput :global(.react-international-phone-country-selector-button:focus),
162
+ .checkoutPhoneInput :global(.react-international-phone-country-selector-button:focus-visible) {
163
+ outline: none !important;
164
+ box-shadow: none !important;
165
+ }
166
+
167
+ .checkoutPhoneInput :global(.react-international-phone-country-selector-dropdown) {
168
+ z-index: 1000;
64
169
  }
65
170
 
66
171
  .contactStaticValue {
@@ -7,6 +7,7 @@ import { PickupLocationSelector } from './PickupLocationSelector';
7
7
  import type { Currency } from './CurrencySwitcher';
8
8
  import type { PickupLocation, Destination } from '../../lib/booking-api';
9
9
  import styles from './CheckoutForm.module.css';
10
+ import { CheckoutOptionalPhoneFields } from './CheckoutOptionalPhoneFields';
10
11
  import { useBookingHost } from '../../runtime';
11
12
  import { AnimatePresence, motion } from 'framer-motion';
12
13
 
@@ -41,9 +42,11 @@ interface CheckoutFormProps {
41
42
  firstName: string;
42
43
  lastName: string;
43
44
  email: string;
45
+ phoneNumber?: string;
44
46
  onFirstNameChange: (value: string) => void;
45
47
  onLastNameChange: (value: string) => void;
46
48
  onEmailChange: (value: string) => void;
49
+ onPhoneNumberChange?: (value: string) => void;
47
50
  readOnlyContactFields?: boolean;
48
51
  // Pickup location
49
52
  pickupLocations?: PickupLocation[];
@@ -104,9 +107,11 @@ export function CheckoutForm({
104
107
  firstName,
105
108
  lastName,
106
109
  email,
110
+ phoneNumber = '',
107
111
  onFirstNameChange,
108
112
  onLastNameChange,
109
113
  onEmailChange,
114
+ onPhoneNumberChange,
110
115
  readOnlyContactFields = false,
111
116
  pickupLocations,
112
117
  destinations = [],
@@ -255,9 +260,16 @@ export function CheckoutForm({
255
260
  className={styles.input}
256
261
  />
257
262
  {email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && (
258
- <p className={styles.inputError}>{t('booking.invalidEmail') || 'Please enter a valid email address'}</p>
263
+ <div className={styles.inputError} role="alert">
264
+ {t('booking.invalidEmail') || 'Please enter a valid email address'}
265
+ </div>
259
266
  )}
260
267
  </div>
268
+ <CheckoutOptionalPhoneFields
269
+ phoneNumber={phoneNumber}
270
+ onPhoneNumberChange={onPhoneNumberChange}
271
+ t={t}
272
+ />
261
273
  </>
262
274
  )}
263
275
  </div>
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useBookingHost } from '../../runtime';
4
+ import styles from './CheckoutForm.module.css';
5
+
6
+ type TranslationFn = (key: string, params?: Record<string, string>) => string;
7
+
8
+ export interface CheckoutOptionalPhoneFieldsProps {
9
+ phoneNumber: string;
10
+ onPhoneNumberChange?: (value: string) => void;
11
+ t: TranslationFn;
12
+ idPrefix?: string;
13
+ labelClassName?: string;
14
+ }
15
+
16
+ export function CheckoutOptionalPhoneFields({
17
+ phoneNumber,
18
+ onPhoneNumberChange,
19
+ t,
20
+ idPrefix = 'booking',
21
+ labelClassName = styles.label,
22
+ }: CheckoutOptionalPhoneFieldsProps) {
23
+ const { slots } = useBookingHost();
24
+ const PhoneInput = slots.PhoneInput;
25
+ const phonePlaceholder =
26
+ t('booking.phoneOptionalPlaceholder') || 'e.g. +1 403 555 0123';
27
+ const fieldId = `${idPrefix}-phone`;
28
+
29
+ return (
30
+ <div className={styles.optionalPhoneField}>
31
+ <label className={labelClassName} htmlFor={fieldId}>
32
+ {t('booking.phoneOptional') || 'Phone'}
33
+ </label>
34
+ {PhoneInput ? (
35
+ <PhoneInput
36
+ id={fieldId}
37
+ value={phoneNumber}
38
+ onChange={(v: string | undefined) => onPhoneNumberChange?.(v ?? '')}
39
+ placeholder={phonePlaceholder}
40
+ className={styles.checkoutPhoneInput}
41
+ />
42
+ ) : (
43
+ <input
44
+ type="tel"
45
+ id={fieldId}
46
+ value={phoneNumber}
47
+ onChange={(e) => onPhoneNumberChange?.(e.target.value)}
48
+ placeholder={phonePlaceholder}
49
+ autoComplete="tel"
50
+ className={styles.input}
51
+ />
52
+ )}
53
+ <div className={styles.optionalPhoneFieldHint} role="note">
54
+ {t('booking.phoneOptionalHint') || 'For pickup and day-of updates'}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -24,12 +24,14 @@ export function DapTourDescription({
24
24
  }
25
25
 
26
26
  return (
27
- <TourDescription
28
- paragraphs={content.paragraphs}
29
- review={content.review}
30
- sections={content.sections}
31
- defaultExpanded={false}
32
- toggleLabel={t('booking.seeFullAddOnDescription')}
33
- />
27
+ <div>
28
+ <TourDescription
29
+ paragraphs={content.paragraphs}
30
+ review={content.review}
31
+ sections={content.sections}
32
+ defaultExpanded={false}
33
+ toggleLabel={t('booking.seeFullAddOnDescription')}
34
+ />
35
+ </div>
34
36
  );
35
37
  }
@@ -467,13 +467,42 @@ export default function DependentAddOnBookingDialog() {
467
467
  }, [isOpen]);
468
468
 
469
469
  useEffect(() => {
470
- if (isOpen) {
471
- document.body.style.overflow = 'hidden';
470
+ if (!isOpen) return;
471
+
472
+ const inheritedScrollLock = document.body.dataset.vviaScrollLockScrollY;
473
+ if (inheritedScrollLock != null) {
472
474
  return () => {
473
- document.body.style.overflow = 'unset';
475
+ document.body.style.overflow = document.body.dataset.vviaScrollLockOverflow ?? '';
476
+ document.body.style.position = document.body.dataset.vviaScrollLockPosition ?? '';
477
+ document.body.style.top = document.body.dataset.vviaScrollLockTop ?? '';
478
+ document.body.style.width = document.body.dataset.vviaScrollLockWidth ?? '';
479
+ document.body.style.paddingRight = document.body.dataset.vviaScrollLockPaddingRight ?? '';
480
+ delete document.body.dataset.vviaScrollLockScrollY;
481
+ delete document.body.dataset.vviaScrollLockOverflow;
482
+ delete document.body.dataset.vviaScrollLockPosition;
483
+ delete document.body.dataset.vviaScrollLockTop;
484
+ delete document.body.dataset.vviaScrollLockWidth;
485
+ delete document.body.dataset.vviaScrollLockPaddingRight;
474
486
  };
475
487
  }
476
- document.body.style.overflow = 'unset';
488
+
489
+ const previousOverflow = document.body.style.overflow;
490
+ const previousPosition = document.body.style.position;
491
+ const previousTop = document.body.style.top;
492
+ const previousWidth = document.body.style.width;
493
+ const previousPaddingRight = document.body.style.paddingRight;
494
+ const scrollbarWidth = Math.max(0, window.innerWidth - document.documentElement.clientWidth);
495
+ document.body.style.overflow = 'hidden';
496
+ if (scrollbarWidth > 0 && !previousPaddingRight) {
497
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
498
+ }
499
+ return () => {
500
+ document.body.style.overflow = previousOverflow;
501
+ document.body.style.position = previousPosition;
502
+ document.body.style.top = previousTop;
503
+ document.body.style.width = previousWidth;
504
+ document.body.style.paddingRight = previousPaddingRight;
505
+ };
477
506
  }, [isOpen]);
478
507
 
479
508
  const clearPaymentPrepForSlotChange = useCallback(() => {
@@ -836,9 +865,10 @@ export default function DependentAddOnBookingDialog() {
836
865
 
837
866
  const dapCheckoutSummary = useMemo(() => {
838
867
  if (!payload || !selectedSlot) return null;
868
+ const receiptTitle = payload.receiptDisplayTitle?.trim() || payload.productDisplayTitle;
839
869
  const lineTitle = sessionLengthLabel
840
- ? `${payload.productDisplayTitle} (${sessionLengthLabel})`
841
- : payload.productDisplayTitle;
870
+ ? `${receiptTitle} - ${sessionLengthLabel}`
871
+ : receiptTitle;
842
872
  const subtotal =
843
873
  paymentSubtotalAmount ??
844
874
  (selectedSlot.price != null ? selectedSlot.price * DAP_SLOT_QUANTITY : 0);
@@ -1053,8 +1083,13 @@ export default function DependentAddOnBookingDialog() {
1053
1083
  }
1054
1084
  >
1055
1085
  <span className={bookingStyles.dapSessionOptionTitle}>
1056
- {opt.label}
1086
+ {opt.name ?? opt.label}
1057
1087
  </span>
1088
+ {opt.name && opt.label ? (
1089
+ <span className={bookingStyles.dapSessionOptionMeta}>
1090
+ {opt.label}
1091
+ </span>
1092
+ ) : null}
1058
1093
  {opt.photosLabel ? (
1059
1094
  <span className={bookingStyles.dapSessionOptionMeta}>
1060
1095
  {opt.photosLabel}