@ticketboothapp/booking 1.2.188 → 1.2.190

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/AddOnsSection.module.css +124 -0
  3. package/src/components/booking/AddOnsSection.tsx +17 -21
  4. package/src/components/booking/AdminChangeBookingContent.tsx +1 -0
  5. package/src/components/booking/AdminChangeBookingFlow.tsx +1 -0
  6. package/src/components/booking/AdminChangeReceiptComparison.tsx +3 -3
  7. package/src/components/booking/Calendar.module.css +124 -10
  8. package/src/components/booking/Calendar.tsx +46 -20
  9. package/src/components/booking/CancellationPolicySelector.module.css +33 -4
  10. package/src/components/booking/CancellationPolicySelector.tsx +30 -28
  11. package/src/components/booking/ChangeBookingFlow.tsx +1 -0
  12. package/src/components/booking/ChangeBookingItineraryPanel.tsx +3 -0
  13. package/src/components/booking/CollapsibleAddOnItem.module.css +1 -1
  14. package/src/components/booking/ItineraryBox.module.css +14 -0
  15. package/src/components/booking/ItineraryBox.tsx +75 -7
  16. package/src/components/booking/MealDrinkAddOnSelector.tsx +10 -12
  17. package/src/components/booking/NewBookingFlow.tsx +1 -0
  18. package/src/components/booking/PrivateShuttlePassengerSection.tsx +7 -1
  19. package/src/components/booking/ReturnTimeSelector.module.css +23 -4
  20. package/src/components/booking/ReturnTimeSelector.tsx +5 -5
  21. package/src/components/booking/StandardBookingItineraryPanel.tsx +3 -0
  22. package/src/components/booking/TicketSelector.module.css +30 -3
  23. package/src/components/booking/TicketSelector.tsx +6 -6
  24. package/src/hooks/useIsBookingLaunchLive.ts +4 -1
  25. package/src/lib/booking/i18n/messages/en.json +2 -0
  26. package/src/lib/booking/i18n/messages/fr.json +2 -0
  27. package/src/lib/booking-constants.ts +1 -1
  28. package/CHANGE_BOOKING_BE_HANDOFF.md +0 -160
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "1.2.188",
3
+ "version": "1.2.190",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -39,3 +39,127 @@
39
39
  line-height: 1.5;
40
40
  color: var(--booking-stone-500, #78716c);
41
41
  }
42
+
43
+ .optionRow {
44
+ display: grid;
45
+ grid-template-columns: minmax(0, 1fr) auto auto;
46
+ grid-template-rows: auto auto;
47
+ align-items: center;
48
+ gap: 0.25rem 0.75rem;
49
+ padding: 0.75rem;
50
+ background: var(--booking-stone-50, #fafaf9);
51
+ border-radius: 0.5rem;
52
+ }
53
+
54
+ .optionLabel {
55
+ grid-column: 1;
56
+ grid-row: 1;
57
+ min-width: 0;
58
+ font-size: 0.875rem;
59
+ color: var(--booking-stone-800, #292524);
60
+ }
61
+
62
+ .optionPrice {
63
+ grid-column: 2;
64
+ grid-row: 1 / 3;
65
+ align-self: center;
66
+ flex-shrink: 0;
67
+ font-size: 0.875rem;
68
+ color: var(--booking-stone-500, #78716c);
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .optionControls {
73
+ grid-column: 3;
74
+ grid-row: 1 / 3;
75
+ align-self: center;
76
+ display: flex;
77
+ flex-shrink: 0;
78
+ align-items: center;
79
+ gap: 0.5rem;
80
+ }
81
+
82
+ .optionDescription {
83
+ grid-column: 1;
84
+ grid-row: 2;
85
+ min-width: 0;
86
+ font-size: 0.75rem;
87
+ font-weight: 400;
88
+ line-height: 1.375;
89
+ color: var(--booking-stone-500, #78716c);
90
+ }
91
+
92
+ .choiceRow {
93
+ display: grid;
94
+ grid-template-columns: minmax(0, 1fr) auto;
95
+ grid-template-rows: auto auto;
96
+ align-items: center;
97
+ gap: 0.25rem 0.75rem;
98
+ width: 100%;
99
+ padding: 0.75rem;
100
+ border-width: 2px;
101
+ border-radius: 0.5rem;
102
+ text-align: left;
103
+ transition: border-color 0.2s, background-color 0.2s;
104
+ }
105
+
106
+ .choiceLabel {
107
+ grid-column: 1;
108
+ grid-row: 1;
109
+ min-width: 0;
110
+ font-size: 0.875rem;
111
+ font-weight: 500;
112
+ color: var(--booking-stone-800, #292524);
113
+ }
114
+
115
+ .choicePrice {
116
+ grid-column: 2;
117
+ grid-row: 1 / 3;
118
+ align-self: center;
119
+ font-size: 0.875rem;
120
+ color: var(--booking-stone-500, #78716c);
121
+ white-space: nowrap;
122
+ }
123
+
124
+ .choiceDescription {
125
+ grid-column: 1;
126
+ grid-row: 2;
127
+ min-width: 0;
128
+ font-size: 0.75rem;
129
+ font-weight: 400;
130
+ line-height: 1.375;
131
+ color: var(--booking-stone-500, #78716c);
132
+ }
133
+
134
+ @media (max-width: 639px) {
135
+ .optionRow {
136
+ grid-template-columns: minmax(0, 1fr) auto;
137
+ grid-template-rows: auto auto auto;
138
+ }
139
+
140
+ .optionPrice {
141
+ grid-column: 1;
142
+ grid-row: 2;
143
+ justify-self: start;
144
+ }
145
+
146
+ .optionControls {
147
+ grid-column: 2;
148
+ grid-row: 1 / 3;
149
+ }
150
+
151
+ .optionDescription {
152
+ grid-column: 1 / -1;
153
+ grid-row: 3;
154
+ margin-top: 0.125rem;
155
+ }
156
+
157
+ .choiceDescription {
158
+ grid-column: 1 / -1;
159
+ margin-top: 0.125rem;
160
+ }
161
+
162
+ .choicePrice {
163
+ grid-row: 1;
164
+ }
165
+ }
@@ -159,19 +159,12 @@ export function AddOnsSection({
159
159
  const canDecrement = qty > 0 && currentTotalForAddOn > minTotalForAddOn;
160
160
  const price = (addOn.price ?? 0) + (variant.priceAdjustment ?? 0);
161
161
  return (
162
- <div key={variant.id} className="flex flex-wrap items-center gap-3 p-3 bg-stone-50 rounded-lg">
163
- <span className="min-w-0 flex-1">
164
- <span className="block text-sm text-stone-800">{variant.label}</span>
165
- {variant.description?.trim() ? (
166
- <span className="mt-0.5 block text-xs leading-snug text-stone-500">
167
- {variant.description.trim()}
168
- </span>
169
- ) : null}
170
- </span>
171
- <span className="text-sm text-stone-500 shrink-0">
162
+ <div key={variant.id} className={styles.optionRow}>
163
+ <span className={styles.optionLabel}>{variant.label}</span>
164
+ <span className={styles.optionPrice}>
172
165
  {suppressPrices ? '—' : `${formatCurrencyAmount(price, currency, locale as 'en' | 'fr')} ea`}
173
166
  </span>
174
- <div className="flex items-center gap-2 shrink-0">
167
+ <div className={styles.optionControls}>
175
168
  <button
176
169
  type="button"
177
170
  onClick={() => {
@@ -220,6 +213,11 @@ export function AddOnsSection({
220
213
  +
221
214
  </button>
222
215
  </div>
216
+ {variant.description?.trim() ? (
217
+ <span className={styles.optionDescription}>
218
+ {variant.description.trim()}
219
+ </span>
220
+ ) : null}
223
221
  </div>
224
222
  );
225
223
  })}
@@ -261,23 +259,21 @@ export function AddOnsSection({
261
259
  ];
262
260
  });
263
261
  }}
264
- className={`flex items-center justify-between w-full p-3 rounded-lg border-2 text-left transition-colors ${
262
+ className={`${styles.choiceRow} ${
265
263
  isSelected
266
264
  ? 'border-emerald-500 bg-emerald-50'
267
265
  : 'border-stone-200 bg-stone-50 hover:border-stone-300'
268
266
  }`}
269
267
  >
270
- <span className="min-w-0 flex-1">
271
- <span className="block text-sm font-medium text-stone-800">{variant.label}</span>
272
- {variant.description?.trim() ? (
273
- <span className="mt-0.5 block text-xs font-normal leading-snug text-stone-500">
274
- {variant.description.trim()}
275
- </span>
276
- ) : null}
277
- </span>
278
- <span className="text-sm text-stone-500">
268
+ <span className={styles.choiceLabel}>{variant.label}</span>
269
+ <span className={styles.choicePrice}>
279
270
  {suppressPrices ? '—' : `+${formatCurrencyAmount(price, currency, locale as 'en' | 'fr')}`}
280
271
  </span>
272
+ {variant.description?.trim() ? (
273
+ <span className={styles.choiceDescription}>
274
+ {variant.description.trim()}
275
+ </span>
276
+ ) : null}
281
277
  </button>
282
278
  );
283
279
  })}
@@ -86,6 +86,7 @@ const calendarPropKeys = [
86
86
  ] as const;
87
87
 
88
88
  const itineraryPropKeys = [
89
+ 'productId',
89
90
  'selectedDate',
90
91
  'hideItineraryBox',
91
92
  'activeOptions',
@@ -1238,6 +1238,7 @@ export function AdminChangeBookingFlow({
1238
1238
  ? getCalendarEffectiveOutboundVacancies
1239
1239
  : undefined,
1240
1240
  hideItineraryBox, activeOptions,
1241
+ productId: product.productId,
1241
1242
  destinationsCount: product.destinations?.length ?? 0, pickupLocationsCount: product.pickupLocations?.length ?? 0,
1242
1243
  getItineraryItems: computeItineraryDisplay,
1243
1244
  ...{ isItinerarySticky, isMobile, showTooltip, selectedPickupLocation, itineraryRef },
@@ -378,10 +378,10 @@ export function AdminChangeReceiptComparison({
378
378
  <button
379
379
  type="button"
380
380
  aria-pressed={preserveExistingReceipt}
381
- className={`mt-3 inline-flex w-full items-center justify-center rounded-lg border px-3 py-2 text-sm font-semibold shadow-sm transition sm:w-auto ${
381
+ className={`mt-1.5 inline-flex text-sm font-semibold underline decoration-1 underline-offset-4 transition ${
382
382
  preserveExistingReceipt
383
- ? 'border-sky-700 bg-sky-700 text-white hover:bg-sky-800'
384
- : 'border-sky-300 bg-white text-sky-800 hover:border-sky-500 hover:bg-sky-50'
383
+ ? 'text-emerald-800 decoration-emerald-700 hover:text-emerald-950'
384
+ : 'text-emerald-700 decoration-emerald-600 hover:text-emerald-900'
385
385
  }`}
386
386
  onClick={() => onPreserveExistingReceiptChange(!preserveExistingReceipt)}
387
387
  >
@@ -71,10 +71,11 @@
71
71
  --cal-soldout-selected-border: rgba(248, 113, 113, 0.5);
72
72
 
73
73
  /* Discount badge */
74
- --cal-discount-bg: #fef2f2;
75
- --cal-discount-text: #b91c1c;
76
- --cal-discount-tag-bg: rgba(239, 68, 68, 0.9);
77
- --cal-discount-tag-text: #fff;
74
+ --cal-discount-bg: #fff0e8;
75
+ --cal-discount-text: #a93600;
76
+ --cal-discount-tag-bg: #fff0e8;
77
+ --cal-discount-tag-text: #a93600;
78
+ --cal-discount-tag-border: var(--accent-orange, #ff4d00);
78
79
 
79
80
  /* Today marker */
80
81
  --cal-today-ring: var(--booking-orange-600, #ff4d00);
@@ -505,6 +506,19 @@
505
506
  padding: 0.5rem 0.5rem 0.125rem;
506
507
  }
507
508
 
509
+ /* Minimum-passenger tours need a second line below the departure time on mobile. */
510
+ .calendarDayCell.calendarDayCellMobilePax {
511
+ min-height: 4.75rem;
512
+ padding-right: 0.125rem;
513
+ padding-left: 0.125rem;
514
+ }
515
+
516
+ /* Status badges need the cell width; the standard mobile gutters are sized for dot/time lists. */
517
+ .calendarDayCell.calendarDayCellMobileStatus {
518
+ padding-right: 0.125rem;
519
+ padding-left: 0.125rem;
520
+ }
521
+
508
522
  .calendarDayCell:disabled {
509
523
  cursor: not-allowed;
510
524
  }
@@ -586,19 +600,55 @@
586
600
  right: 0.25rem;
587
601
  display: flex;
588
602
  align-items: center;
589
- gap: 0.125rem;
590
603
  background-color: var(--cal-discount-tag-bg);
591
604
  color: var(--cal-discount-tag-text);
605
+ border: 1px solid var(--cal-discount-tag-border);
606
+ box-shadow: 0 1px 3px rgba(255, 77, 0, 0.18);
592
607
  font-size: 9px;
593
- font-weight: 700;
594
- padding: 0.125rem 0.25rem;
608
+ font-weight: 800;
609
+ line-height: 1.1;
610
+ padding: 0.25rem 0.375rem;
595
611
  border-radius: 0.25rem;
612
+ white-space: nowrap;
613
+ }
614
+
615
+ .calendarDiscountOff {
616
+ margin-left: 0.1875rem;
596
617
  }
618
+
597
619
  /* Mobile: position discount below day number (top-left) so it doesn't overlap */
598
620
  .calendarDiscountTagMobile {
599
621
  top: 1rem;
600
622
  left: 0.25rem;
601
- right: auto;
623
+ right: 0.25rem;
624
+ flex-direction: column;
625
+ justify-content: center;
626
+ padding: 0.25rem;
627
+ font-size: 9px;
628
+ line-height: 1;
629
+ text-align: center;
630
+ white-space: normal;
631
+ }
632
+
633
+ .calendarDiscountTagMobile .calendarDiscountOff {
634
+ margin-left: 0;
635
+ }
636
+
637
+ @media (min-width: 640px) {
638
+ .calendarDiscountTag {
639
+ border-width: 1.5px;
640
+ font-size: 10px;
641
+ }
642
+ }
643
+
644
+ .calendarAvailabilityIndicators {
645
+ min-width: 0;
646
+ }
647
+
648
+ @media (min-width: 640px) {
649
+ .calendarAvailabilityIndicatorsWithDiscount {
650
+ padding-top: 1.625rem;
651
+ }
602
652
  }
603
653
 
604
654
  .calendarSoldOutBadge {
@@ -613,6 +663,14 @@
613
663
  color: var(--cal-soldout-text);
614
664
  border: 1px solid var(--cal-soldout-border);
615
665
  }
666
+
667
+ @media (max-width: 639px) {
668
+ .calendarDayCellMobile .calendarSoldOutBadge,
669
+ .calendarDayCellMobileTall .calendarSoldOutBadge {
670
+ margin-top: 1rem;
671
+ margin-bottom: 0.25rem;
672
+ }
673
+ }
616
674
  .calendarDayCellSelected .calendarSoldOutBadge {
617
675
  background-color: var(--cal-soldout-selected-bg);
618
676
  color: var(--cal-cell-selected-text);
@@ -636,6 +694,34 @@
636
694
  color: var(--cal-cell-selected-text);
637
695
  }
638
696
 
697
+ @media (max-width: 639px) {
698
+ .calendarStatusPillMobile {
699
+ max-width: 100%;
700
+ min-width: 0;
701
+ margin-top: 1rem;
702
+ margin-bottom: 0.25rem;
703
+ padding: 0.25rem 0.5rem;
704
+ border: 0;
705
+ background-color: var(--cal-time-available-bg);
706
+ color: var(--cal-time-available-text);
707
+ font-size: 9px;
708
+ font-weight: 500;
709
+ line-height: 1;
710
+ letter-spacing: 0;
711
+ white-space: nowrap;
712
+ }
713
+
714
+ .calendarStatusPillMobileWithDiscount {
715
+ margin-top: 2.25rem;
716
+ }
717
+
718
+ .calendarDayCellSelected .calendarStatusPillMobile {
719
+ border-color: rgba(255, 255, 255, 0.8);
720
+ background-color: rgba(255, 255, 255, 0.18);
721
+ color: var(--cal-cell-selected-text);
722
+ }
723
+ }
724
+
639
725
  .calendarTimePillLow {
640
726
  background-color: var(--cal-time-low-bg);
641
727
  color: var(--cal-time-low-text);
@@ -703,10 +789,12 @@
703
789
  align-items: center;
704
790
  gap: 0.0625rem;
705
791
  width: 100%;
792
+ margin-top: 1rem;
793
+ margin-bottom: 0.25rem;
706
794
  }
707
795
  /* Mobile: push time slots lower when discount tag is present */
708
796
  .calendarMobileTimeListWithDiscount {
709
- margin-top: 1.25rem;
797
+ margin-top: 2.25rem;
710
798
  }
711
799
 
712
800
  .calendarMobileTimeDot {
@@ -719,14 +807,40 @@
719
807
  line-height: 1.2;
720
808
  }
721
809
 
810
+ .calendarMobileTimeDotWithPax {
811
+ flex-direction: column;
812
+ align-items: center;
813
+ width: 100%;
814
+ row-gap: 0.125rem;
815
+ }
816
+
817
+ .calendarMobileTimeRow {
818
+ display: flex;
819
+ align-items: center;
820
+ justify-content: center;
821
+ gap: 0.25rem;
822
+ }
823
+
824
+ .calendarMobileTimeLabel {
825
+ min-width: 0;
826
+ white-space: nowrap;
827
+ }
828
+
722
829
  .calendarMobilePaxProgress {
830
+ align-self: center;
723
831
  color: var(--booking-stone-600, #57534e);
724
- font-size: 8px;
832
+ max-width: 100%;
833
+ font-size: 7px;
725
834
  font-weight: 700;
835
+ line-height: 1;
836
+ text-align: center;
726
837
  white-space: nowrap;
727
838
  }
728
839
  .calendarMobilePaxProgressReached {
729
840
  color: #047857;
841
+ max-width: 2.75rem;
842
+ line-height: 1.05;
843
+ white-space: normal;
730
844
  }
731
845
  .calendarMobilePaxProgressReached::before {
732
846
  content: "✓ ";
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useMemo, useRef, useEffect, useCallback, memo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { startOfWeek, addDays, addWeeks, subWeeks, isSameDay, parseISO, startOfMonth, endOfMonth, eachDayOfInterval, addMonths, subMonths } from 'date-fns';
5
+ import { addDays, addWeeks, subWeeks, isSameDay, parseISO, startOfMonth, endOfMonth, addMonths, subMonths } from 'date-fns';
6
6
  import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
7
7
  import { enUS, fr } from 'date-fns/locale';
8
8
  import type { Availability } from '../../lib/booking-api';
@@ -205,6 +205,7 @@ const DateCell = memo(function DateCell({
205
205
  const { locale } = useLocale();
206
206
  const dateFnsLocale = dateFnsLocales[locale] || enUS;
207
207
  const dayNumber = formatInTimeZone(date, timezone, 'd');
208
+ const fullDateLabel = formatInTimeZone(date, timezone, 'EEEE, MMMM d, yyyy', { locale: dateFnsLocale });
208
209
  const allowPinnedSoldOutDate = Boolean(selectableSoldOutDate && selectableSoldOutDate === dateStr);
209
210
  const hasAvailabilityData = availability !== null;
210
211
  // When showCapacity (admin), allow selecting sold-out slots for overbooking.
@@ -233,12 +234,16 @@ const DateCell = memo(function DateCell({
233
234
  // Admin capacity line under each time pill needs extra vertical room (provider dashboard only).
234
235
  const slotCount = availability?.startTimes?.length ?? 0;
235
236
  const needsTallerCell = !isMobile && slotCount > 2;
236
- const mobileNeedsTaller = isMobile && slotCount > 2;
237
+ const hasMobilePaxProgress =
238
+ isMobile && Boolean(availability?.timeBookingProgress && Object.keys(availability.timeBookingProgress).length > 0);
239
+ const mobileNeedsTaller = isMobile && (slotCount > 2 || hasMobilePaxProgress);
237
240
  const adminCapacityTallCells =
238
241
  Boolean(showCapacity) && !isMobile && displayMode === 'times';
239
242
  const buttonClassName = useMemo(() => cn(
240
243
  styles.calendarDayCell,
241
244
  isMobile && (mobileNeedsTaller ? styles.calendarDayCellMobileTall : styles.calendarDayCellMobile),
245
+ hasMobilePaxProgress && styles.calendarDayCellMobilePax,
246
+ isMobile && displayMode === 'status' && styles.calendarDayCellMobileStatus,
242
247
  !isMobile && (
243
248
  adminCapacityTallCells
244
249
  ? needsTallerCell
@@ -256,16 +261,17 @@ const DateCell = memo(function DateCell({
256
261
  ? styles.calendarDayCellSelected
257
262
  : styles.calendarDayCellAvailable,
258
263
  isToday && !isPast && !isSelected && styles.calendarDayCellToday
259
- ), [isPast, isDisabled, isSelected, isToday, needsTallerCell, isMobile, mobileNeedsTaller, adminCapacityTallCells]);
264
+ ), [isPast, isDisabled, isSelected, isToday, needsTallerCell, isMobile, mobileNeedsTaller, hasMobilePaxProgress, adminCapacityTallCells, displayMode]);
260
265
 
261
266
  const ariaLabel = useMemo(() => {
262
- const parts = [dayNumber];
267
+ const parts = [fullDateLabel];
263
268
  if (isPast) parts.push(' - Past date');
264
269
  if (availability?.isSoldOut) parts.push(' - Sold out');
270
+ else if (availability) parts.push(` - ${t('calendar.available')}`);
265
271
  const pct = availability?.totalDiscountPercent;
266
272
  if (pct != null && pct > 0) parts.push(` - ${pct}% off`);
267
273
  return parts.join('');
268
- }, [dayNumber, isPast, availability?.isSoldOut, availability?.totalDiscountPercent]);
274
+ }, [fullDateLabel, isPast, availability, t]);
269
275
 
270
276
  return (
271
277
  <button
@@ -273,6 +279,7 @@ const DateCell = memo(function DateCell({
273
279
  disabled={isDisabled}
274
280
  className={buttonClassName}
275
281
  aria-label={ariaLabel}
282
+ aria-pressed={isSelected}
276
283
  >
277
284
  <div className={cn(styles.calendarDayCellInner, isMobile && styles.calendarDayCellInnerMobile)}>
278
285
  {/* Day Number - on mobile, in a top row with discount below to avoid overlap */}
@@ -281,12 +288,10 @@ const DateCell = memo(function DateCell({
281
288
  </div>
282
289
 
283
290
  {/* Discount Tag - top-right on desktop; below day number on mobile to avoid overlap */}
284
- {availability?.totalDiscountPercent && availability.totalDiscountPercent > 0 && (
291
+ {availability?.totalDiscountPercent && availability.totalDiscountPercent > 0 && !availability.isSoldOut && (
285
292
  <div className={cn(styles.calendarDiscountTag, isMobile && styles.calendarDiscountTagMobile)}>
286
- <svg className="w-2.5 h-2.5" fill="currentColor" viewBox="0 0 20 20">
287
- <path fillRule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
288
- </svg>
289
- <span>-{availability.totalDiscountPercent}%</span>
293
+ <span>{availability.totalDiscountPercent}%</span>
294
+ <span className={styles.calendarDiscountOff}>OFF</span>
290
295
  </div>
291
296
  )}
292
297
 
@@ -294,7 +299,14 @@ const DateCell = memo(function DateCell({
294
299
  {availability && (
295
300
  <div
296
301
  className={cn(
297
- 'flex flex-col items-center space-y-0 w-full px-0.5',
302
+ styles.calendarAvailabilityIndicators,
303
+ !isMobile &&
304
+ availability.totalDiscountPercent != null &&
305
+ availability.totalDiscountPercent > 0 &&
306
+ !availability.isSoldOut &&
307
+ styles.calendarAvailabilityIndicatorsWithDiscount,
308
+ 'flex flex-col items-center space-y-0 w-full',
309
+ isMobile && displayMode === 'status' ? 'px-0' : 'px-0.5',
298
310
  isMobile ? 'justify-start' : 'justify-end'
299
311
  )}
300
312
  >
@@ -307,8 +319,15 @@ const DateCell = memo(function DateCell({
307
319
  <>
308
320
  {/* Mobile: green dot + time (compact). Desktop: full time pills */}
309
321
  {displayMode === 'status' ? (
310
- <div className={styles.calendarTimePill}>
311
- {t('calendar.available')}
322
+ <div className={cn(
323
+ styles.calendarTimePill,
324
+ isMobile && styles.calendarStatusPillMobile,
325
+ isMobile &&
326
+ availability.totalDiscountPercent != null &&
327
+ availability.totalDiscountPercent > 0 &&
328
+ styles.calendarStatusPillMobileWithDiscount
329
+ )}>
330
+ {isMobile ? t('calendar.open') : t('calendar.available')}
312
331
  </div>
313
332
  ) : availability.startTimes && availability.startTimes.length > 0 ? (
314
333
  isMobile ? (
@@ -333,13 +352,18 @@ const DateCell = memo(function DateCell({
333
352
  key={time}
334
353
  className={cn(
335
354
  styles.calendarMobileTimeDot,
355
+ paxProgress && !isTimeSoldOut && !isLowAvailability && styles.calendarMobileTimeDotWithPax,
336
356
  isTimeSoldOut && styles.calendarMobileTimeDotSoldOut,
337
357
  isLowAvailability && !isTimeSoldOut && styles.calendarMobileTimeDotLow,
338
358
  showMinimumReached && !isTimeSoldOut && styles.calendarMobileTimeDotMinimumReached
339
359
  )}
340
360
  >
341
- <span className={styles.calendarMobileTimeDotBullet} aria-hidden />
342
- <span>{isDailyAvailability ? t('calendar.available') : formatTime(time)}</span>
361
+ <span className={styles.calendarMobileTimeRow}>
362
+ <span className={styles.calendarMobileTimeDotBullet} aria-hidden />
363
+ <span className={styles.calendarMobileTimeLabel}>
364
+ {isDailyAvailability ? t('calendar.available') : formatTime(time)}
365
+ </span>
366
+ </span>
343
367
  {paxProgress && !isTimeSoldOut && !isLowAvailability && (
344
368
  <span
345
369
  className={cn(
@@ -348,7 +372,7 @@ const DateCell = memo(function DateCell({
348
372
  )}
349
373
  >
350
374
  {showMinimumReached
351
- ? t('calendar.minimumReached')
375
+ ? t('calendar.minimumReachedShort')
352
376
  : t('calendar.paxProgress', {
353
377
  booked: paxProgress.booked,
354
378
  minimum: paxProgress.minimum,
@@ -360,8 +384,10 @@ const DateCell = memo(function DateCell({
360
384
  })}
361
385
  {availability.startTimes.length > 3 && (
362
386
  <div className={styles.calendarMobileTimeDot}>
363
- <span className={styles.calendarMobileTimeDotBullet} aria-hidden />
364
- <span>+{availability.startTimes.length - 3}</span>
387
+ <span className={styles.calendarMobileTimeRow}>
388
+ <span className={styles.calendarMobileTimeDotBullet} aria-hidden />
389
+ <span>+{availability.startTimes.length - 3}</span>
390
+ </span>
365
391
  </div>
366
392
  )}
367
393
  </div>
@@ -696,7 +722,7 @@ export function Calendar({
696
722
  const lastAvailableDate = availableDates[availableDates.length - 1];
697
723
  const currentEndDate = addDays(currentStartDate, WEEKS_TO_SHOW * 7 - 1);
698
724
  return lastAvailableDate > currentEndDate;
699
- }, [availableDates, currentStartDate]);
725
+ }, [availableDates, currentStartDate, WEEKS_TO_SHOW]);
700
726
 
701
727
  const lastReportedRangeRef = useRef<{ start: Date; end: Date } | null>(null);
702
728
  const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -758,7 +784,7 @@ export function Calendar({
758
784
  days.push(addDays(currentStartDate, i));
759
785
  }
760
786
  return days;
761
- }, [currentStartDate]);
787
+ }, [currentStartDate, WEEKS_TO_SHOW]);
762
788
 
763
789
  // Notify parent of visible date range changes (with buffer weeks before/after)
764
790
  // When dropdown is open, expand range to include the visible month so we fetch availability for it
@@ -30,10 +30,11 @@
30
30
 
31
31
  .btn {
32
32
  width: 100%;
33
- display: flex;
33
+ display: grid;
34
+ grid-template-columns: minmax(0, 1fr) auto;
35
+ grid-template-rows: auto auto;
34
36
  align-items: center;
35
- justify-content: space-between;
36
- gap: 0.75rem;
37
+ column-gap: 0.75rem;
37
38
  padding: 1rem 1.25rem;
38
39
  border-radius: 0.5rem;
39
40
  border: 2px solid;
@@ -85,12 +86,20 @@
85
86
  display: flex;
86
87
  align-items: center;
87
88
  gap: 0.75rem;
89
+ min-width: 0;
90
+ grid-column: 1;
91
+ grid-row: 1;
88
92
  }
89
93
 
90
- .policyContent {
94
+ .policyDetails {
91
95
  display: flex;
92
96
  flex-direction: column;
93
97
  gap: 0.125rem;
98
+ grid-column: 1;
99
+ grid-row: 2;
100
+ min-width: 0;
101
+ margin-top: 0.125rem;
102
+ margin-left: 2rem;
94
103
  }
95
104
 
96
105
  .policyLabel {
@@ -110,10 +119,30 @@
110
119
  }
111
120
 
112
121
  .fee {
122
+ grid-column: 2;
123
+ grid-row: 1 / 3;
124
+ align-self: center;
113
125
  font-size: 0.875rem;
114
126
  white-space: nowrap;
115
127
  }
116
128
 
129
+ .rowSubtitle {
130
+ display: block;
131
+ margin-top: 0.25rem;
132
+ }
133
+
134
+ @media (max-width: 639px) {
135
+ .policyDetails {
136
+ grid-column: 1 / -1;
137
+ margin-left: 0;
138
+ margin-top: 0.375rem;
139
+ }
140
+
141
+ .fee {
142
+ grid-row: 1;
143
+ }
144
+ }
145
+
117
146
  .feeFree {
118
147
  color: var(--booking-stone-500, #78716c);
119
148
  }