@resira/ui 0.3.2 → 0.4.0

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
@@ -234,6 +234,28 @@ interface ResiraClassNames {
234
234
  /** Price preview panel. */
235
235
  pricePreview?: string;
236
236
  }
237
+ /** Pre-filled selection data for deeplink integration. */
238
+ interface DeeplinkSelection {
239
+ /** Pre-selected product/service ID. */
240
+ productId?: string;
241
+ /** Pre-selected date (YYYY-MM-DD). */
242
+ date?: string;
243
+ /** Pre-selected party size. */
244
+ partySize?: number;
245
+ /** Pre-selected duration in minutes. */
246
+ duration?: number;
247
+ }
248
+ /** Pre-filled guest data for deeplink integration. */
249
+ interface DeeplinkGuest {
250
+ /** Guest name. */
251
+ name?: string;
252
+ /** Guest email. */
253
+ email?: string;
254
+ /** Guest phone. */
255
+ phone?: string;
256
+ /** Notes. */
257
+ notes?: string;
258
+ }
237
259
  /** Configuration passed to ResiraProvider. */
238
260
  interface ResiraProviderConfig {
239
261
  /** Theme overrides. */
@@ -270,6 +292,23 @@ interface ResiraProviderConfig {
270
292
  showRemainingSpots?: boolean;
271
293
  /** Percentage of total to charge upfront (0-100). Default 100. */
272
294
  depositPercent?: number;
295
+ /** Whether to show the step indicator bar. @default true */
296
+ showStepIndicator?: boolean;
297
+ /** Pre-fill booking data for deeplink integration. */
298
+ deeplink?: DeeplinkSelection;
299
+ /** Pre-fill guest information. */
300
+ deeplinkGuest?: DeeplinkGuest;
301
+ /** Called when the booking step changes. */
302
+ onStepChange?: (step: BookingStep) => void;
303
+ /** Called when a booking is successfully completed. */
304
+ onBookingComplete?: (data: {
305
+ reservationId?: string;
306
+ product?: Product;
307
+ selection: BookingSelection;
308
+ guest: GuestFormValues;
309
+ }) => void;
310
+ /** Called when an error occurs during the booking flow. */
311
+ onError?: (code: string, message: string) => void;
273
312
  }
274
313
  /** Props for the ResiraProvider component. */
275
314
  interface ResiraProviderProps {
@@ -336,6 +375,23 @@ interface ResiraContextValue {
336
375
  groupServicesByCategory: boolean;
337
376
  /** Custom render function for service cards. */
338
377
  renderServiceCard?: (product: Product, selected: boolean) => React.ReactNode;
378
+ /** Whether the step indicator bar is visible. */
379
+ showStepIndicator: boolean;
380
+ /** Pre-fill booking data for deeplink integration. */
381
+ deeplink?: DeeplinkSelection;
382
+ /** Pre-fill guest information. */
383
+ deeplinkGuest?: DeeplinkGuest;
384
+ /** Called when the booking step changes. */
385
+ onStepChange?: (step: BookingStep) => void;
386
+ /** Called when a booking is successfully completed. */
387
+ onBookingComplete?: (data: {
388
+ reservationId?: string;
389
+ product?: Product;
390
+ selection: BookingSelection;
391
+ guest: GuestFormValues;
392
+ }) => void;
393
+ /** Called when an error occurs during the booking flow. */
394
+ onError?: (code: string, message: string) => void;
339
395
  }
340
396
  /** Steps in the booking flow. */
341
397
  type BookingStep = "resource" | "availability" | "details" | "terms" | "payment" | "confirmation";
@@ -465,14 +521,13 @@ interface ProductSelectorProps {
465
521
  onSelect: (product: Product) => void;
466
522
  loading?: boolean;
467
523
  error?: string | null;
468
- /** Override layout from provider. */
469
524
  layout?: ServiceLayout;
470
- /** Override visible count from provider. */
471
525
  visibleCount?: number;
472
- /** Override category grouping from provider. @default true */
473
526
  groupByCategory?: boolean;
527
+ /** Pre-select a specific category to show (skip category tiles). */
528
+ initialCategory?: string;
474
529
  }
475
- declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
530
+ declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, initialCategory, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
476
531
 
477
532
  /**
478
533
  * Validate the guest form. Returns an errors object (empty = valid).
@@ -743,4 +798,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
743
798
  declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
744
799
  declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
745
800
 
746
- export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
801
+ export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, 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, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
package/dist/index.d.ts CHANGED
@@ -234,6 +234,28 @@ interface ResiraClassNames {
234
234
  /** Price preview panel. */
235
235
  pricePreview?: string;
236
236
  }
237
+ /** Pre-filled selection data for deeplink integration. */
238
+ interface DeeplinkSelection {
239
+ /** Pre-selected product/service ID. */
240
+ productId?: string;
241
+ /** Pre-selected date (YYYY-MM-DD). */
242
+ date?: string;
243
+ /** Pre-selected party size. */
244
+ partySize?: number;
245
+ /** Pre-selected duration in minutes. */
246
+ duration?: number;
247
+ }
248
+ /** Pre-filled guest data for deeplink integration. */
249
+ interface DeeplinkGuest {
250
+ /** Guest name. */
251
+ name?: string;
252
+ /** Guest email. */
253
+ email?: string;
254
+ /** Guest phone. */
255
+ phone?: string;
256
+ /** Notes. */
257
+ notes?: string;
258
+ }
237
259
  /** Configuration passed to ResiraProvider. */
238
260
  interface ResiraProviderConfig {
239
261
  /** Theme overrides. */
@@ -270,6 +292,23 @@ interface ResiraProviderConfig {
270
292
  showRemainingSpots?: boolean;
271
293
  /** Percentage of total to charge upfront (0-100). Default 100. */
272
294
  depositPercent?: number;
295
+ /** Whether to show the step indicator bar. @default true */
296
+ showStepIndicator?: boolean;
297
+ /** Pre-fill booking data for deeplink integration. */
298
+ deeplink?: DeeplinkSelection;
299
+ /** Pre-fill guest information. */
300
+ deeplinkGuest?: DeeplinkGuest;
301
+ /** Called when the booking step changes. */
302
+ onStepChange?: (step: BookingStep) => void;
303
+ /** Called when a booking is successfully completed. */
304
+ onBookingComplete?: (data: {
305
+ reservationId?: string;
306
+ product?: Product;
307
+ selection: BookingSelection;
308
+ guest: GuestFormValues;
309
+ }) => void;
310
+ /** Called when an error occurs during the booking flow. */
311
+ onError?: (code: string, message: string) => void;
273
312
  }
274
313
  /** Props for the ResiraProvider component. */
275
314
  interface ResiraProviderProps {
@@ -336,6 +375,23 @@ interface ResiraContextValue {
336
375
  groupServicesByCategory: boolean;
337
376
  /** Custom render function for service cards. */
338
377
  renderServiceCard?: (product: Product, selected: boolean) => React.ReactNode;
378
+ /** Whether the step indicator bar is visible. */
379
+ showStepIndicator: boolean;
380
+ /** Pre-fill booking data for deeplink integration. */
381
+ deeplink?: DeeplinkSelection;
382
+ /** Pre-fill guest information. */
383
+ deeplinkGuest?: DeeplinkGuest;
384
+ /** Called when the booking step changes. */
385
+ onStepChange?: (step: BookingStep) => void;
386
+ /** Called when a booking is successfully completed. */
387
+ onBookingComplete?: (data: {
388
+ reservationId?: string;
389
+ product?: Product;
390
+ selection: BookingSelection;
391
+ guest: GuestFormValues;
392
+ }) => void;
393
+ /** Called when an error occurs during the booking flow. */
394
+ onError?: (code: string, message: string) => void;
339
395
  }
340
396
  /** Steps in the booking flow. */
341
397
  type BookingStep = "resource" | "availability" | "details" | "terms" | "payment" | "confirmation";
@@ -465,14 +521,13 @@ interface ProductSelectorProps {
465
521
  onSelect: (product: Product) => void;
466
522
  loading?: boolean;
467
523
  error?: string | null;
468
- /** Override layout from provider. */
469
524
  layout?: ServiceLayout;
470
- /** Override visible count from provider. */
471
525
  visibleCount?: number;
472
- /** Override category grouping from provider. @default true */
473
526
  groupByCategory?: boolean;
527
+ /** Pre-select a specific category to show (skip category tiles). */
528
+ initialCategory?: string;
474
529
  }
475
- declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
530
+ declare function ProductSelector({ products, resources, selectedId, onSelect, loading, error, layout: layoutProp, visibleCount: visibleCountProp, groupByCategory: groupByCategoryProp, initialCategory, }: ProductSelectorProps): react_jsx_runtime.JSX.Element;
476
531
 
477
532
  /**
478
533
  * Validate the guest form. Returns an errors object (empty = valid).
@@ -743,4 +798,4 @@ declare function TagIcon({ size, className }: IconProps): react_jsx_runtime.JSX.
743
798
  declare function CubeIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
744
799
  declare function ViewfinderIcon({ size, className }: IconProps): react_jsx_runtime.JSX.Element;
745
800
 
746
- export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon, ConfirmationView, CreditCardIcon, CubeIcon, DEFAULT_LOCALE, DEFAULT_THEME, DishShowcase, type DishShowcaseProps, type DomainConfig, GuestForm, type GuestFormErrors, type GuestFormValues, LockIcon, MailIcon, MinusIcon, NoteIcon, PaymentForm, PhoneIcon, PlusIcon, ProductSelector, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
801
+ export { AlertCircleIcon, BookingCalendar, BookingModal, type BookingSelection, type BookingStep, CalendarIcon, CheckCircleIcon, CheckIcon, 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, ResiraBookingWidget, type ResiraClassNames, type ResiraContextValue, type ResiraDomain, type ResiraLocale, ResiraProvider, type ResiraProviderConfig, type ResiraProviderProps, type ResiraTheme, ResourcePicker, type ServiceLayout, ShieldIcon, SummaryPreview, TagIcon, TimeSlotPicker, UserIcon, UsersIcon, ViewfinderIcon, WaiverConsent, XIcon, resolveTheme, themeToCSS, useAvailability, useDish, useDishes, usePaymentIntent, useProducts, useReservation, useResira, useResources, validateGuestForm };
package/dist/index.js CHANGED
@@ -222,6 +222,12 @@ function ResiraProvider({
222
222
  const visibleServiceCount = config?.visibleServiceCount ?? 4;
223
223
  const groupServicesByCategory = config?.groupServicesByCategory ?? true;
224
224
  const renderServiceCard = config?.renderServiceCard;
225
+ const showStepIndicator = config?.showStepIndicator ?? true;
226
+ const deeplink = config?.deeplink;
227
+ const deeplinkGuest = config?.deeplinkGuest;
228
+ const onStepChange = config?.onStepChange;
229
+ const onBookingComplete = config?.onBookingComplete;
230
+ const onError = config?.onError;
225
231
  const cssVars = useMemo(() => themeToCSS(theme), [theme]);
226
232
  const value = useMemo(
227
233
  () => ({
@@ -248,9 +254,15 @@ function ResiraProvider({
248
254
  serviceLayout,
249
255
  visibleServiceCount,
250
256
  groupServicesByCategory,
251
- renderServiceCard
257
+ renderServiceCard,
258
+ showStepIndicator,
259
+ deeplink,
260
+ deeplinkGuest,
261
+ onStepChange,
262
+ onBookingComplete,
263
+ onError
252
264
  }),
253
- [client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard]
265
+ [client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard, showStepIndicator, deeplink, deeplinkGuest, onStepChange, onBookingComplete, onError]
254
266
  );
255
267
  return /* @__PURE__ */ jsx(ResiraContext.Provider, { value, children: /* @__PURE__ */ jsx("div", { className: "resira-root", style: cssVars, children }) });
256
268
  }
@@ -1630,39 +1642,90 @@ function formatCategoryLabel(resourceType) {
1630
1642
  return resourceType.trim().split(/[_\-\s]+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()).join(" ");
1631
1643
  }
1632
1644
  function groupProductsByCategory(products, resources) {
1633
- const resourceTypeById = new Map(
1634
- resources.map((resource) => [resource.id, resource.resourceType])
1645
+ const resourceById = new Map(
1646
+ resources.map((resource) => [resource.id, resource])
1635
1647
  );
1636
1648
  const groups = /* @__PURE__ */ new Map();
1637
1649
  products.forEach((product) => {
1638
- const categoryType = product.equipmentIds.map((equipmentId) => resourceTypeById.get(equipmentId)?.trim()).find((resourceType) => Boolean(resourceType));
1650
+ let categoryResource;
1651
+ const categoryType = product.equipmentIds.map((equipmentId) => {
1652
+ const res = resourceById.get(equipmentId);
1653
+ if (res?.resourceType?.trim() && !categoryResource) {
1654
+ categoryResource = res;
1655
+ }
1656
+ return res?.resourceType?.trim();
1657
+ }).find((resourceType) => Boolean(resourceType));
1639
1658
  const groupId = categoryType?.toLowerCase() ?? UNCATEGORIZED_CATEGORY_KEY;
1640
1659
  const label = categoryType ? formatCategoryLabel(categoryType) : UNCATEGORIZED_CATEGORY_LABEL;
1641
1660
  if (!groups.has(groupId)) {
1642
1661
  groups.set(groupId, {
1643
1662
  id: groupId,
1644
1663
  label,
1664
+ imageUrl: categoryResource?.imageUrl ?? void 0,
1645
1665
  products: []
1646
1666
  });
1647
1667
  }
1648
- groups.get(groupId)?.products.push(product);
1668
+ const group = groups.get(groupId);
1669
+ group.products.push(product);
1670
+ if (!group.imageUrl && categoryResource?.imageUrl) {
1671
+ group.imageUrl = categoryResource.imageUrl;
1672
+ }
1649
1673
  });
1650
1674
  return Array.from(groups.values());
1651
1675
  }
1652
- function DefaultServiceCard({
1676
+ function BackArrow() {
1677
+ return /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1678
+ /* @__PURE__ */ jsx("path", { d: "M19 12H5" }),
1679
+ /* @__PURE__ */ jsx("path", { d: "M12 19l-7-7 7-7" })
1680
+ ] });
1681
+ }
1682
+ function CategoryTile({
1683
+ group,
1684
+ onClick
1685
+ }) {
1686
+ return /* @__PURE__ */ jsxs(
1687
+ "button",
1688
+ {
1689
+ type: "button",
1690
+ className: "resira-category-tile",
1691
+ onClick,
1692
+ children: [
1693
+ /* @__PURE__ */ jsxs("div", { className: "resira-category-tile-image", children: [
1694
+ /* @__PURE__ */ jsx(
1695
+ "img",
1696
+ {
1697
+ src: group.imageUrl || PLACEHOLDER_IMG2,
1698
+ alt: group.label,
1699
+ loading: "lazy"
1700
+ }
1701
+ ),
1702
+ /* @__PURE__ */ jsx("div", { className: "resira-category-tile-overlay" })
1703
+ ] }),
1704
+ /* @__PURE__ */ jsxs("div", { className: "resira-category-tile-content", children: [
1705
+ /* @__PURE__ */ jsx("h3", { className: "resira-category-tile-name", children: group.label }),
1706
+ /* @__PURE__ */ jsxs("span", { className: "resira-category-tile-count", children: [
1707
+ group.products.length,
1708
+ " ",
1709
+ group.products.length === 1 ? "service" : "services"
1710
+ ] })
1711
+ ] })
1712
+ ]
1713
+ }
1714
+ );
1715
+ }
1716
+ function ServiceOverlayCard({
1653
1717
  product,
1654
1718
  isSelected,
1655
- layout,
1656
1719
  locale,
1657
1720
  cardClassName
1658
1721
  }) {
1659
1722
  const currency = product.currency ?? "EUR";
1660
1723
  const priceLabel = product.pricingModel === "per_rider" ? "per rider" : product.pricingModel === "per_person" ? locale.perPerson : locale.perSession;
1661
- let className = `resira-service-card resira-service-card--${layout}`;
1662
- if (isSelected) className += " resira-service-card--selected";
1724
+ let className = "resira-service-overlay-card";
1725
+ if (isSelected) className += " resira-service-overlay-card--selected";
1663
1726
  if (cardClassName) className += ` ${cardClassName}`;
1664
1727
  return /* @__PURE__ */ jsxs("div", { className, children: [
1665
- /* @__PURE__ */ jsx("div", { className: "resira-service-card-image", children: /* @__PURE__ */ jsx(
1728
+ /* @__PURE__ */ jsx("div", { className: "resira-service-overlay-card-bg", children: /* @__PURE__ */ jsx(
1666
1729
  "img",
1667
1730
  {
1668
1731
  src: product.imageUrl ?? PLACEHOLDER_IMG2,
@@ -1670,30 +1733,27 @@ function DefaultServiceCard({
1670
1733
  loading: "lazy"
1671
1734
  }
1672
1735
  ) }),
1673
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-body", children: [
1674
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-top", children: [
1675
- /* @__PURE__ */ jsx("h3", { className: "resira-service-card-name", children: product.name }),
1676
- product.description && /* @__PURE__ */ jsx("p", { className: "resira-service-card-desc", children: product.description })
1736
+ /* @__PURE__ */ jsx("div", { className: "resira-service-overlay-card-gradient" }),
1737
+ /* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-content", children: [
1738
+ /* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-top", children: [
1739
+ /* @__PURE__ */ jsx("h3", { className: "resira-service-overlay-card-name", children: product.name }),
1740
+ product.description && /* @__PURE__ */ jsx("p", { className: "resira-service-overlay-card-desc", children: product.description })
1677
1741
  ] }),
1678
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-bottom", children: [
1679
- /* @__PURE__ */ jsxs("span", { className: "resira-service-card-price", children: [
1742
+ /* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-bottom", children: [
1743
+ /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-price", children: [
1680
1744
  formatPrice2(product.priceCents, currency),
1681
- /* @__PURE__ */ jsxs("span", { className: "resira-service-card-price-unit", children: [
1745
+ /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-price-unit", children: [
1682
1746
  "/",
1683
1747
  priceLabel
1684
1748
  ] })
1685
1749
  ] }),
1686
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-pills", children: [
1687
- product.durationMinutes > 0 && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1688
- /* @__PURE__ */ jsx(ClockIcon, { size: 12 }),
1750
+ /* @__PURE__ */ jsxs("div", { className: "resira-service-overlay-card-pills", children: [
1751
+ product.durationMinutes > 0 && /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-pill", children: [
1752
+ /* @__PURE__ */ jsx(ClockIcon, { size: 11 }),
1689
1753
  formatDuration2(product.durationMinutes)
1690
1754
  ] }),
1691
- product.pricingModel === "per_person" && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1692
- /* @__PURE__ */ jsx(UsersIcon, { size: 12 }),
1693
- product.maxPartySize ? `1\u2013${product.maxPartySize}` : locale.perPerson
1694
- ] }),
1695
- product.maxPartySize && product.pricingModel !== "per_person" && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1696
- /* @__PURE__ */ jsx(UsersIcon, { size: 12 }),
1755
+ product.maxPartySize && /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-pill", children: [
1756
+ /* @__PURE__ */ jsx(UsersIcon, { size: 11 }),
1697
1757
  "max ",
1698
1758
  product.maxPartySize
1699
1759
  ] })
@@ -1721,7 +1781,8 @@ function ProductSelector({
1721
1781
  error = null,
1722
1782
  layout: layoutProp,
1723
1783
  visibleCount: visibleCountProp,
1724
- groupByCategory: groupByCategoryProp
1784
+ groupByCategory: groupByCategoryProp,
1785
+ initialCategory
1725
1786
  }) {
1726
1787
  const {
1727
1788
  locale,
@@ -1735,21 +1796,37 @@ function ProductSelector({
1735
1796
  const visibleCount = visibleCountProp ?? providerCount;
1736
1797
  const groupByCategory = groupByCategoryProp ?? providerGroupByCategory;
1737
1798
  const containerRef = useRef(null);
1799
+ const [activeCategory, setActiveCategory] = useState(initialCategory ?? null);
1738
1800
  const productGroups = useMemo(
1739
- () => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", products }],
1801
+ () => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", imageUrl: void 0, products }],
1740
1802
  [groupByCategory, products, resources]
1741
1803
  );
1742
- const containerStyle = useMemo(() => {
1743
- if (layout === "horizontal") {
1744
- return {};
1804
+ const skipCategoryView = productGroups.length <= 1;
1805
+ useEffect(() => {
1806
+ if (initialCategory && productGroups.some((g) => g.id === initialCategory)) {
1807
+ setActiveCategory(initialCategory);
1808
+ }
1809
+ }, [initialCategory, productGroups]);
1810
+ useEffect(() => {
1811
+ if (activeCategory && !productGroups.some((g) => g.id === activeCategory)) {
1812
+ setActiveCategory(null);
1745
1813
  }
1746
- const cardHeight = 88;
1747
- const gap = 10;
1748
- const sectionHeaderHeight = groupByCategory ? productGroups.length * 30 : 0;
1749
- const sectionGap = groupByCategory ? Math.max(productGroups.length - 1, 0) * 12 : 0;
1750
- const maxHeight = visibleCount * cardHeight + (visibleCount - 1) * gap + sectionHeaderHeight + sectionGap;
1814
+ }, [activeCategory, productGroups]);
1815
+ const activeCategoryGroup = useMemo(
1816
+ () => productGroups.find((g) => g.id === activeCategory) ?? null,
1817
+ [productGroups, activeCategory]
1818
+ );
1819
+ const displayProducts = useMemo(() => {
1820
+ if (skipCategoryView) return products;
1821
+ return activeCategoryGroup?.products ?? [];
1822
+ }, [skipCategoryView, products, activeCategoryGroup]);
1823
+ const containerStyle = useMemo(() => {
1824
+ if (layout === "horizontal") return {};
1825
+ const cardHeight = 180;
1826
+ const gap = 12;
1827
+ const maxHeight = visibleCount * cardHeight + (visibleCount - 1) * gap;
1751
1828
  return { maxHeight, overflowY: "auto" };
1752
- }, [groupByCategory, layout, productGroups.length, visibleCount]);
1829
+ }, [layout, visibleCount]);
1753
1830
  if (loading) {
1754
1831
  return /* @__PURE__ */ jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
1755
1832
  /* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
@@ -1762,54 +1839,64 @@ function ProductSelector({
1762
1839
  if (products.length === 0) {
1763
1840
  return /* @__PURE__ */ jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsx("p", { children: "No services available at the moment." }) });
1764
1841
  }
1842
+ if (groupByCategory && !skipCategoryView && !activeCategory) {
1843
+ return /* @__PURE__ */ jsx("div", { className: "resira-service-picker", children: /* @__PURE__ */ jsx("div", { className: "resira-category-grid", children: productGroups.map((group) => /* @__PURE__ */ jsx(
1844
+ CategoryTile,
1845
+ {
1846
+ group,
1847
+ onClick: () => setActiveCategory(group.id)
1848
+ },
1849
+ group.id
1850
+ )) }) });
1851
+ }
1765
1852
  const listClassName = [
1766
- "resira-service-list",
1767
- `resira-service-list--${layout}`,
1853
+ "resira-service-overlay-list",
1768
1854
  classNames.serviceList
1769
1855
  ].filter(Boolean).join(" ");
1770
- const groupedListClassName = [
1771
- listClassName,
1772
- groupByCategory ? "resira-service-list--grouped" : ""
1773
- ].filter(Boolean).join(" ");
1774
1856
  return /* @__PURE__ */ jsxs("div", { className: "resira-service-picker", children: [
1857
+ groupByCategory && !skipCategoryView && activeCategory && /* @__PURE__ */ jsxs(
1858
+ "button",
1859
+ {
1860
+ type: "button",
1861
+ className: "resira-category-back",
1862
+ onClick: () => setActiveCategory(null),
1863
+ children: [
1864
+ /* @__PURE__ */ jsx(BackArrow, {}),
1865
+ /* @__PURE__ */ jsx("span", { children: activeCategoryGroup?.label ?? "All categories" })
1866
+ ]
1867
+ }
1868
+ ),
1775
1869
  /* @__PURE__ */ jsx(
1776
1870
  "div",
1777
1871
  {
1778
1872
  ref: containerRef,
1779
- className: groupByCategory ? "resira-service-group-list" : listClassName,
1873
+ className: listClassName,
1780
1874
  style: containerStyle,
1781
- children: productGroups.map((group) => /* @__PURE__ */ jsxs("section", { className: "resira-service-group", "aria-label": group.label || locale.chooseService, children: [
1782
- groupByCategory && group.label && /* @__PURE__ */ jsxs("div", { className: "resira-service-group-header", children: [
1783
- /* @__PURE__ */ jsx("h3", { className: "resira-service-group-title", children: group.label }),
1784
- /* @__PURE__ */ jsx("span", { className: "resira-service-group-count", children: group.products.length })
1785
- ] }),
1786
- /* @__PURE__ */ jsx("div", { className: groupByCategory ? groupedListClassName : listClassName, children: group.products.map((product) => {
1787
- const isSelected = selectedId === product.id;
1788
- return /* @__PURE__ */ jsx(
1789
- "button",
1790
- {
1791
- type: "button",
1792
- className: "resira-service-card-btn",
1793
- onClick: () => onSelect(product),
1794
- "aria-pressed": isSelected,
1795
- children: renderServiceCard ? renderServiceCard(product, isSelected) : /* @__PURE__ */ jsx(
1796
- DefaultServiceCard,
1797
- {
1798
- product,
1799
- isSelected,
1800
- layout,
1801
- locale,
1802
- cardClassName: isSelected ? classNames.serviceCardSelected : classNames.serviceCard
1803
- }
1804
- )
1805
- },
1806
- product.id
1807
- );
1808
- }) })
1809
- ] }, group.id))
1875
+ children: displayProducts.map((product) => {
1876
+ const isSelected = selectedId === product.id;
1877
+ return /* @__PURE__ */ jsx(
1878
+ "button",
1879
+ {
1880
+ type: "button",
1881
+ className: "resira-service-card-btn",
1882
+ onClick: () => onSelect(product),
1883
+ "aria-pressed": isSelected,
1884
+ children: renderServiceCard ? renderServiceCard(product, isSelected) : /* @__PURE__ */ jsx(
1885
+ ServiceOverlayCard,
1886
+ {
1887
+ product,
1888
+ isSelected,
1889
+ locale,
1890
+ cardClassName: isSelected ? classNames.serviceCardSelected : classNames.serviceCard
1891
+ }
1892
+ )
1893
+ },
1894
+ product.id
1895
+ );
1896
+ })
1810
1897
  }
1811
1898
  ),
1812
- layout === "vertical" && products.length > visibleCount && /* @__PURE__ */ jsxs("div", { className: "resira-service-scroll-hint", children: [
1899
+ displayProducts.length > visibleCount && /* @__PURE__ */ jsxs("div", { className: "resira-service-scroll-hint", children: [
1813
1900
  /* @__PURE__ */ jsx("span", { children: "Scroll for more" }),
1814
1901
  /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4" }) })
1815
1902
  ] })
@@ -2604,7 +2691,13 @@ function ResiraBookingWidget() {
2604
2691
  showWaiver,
2605
2692
  showRemainingSpots,
2606
2693
  depositPercent,
2607
- onClose
2694
+ onClose,
2695
+ showStepIndicator,
2696
+ deeplink,
2697
+ deeplinkGuest,
2698
+ onStepChange,
2699
+ onBookingComplete,
2700
+ onError
2608
2701
  } = useResira();
2609
2702
  const isDateBased = domain === "rental";
2610
2703
  const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
@@ -2962,8 +3055,9 @@ function ResiraBookingWidget() {
2962
3055
  setStep("confirmation");
2963
3056
  }
2964
3057
  }, [createPayment, paymentPayload, confirmPayment]);
2965
- const handlePaymentError = useCallback((_msg) => {
2966
- }, []);
3058
+ const handlePaymentError = useCallback((msg) => {
3059
+ onError?.("payment_error", msg);
3060
+ }, [onError]);
2967
3061
  const handleSubmitNoPayment = useCallback(async () => {
2968
3062
  if (showTerms && !termsAccepted) {
2969
3063
  setTermsError(locale.termsRequired);
@@ -2993,9 +3087,63 @@ function ResiraBookingWidget() {
2993
3087
  }, [guest, selection, locale, submit, termsAccepted, waiverAccepted, showTerms, showWaiver, activeResourceId, selectedProduct]);
2994
3088
  const footerBusy = submitting || creatingPayment || confirmingPayment || paymentFormSubmitting;
2995
3089
  const paymentActionDisabled = !paymentIntent || !paymentFormReady || paymentFormSubmitting || confirmingPayment;
3090
+ const [deeplinkApplied, setDeeplinkApplied] = useState(false);
3091
+ useEffect(() => {
3092
+ if (deeplinkApplied || !deeplink) return;
3093
+ if (deeplink.productId && products.length > 0 && !selectedProduct) {
3094
+ const target = products.find((p) => p.id === deeplink.productId);
3095
+ if (target) {
3096
+ handleProductSelect(target);
3097
+ }
3098
+ }
3099
+ if (deeplink.partySize || deeplink.duration || deeplink.date) {
3100
+ setSelection((prev) => ({
3101
+ ...prev,
3102
+ ...deeplink.partySize ? { partySize: deeplink.partySize } : {},
3103
+ ...deeplink.duration ? { duration: deeplink.duration } : {},
3104
+ ...deeplink.date ? { startDate: deeplink.date } : {}
3105
+ }));
3106
+ if (deeplink.date) setSlotDate(deeplink.date);
3107
+ }
3108
+ if (deeplinkGuest) {
3109
+ setGuest((prev) => ({
3110
+ guestName: deeplinkGuest.name ?? prev.guestName,
3111
+ guestEmail: deeplinkGuest.email ?? prev.guestEmail,
3112
+ guestPhone: deeplinkGuest.phone ?? prev.guestPhone,
3113
+ notes: deeplinkGuest.notes ?? prev.notes
3114
+ }));
3115
+ }
3116
+ if (deeplink.productId && step === "resource" && isServiceBased) {
3117
+ const target = products.find((p) => p.id === deeplink.productId);
3118
+ if (target) {
3119
+ setStep(STEPS[stepIndex("resource", STEPS) + 1]);
3120
+ }
3121
+ }
3122
+ setDeeplinkApplied(true);
3123
+ }, [deeplink, deeplinkGuest, products, selectedProduct, step, isServiceBased, STEPS, handleProductSelect]);
3124
+ useEffect(() => {
3125
+ onStepChange?.(step);
3126
+ }, [step, onStepChange]);
3127
+ useEffect(() => {
3128
+ if (step === "confirmation") {
3129
+ const bookingId = reservation?.id ?? paymentIntent?.reservationId;
3130
+ onBookingComplete?.({
3131
+ reservationId: bookingId,
3132
+ product: selectedProduct ?? void 0,
3133
+ selection,
3134
+ guest
3135
+ });
3136
+ }
3137
+ }, [step]);
3138
+ useEffect(() => {
3139
+ if (submitError) onError?.("submit_error", submitError);
3140
+ }, [submitError, onError]);
3141
+ useEffect(() => {
3142
+ if (paymentError) onError?.("payment_error", paymentError);
3143
+ }, [paymentError, onError]);
2996
3144
  return /* @__PURE__ */ jsxs("div", { className: "resira-widget", children: [
2997
3145
  step !== "confirmation" && /* @__PURE__ */ jsxs("div", { className: "resira-widget-topbar", children: [
2998
- /* @__PURE__ */ jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
3146
+ showStepIndicator && /* @__PURE__ */ jsx("nav", { className: "resira-steps", "aria-label": "Booking steps", children: STEPS.filter((s) => s !== "confirmation").map((s, i) => {
2999
3147
  const isCompleted = stepIndex(step, STEPS) > i;
3000
3148
  const isActive = s === step;
3001
3149
  return /* @__PURE__ */ jsxs(