@resira/ui 0.4.14 → 0.4.16

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/index.d.cts CHANGED
@@ -519,6 +519,7 @@ interface TimeSlotPickerProps {
519
519
  showDuration?: boolean;
520
520
  selectedDuration?: number;
521
521
  onDurationChange?: (minutes: number) => void;
522
+ minPartySizeOverride?: number;
522
523
  maxPartySizeOverride?: number;
523
524
  durationPricing?: DurationPrice[];
524
525
  currency?: string;
@@ -527,7 +528,7 @@ interface TimeSlotPickerProps {
527
528
  /** Whether to show remaining-capacity labels under available slots. */
528
529
  showRemainingSpots?: boolean;
529
530
  }
530
- declare function TimeSlotPicker({ timeSlots, selectedDate, onDateChange, selectedSlot, onSlotSelect, partySize, onPartySizeChange, showPartySize, showDuration, selectedDuration, onDurationChange, maxPartySizeOverride, durationPricing, currency, fullyBookedDates, showRemainingSpots, }: TimeSlotPickerProps): react_jsx_runtime.JSX.Element;
531
+ declare function TimeSlotPicker({ timeSlots, selectedDate, onDateChange, selectedSlot, onSlotSelect, partySize, onPartySizeChange, showPartySize, showDuration, selectedDuration, onDurationChange, minPartySizeOverride, maxPartySizeOverride, durationPricing, currency, fullyBookedDates, showRemainingSpots, }: TimeSlotPickerProps): react_jsx_runtime.JSX.Element;
531
532
 
532
533
  interface ResourcePickerProps {
533
534
  /** List of resources to display. */
@@ -873,6 +874,25 @@ interface Service {
873
874
  /** Raw product object from the API. */
874
875
  raw: Product;
875
876
  }
877
+ interface ResolveServicePriceParams {
878
+ /** Selected duration in minutes (number or numeric string like "180"). */
879
+ durationMinutes?: number | string | null;
880
+ /** Party size used for per_person pricing. */
881
+ partySize?: number;
882
+ }
883
+ interface ResolvedServicePrice {
884
+ /** Unit price (one session) in cents after duration selection. */
885
+ unitPriceCents: number;
886
+ /** Final total in cents, including per_person multiplier if applicable. */
887
+ totalPriceCents: number;
888
+ /** Duration actually used to resolve the option (if any). */
889
+ matchedDurationMinutes?: number;
890
+ }
891
+ /**
892
+ * Resolve cart/summary price safely from selected duration + party size.
893
+ * Prevents falling back to base price when duration comes as a string.
894
+ */
895
+ declare function resolveServicePrice(service: Pick<Service, "pricingModel" | "priceCents" | "durationMinutes" | "options">, params?: ResolveServicePriceParams): ResolvedServicePrice;
876
896
  interface UseServicesReturn {
877
897
  /** Enriched service list. */
878
898
  services: Service[];
@@ -969,4 +989,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
969
989
  declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
970
990
  declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
971
991
 
972
- export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, type CheckoutSessionErrorCode, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, type PromoterModeConfig, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type Service, type ServiceLayout, type ServiceOption, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, type UseServicesReturn, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
992
+ export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, type CheckoutSessionErrorCode, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, type PromoterModeConfig, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, type ResolveServicePriceParams, type ResolvedServicePrice, ResourcePicker, type Service, type ServiceLayout, type ServiceOption, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, type UseServicesReturn, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveServicePrice, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
package/dist/index.d.ts CHANGED
@@ -519,6 +519,7 @@ interface TimeSlotPickerProps {
519
519
  showDuration?: boolean;
520
520
  selectedDuration?: number;
521
521
  onDurationChange?: (minutes: number) => void;
522
+ minPartySizeOverride?: number;
522
523
  maxPartySizeOverride?: number;
523
524
  durationPricing?: DurationPrice[];
524
525
  currency?: string;
@@ -527,7 +528,7 @@ interface TimeSlotPickerProps {
527
528
  /** Whether to show remaining-capacity labels under available slots. */
528
529
  showRemainingSpots?: boolean;
529
530
  }
530
- declare function TimeSlotPicker({ timeSlots, selectedDate, onDateChange, selectedSlot, onSlotSelect, partySize, onPartySizeChange, showPartySize, showDuration, selectedDuration, onDurationChange, maxPartySizeOverride, durationPricing, currency, fullyBookedDates, showRemainingSpots, }: TimeSlotPickerProps): react_jsx_runtime.JSX.Element;
531
+ declare function TimeSlotPicker({ timeSlots, selectedDate, onDateChange, selectedSlot, onSlotSelect, partySize, onPartySizeChange, showPartySize, showDuration, selectedDuration, onDurationChange, minPartySizeOverride, maxPartySizeOverride, durationPricing, currency, fullyBookedDates, showRemainingSpots, }: TimeSlotPickerProps): react_jsx_runtime.JSX.Element;
531
532
 
532
533
  interface ResourcePickerProps {
533
534
  /** List of resources to display. */
@@ -873,6 +874,25 @@ interface Service {
873
874
  /** Raw product object from the API. */
874
875
  raw: Product;
875
876
  }
877
+ interface ResolveServicePriceParams {
878
+ /** Selected duration in minutes (number or numeric string like "180"). */
879
+ durationMinutes?: number | string | null;
880
+ /** Party size used for per_person pricing. */
881
+ partySize?: number;
882
+ }
883
+ interface ResolvedServicePrice {
884
+ /** Unit price (one session) in cents after duration selection. */
885
+ unitPriceCents: number;
886
+ /** Final total in cents, including per_person multiplier if applicable. */
887
+ totalPriceCents: number;
888
+ /** Duration actually used to resolve the option (if any). */
889
+ matchedDurationMinutes?: number;
890
+ }
891
+ /**
892
+ * Resolve cart/summary price safely from selected duration + party size.
893
+ * Prevents falling back to base price when duration comes as a string.
894
+ */
895
+ declare function resolveServicePrice(service: Pick<Service, "pricingModel" | "priceCents" | "durationMinutes" | "options">, params?: ResolveServicePriceParams): ResolvedServicePrice;
876
896
  interface UseServicesReturn {
877
897
  /** Enriched service list. */
878
898
  services: Service[];
@@ -969,4 +989,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
969
989
  declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
970
990
  declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
971
991
 
972
- export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, type CheckoutSessionErrorCode, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, type PromoterModeConfig, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type Service, type ServiceLayout, type ServiceOption, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, type UseServicesReturn, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
992
+ export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, type CheckoutSessionErrorCode, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, type DeeplinkGuest, type DeeplinkSelection, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, type PromoterModeConfig, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, type ResolveServicePriceParams, type ResolvedServicePrice, ResourcePicker, type Service, type ServiceLayout, type ServiceOption, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, type UseServicesReturn, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveServicePrice, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
package/dist/index.js CHANGED
@@ -781,6 +781,28 @@ function useCheckoutSession(token) {
781
781
  }, [client, token]);
782
782
  return { session, loading, error, errorCode };
783
783
  }
784
+ function normalizeMinutes(value) {
785
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
786
+ if (typeof value === "string" && value.trim()) {
787
+ const parsed = Number(value);
788
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
789
+ }
790
+ return void 0;
791
+ }
792
+ function resolveServicePrice(service, params = {}) {
793
+ const partySize = Math.max(1, Number(params.partySize ?? 1));
794
+ const selectedMinutes = normalizeMinutes(params.durationMinutes);
795
+ const options = Array.isArray(service.options) ? service.options : [];
796
+ const matchedOption = selectedMinutes ? options.find((option) => option.durationMinutes === selectedMinutes) : void 0;
797
+ const fallbackOption = options.find((option) => option.durationMinutes === service.durationMinutes) ?? options[0];
798
+ const unitPriceCents = matchedOption?.priceCents ?? fallbackOption?.priceCents ?? service.priceCents;
799
+ const totalPriceCents = service.pricingModel === "per_person" ? unitPriceCents * partySize : unitPriceCents;
800
+ return {
801
+ unitPriceCents,
802
+ totalPriceCents,
803
+ matchedDurationMinutes: matchedOption?.durationMinutes ?? fallbackOption?.durationMinutes
804
+ };
805
+ }
784
806
  function formatPriceCentsUtil(cents, currency) {
785
807
  return new Intl.NumberFormat("default", { style: "currency", currency }).format(cents / 100);
786
808
  }
@@ -1677,6 +1699,7 @@ function TimeSlotPicker({
1677
1699
  showDuration = false,
1678
1700
  selectedDuration,
1679
1701
  onDurationChange,
1702
+ minPartySizeOverride,
1680
1703
  maxPartySizeOverride,
1681
1704
  durationPricing,
1682
1705
  currency,
@@ -1684,7 +1707,7 @@ function TimeSlotPicker({
1684
1707
  showRemainingSpots = false
1685
1708
  }) {
1686
1709
  const { locale, domainConfig, domain, classNames } = useResira();
1687
- const minParty = domainConfig.minPartySize ?? 1;
1710
+ const minParty = minPartySizeOverride ?? domainConfig.minPartySize ?? 1;
1688
1711
  const maxParty = maxPartySizeOverride ?? domainConfig.maxPartySize ?? 12;
1689
1712
  const durations = useMemo(() => {
1690
1713
  if (durationPricing && durationPricing.length > 0) {
@@ -3085,6 +3108,56 @@ function ConfirmationView({ reservation }) {
3085
3108
  ] })
3086
3109
  ] });
3087
3110
  }
3111
+
3112
+ // src/service-rules.ts
3113
+ function isPositiveInt(value) {
3114
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
3115
+ }
3116
+ function getRiderTierBounds(product) {
3117
+ const tiers = product.riderTierPricing ?? [];
3118
+ if (!tiers.length) return {};
3119
+ let min;
3120
+ let max;
3121
+ for (const tier of tiers) {
3122
+ const tierMin = isPositiveInt(tier.minRiders) ? tier.minRiders : isPositiveInt(tier.riders) ? tier.riders : void 0;
3123
+ const tierMax = isPositiveInt(tier.maxRiders) ? tier.maxRiders : isPositiveInt(tier.riders) ? tier.riders : isPositiveInt(tier.minRiders) ? tier.minRiders : void 0;
3124
+ if (isPositiveInt(tierMin)) {
3125
+ min = min == null ? tierMin : Math.min(min, tierMin);
3126
+ }
3127
+ if (isPositiveInt(tierMax)) {
3128
+ max = max == null ? tierMax : Math.max(max, tierMax);
3129
+ }
3130
+ }
3131
+ return { min, max };
3132
+ }
3133
+ function getServicePartySizeBounds(product, domainConfig) {
3134
+ const baseMin = isPositiveInt(domainConfig?.minPartySize) ? domainConfig.minPartySize : 1;
3135
+ const baseMax = isPositiveInt(domainConfig?.maxPartySize) ? domainConfig.maxPartySize : 12;
3136
+ let min = baseMin;
3137
+ let max = baseMax;
3138
+ if (product) {
3139
+ if (isPositiveInt(product.maxPartySize)) {
3140
+ max = Math.min(max, product.maxPartySize);
3141
+ }
3142
+ if (product.pricingModel === "per_rider" && product.riderTierPricing?.length) {
3143
+ const tierBounds = getRiderTierBounds(product);
3144
+ if (isPositiveInt(tierBounds.min)) {
3145
+ min = Math.max(min, tierBounds.min);
3146
+ }
3147
+ if (isPositiveInt(tierBounds.max)) {
3148
+ max = Math.min(max, tierBounds.max);
3149
+ }
3150
+ }
3151
+ }
3152
+ if (max < min) {
3153
+ return { min, max: min };
3154
+ }
3155
+ return { min, max };
3156
+ }
3157
+ function clampPartySize(value, bounds) {
3158
+ if (!Number.isFinite(value)) return bounds.min;
3159
+ return Math.min(bounds.max, Math.max(bounds.min, Math.round(value)));
3160
+ }
3088
3161
  function todayStr() {
3089
3162
  const d = /* @__PURE__ */ new Date();
3090
3163
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
@@ -3095,6 +3168,14 @@ function formatPrice5(cents, currency) {
3095
3168
  currency
3096
3169
  }).format(cents / 100);
3097
3170
  }
3171
+ function normalizeDurationMinutes(value) {
3172
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
3173
+ if (typeof value === "string" && value.trim()) {
3174
+ const parsed = Number(value);
3175
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3176
+ }
3177
+ return void 0;
3178
+ }
3098
3179
  function buildSteps(domain, hasPayment, catalogMode) {
3099
3180
  const steps = [];
3100
3181
  if (domain === "watersport" || domain === "service" || catalogMode && domain !== "restaurant") {
@@ -3196,6 +3277,7 @@ function ResiraBookingWidget() {
3196
3277
  partySize: domainConfig.defaultPartySize ?? 2,
3197
3278
  duration: domainConfig.defaultDuration
3198
3279
  });
3280
+ const [partySizeByProductId, setPartySizeByProductId] = useState({});
3199
3281
  const [guest, setGuest] = useState({
3200
3282
  guestName: "",
3201
3283
  guestEmail: "",
@@ -3212,6 +3294,11 @@ function ResiraBookingWidget() {
3212
3294
  setPromoValidation(result);
3213
3295
  }, []);
3214
3296
  const [slotDate, setSlotDate] = useState(todayStr());
3297
+ const activeDurationMinutes = normalizeDurationMinutes(selection.duration);
3298
+ const selectedProductPartyBounds = useMemo(
3299
+ () => getServicePartySizeBounds(selectedProduct, domainConfig),
3300
+ [selectedProduct, domainConfig]
3301
+ );
3215
3302
  const availabilityParams = useMemo(() => {
3216
3303
  if (isDateBased) {
3217
3304
  if (selection.startDate && selection.endDate) {
@@ -3222,9 +3309,9 @@ function ResiraBookingWidget() {
3222
3309
  return {
3223
3310
  date: slotDate,
3224
3311
  partySize: selection.partySize,
3225
- durationMinutes: selection.duration
3312
+ durationMinutes: activeDurationMinutes
3226
3313
  };
3227
- }, [isDateBased, selection.startDate, selection.endDate, slotDate, selection.partySize, selection.duration]);
3314
+ }, [isDateBased, selection.startDate, selection.endDate, slotDate, selection.partySize, activeDurationMinutes]);
3228
3315
  const availabilityProductId = isServiceBased ? selectedProduct?.id : void 0;
3229
3316
  const { data: availability, loading, error, refetch } = useAvailability(
3230
3317
  availabilityParams,
@@ -3268,7 +3355,7 @@ function ResiraBookingWidget() {
3268
3355
  productId: selectedProduct?.id ?? "",
3269
3356
  resourceId,
3270
3357
  partySize: selection.partySize,
3271
- durationMinutes: selection.duration,
3358
+ durationMinutes: activeDurationMinutes,
3272
3359
  startDate: selection.startDate,
3273
3360
  startTime: selection.startTime,
3274
3361
  endTime: selection.endTime,
@@ -3280,7 +3367,7 @@ function ResiraBookingWidget() {
3280
3367
  termsAccepted: termsAccepted || void 0,
3281
3368
  waiverAccepted: waiverAccepted || void 0
3282
3369
  };
3283
- }, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken]);
3370
+ }, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken, activeDurationMinutes]);
3284
3371
  const blockedDates = useMemo(() => {
3285
3372
  const dates = calendarData?.dates?.blockedDates ?? availability?.dates?.blockedDates ?? [];
3286
3373
  return new Set(dates);
@@ -3300,11 +3387,11 @@ function ResiraBookingWidget() {
3300
3387
  if (selectedProduct) {
3301
3388
  let base = selectedProduct.priceCents;
3302
3389
  if (selectedProduct.pricingModel === "per_rider" && activeRiderDurationPricing?.length) {
3303
- const match = selection.duration ? activeRiderDurationPricing.find((dp) => dp.durationMinutes === selection.duration) : activeRiderDurationPricing[0];
3390
+ const match = activeDurationMinutes ? activeRiderDurationPricing.find((dp) => dp.durationMinutes === activeDurationMinutes) : activeRiderDurationPricing[0];
3304
3391
  if (match) base = match.priceCents;
3305
- } else if (selectedProduct.durationPricing?.length && selection.duration) {
3392
+ } else if (selectedProduct.durationPricing?.length && activeDurationMinutes) {
3306
3393
  const match = selectedProduct.durationPricing.find(
3307
- (dp) => dp.durationMinutes === selection.duration
3394
+ (dp) => dp.durationMinutes === activeDurationMinutes
3308
3395
  );
3309
3396
  if (match) base = match.priceCents;
3310
3397
  }
@@ -3322,7 +3409,7 @@ function ResiraBookingWidget() {
3322
3409
  return { total, amountNow, amountAtVenue };
3323
3410
  }
3324
3411
  return null;
3325
- }, [selectedProduct, availability, selection.partySize, selection.duration, depositPercent, activeRiderDurationPricing]);
3412
+ }, [selectedProduct, availability, selection.partySize, activeDurationMinutes, depositPercent, activeRiderDurationPricing]);
3326
3413
  const stepTitle = useMemo(() => {
3327
3414
  switch (step) {
3328
3415
  case "resource":
@@ -3409,8 +3496,12 @@ function ResiraBookingWidget() {
3409
3496
  [slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS]
3410
3497
  );
3411
3498
  const handlePartySizeChange = useCallback((size) => {
3412
- setSelection((prev) => ({ ...prev, partySize: size }));
3413
- }, []);
3499
+ const clamped = clampPartySize(size, selectedProductPartyBounds);
3500
+ setSelection((prev) => ({ ...prev, partySize: clamped }));
3501
+ if (selectedProduct?.id) {
3502
+ setPartySizeByProductId((prev) => ({ ...prev, [selectedProduct.id]: clamped }));
3503
+ }
3504
+ }, [selectedProduct?.id, selectedProductPartyBounds]);
3414
3505
  const handleDurationChange = useCallback((minutes) => {
3415
3506
  setSelection((prev) => ({
3416
3507
  ...prev,
@@ -3426,16 +3517,29 @@ function ResiraBookingWidget() {
3426
3517
  setActiveResourceId(product.equipmentIds[0]);
3427
3518
  }
3428
3519
  setSelection((prev) => {
3429
- const maxParty = product.maxPartySize ?? domainConfig.maxPartySize ?? 10;
3430
- const clampedPartySize = Math.min(prev.partySize, maxParty);
3520
+ const bounds = getServicePartySizeBounds(product, domainConfig);
3521
+ const persistedPartySize = partySizeByProductId[product.id];
3522
+ const nextPartySize = clampPartySize(
3523
+ persistedPartySize ?? prev.partySize,
3524
+ bounds
3525
+ );
3431
3526
  const defaultDuration = product.durationPricing?.[0]?.durationMinutes ?? product.durationMinutes ?? prev.duration;
3432
3527
  return {
3433
3528
  ...prev,
3434
3529
  productId: product.id,
3435
3530
  duration: defaultDuration,
3436
- partySize: clampedPartySize
3531
+ partySize: nextPartySize
3437
3532
  };
3438
3533
  });
3534
+ setPartySizeByProductId((prev) => {
3535
+ const bounds = getServicePartySizeBounds(product, domainConfig);
3536
+ const persistedPartySize = prev[product.id];
3537
+ const nextPartySize = clampPartySize(
3538
+ persistedPartySize ?? selection.partySize,
3539
+ bounds
3540
+ );
3541
+ return { ...prev, [product.id]: nextPartySize };
3542
+ });
3439
3543
  if (step === "resource" && isServiceBased) {
3440
3544
  const nextIdx = stepIndex("resource", STEPS) + 1;
3441
3545
  if (nextIdx < STEPS.length) {
@@ -3443,7 +3547,7 @@ function ResiraBookingWidget() {
3443
3547
  }
3444
3548
  }
3445
3549
  },
3446
- [setActiveResourceId, domainConfig.maxPartySize, step, isServiceBased, STEPS]
3550
+ [setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS]
3447
3551
  );
3448
3552
  const handleResourceSelect = useCallback(
3449
3553
  (resourceId) => {
@@ -3643,6 +3747,11 @@ function ResiraBookingWidget() {
3643
3747
  ...deeplink.duration ? { duration: deeplink.duration } : {},
3644
3748
  ...deeplink.date ? { startDate: deeplink.date } : {}
3645
3749
  }));
3750
+ if (deeplink.productId && deeplink.partySize) {
3751
+ const deeplinkProductId = deeplink.productId;
3752
+ const deeplinkPartySize = deeplink.partySize;
3753
+ setPartySizeByProductId((prev) => ({ ...prev, [deeplinkProductId]: deeplinkPartySize }));
3754
+ }
3646
3755
  if (deeplink.date) setSlotDate(deeplink.date);
3647
3756
  }
3648
3757
  if (deeplinkGuest) {
@@ -3766,9 +3875,10 @@ function ResiraBookingWidget() {
3766
3875
  onPartySizeChange: handlePartySizeChange,
3767
3876
  showPartySize: true,
3768
3877
  showDuration: domain === "watersport" || domain === "service",
3769
- selectedDuration: selection.duration,
3878
+ selectedDuration: activeDurationMinutes,
3770
3879
  onDurationChange: handleDurationChange,
3771
- maxPartySizeOverride: selectedProduct?.maxPartySize,
3880
+ minPartySizeOverride: selectedProductPartyBounds.min,
3881
+ maxPartySizeOverride: selectedProductPartyBounds.max,
3772
3882
  durationPricing: activeRiderDurationPricing ?? selectedProduct?.durationPricing,
3773
3883
  currency,
3774
3884
  showRemainingSpots
@@ -3778,19 +3888,19 @@ function ResiraBookingWidget() {
3778
3888
  /* @__PURE__ */ jsxs("div", { className: "resira-price-preview-row", children: [
3779
3889
  /* @__PURE__ */ jsxs("span", { children: [
3780
3890
  selectedProduct.name,
3781
- selection.duration && selectedProduct.durationPricing && selectedProduct.durationPricing.length > 1 && /* @__PURE__ */ jsxs("span", { className: "resira-price-preview-label", children: [
3891
+ activeDurationMinutes && selectedProduct.durationPricing && selectedProduct.durationPricing.length > 1 && /* @__PURE__ */ jsxs("span", { className: "resira-price-preview-label", children: [
3782
3892
  " ",
3783
3893
  "(",
3784
- selection.duration < 60 ? `${selection.duration} min` : `${Math.floor(selection.duration / 60)}h${selection.duration % 60 ? selection.duration % 60 : ""}`,
3894
+ activeDurationMinutes < 60 ? `${activeDurationMinutes} min` : `${Math.floor(activeDurationMinutes / 60)}h${activeDurationMinutes % 60 ? activeDurationMinutes % 60 : ""}`,
3785
3895
  ")"
3786
3896
  ] })
3787
3897
  ] }),
3788
3898
  /* @__PURE__ */ jsxs("span", { children: [
3789
3899
  (() => {
3790
3900
  let unitPrice = selectedProduct.priceCents;
3791
- if (selectedProduct.durationPricing?.length && selection.duration) {
3901
+ if (selectedProduct.durationPricing?.length && activeDurationMinutes) {
3792
3902
  const match = selectedProduct.durationPricing.find(
3793
- (dp) => dp.durationMinutes === selection.duration
3903
+ (dp) => dp.durationMinutes === activeDurationMinutes
3794
3904
  );
3795
3905
  if (match) unitPrice = match.priceCents;
3796
3906
  }
@@ -4630,6 +4740,6 @@ function DishShowcase({
4630
4740
  ] });
4631
4741
  }
4632
4742
 
4633
- export { AlertCircleIcon, BookingCalendar, BookingModal, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, GuestForm, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, ResiraProvider, ResourcePicker, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
4743
+ export { AlertCircleIcon, BookingCalendar, BookingModal, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, GuestForm, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, ResiraProvider, ResourcePicker, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, fetchServices, resolveServicePrice, resolveTheme, themeToCSS, useAvailability, useCheckoutSession, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, useServices, validateGuestForm };
4634
4744
  //# sourceMappingURL=index.js.map
4635
4745
  //# sourceMappingURL=index.js.map