@resira/ui 0.3.1 → 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.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
  }
@@ -545,12 +557,17 @@ function useDish(dishId) {
545
557
  }, [client, dishId]);
546
558
  return { dish, loading, error };
547
559
  }
548
- function useDishes() {
560
+ function useDishes(enabled = true) {
549
561
  const { client } = useResira();
550
562
  const [dishes, setDishes] = useState([]);
551
- const [loading, setLoading] = useState(true);
563
+ const [loading, setLoading] = useState(enabled);
552
564
  const [error, setError] = useState(null);
553
565
  useEffect(() => {
566
+ if (!enabled) {
567
+ setLoading(false);
568
+ setError(null);
569
+ return;
570
+ }
554
571
  let cancelled = false;
555
572
  async function fetchDishes() {
556
573
  try {
@@ -574,7 +591,7 @@ function useDishes() {
574
591
  return () => {
575
592
  cancelled = true;
576
593
  };
577
- }, [client]);
594
+ }, [client, enabled]);
578
595
  return { dishes, loading, error };
579
596
  }
580
597
  var defaultSize = 20;
@@ -1540,13 +1557,13 @@ function ResourcePicker({
1540
1557
  error = null
1541
1558
  }) {
1542
1559
  if (loading) {
1543
- return /* @__PURE__ */ jsxs("div", { className: "resira-loading", children: [
1544
- /* @__PURE__ */ jsx("div", { className: "resira-spinner" }),
1560
+ return /* @__PURE__ */ jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
1561
+ /* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
1545
1562
  /* @__PURE__ */ jsx("span", { className: "resira-loading-text", children: "Loading resources\u2026" })
1546
1563
  ] });
1547
1564
  }
1548
1565
  if (error) {
1549
- return /* @__PURE__ */ jsx("div", { className: "resira-error", children: /* @__PURE__ */ jsx("p", { className: "resira-error-message", children: error }) });
1566
+ return /* @__PURE__ */ jsx("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsx("p", { className: "resira-error-message", children: error }) });
1550
1567
  }
1551
1568
  if (resources.length === 0) {
1552
1569
  return /* @__PURE__ */ jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsx("p", { children: "No resources available at the moment." }) });
@@ -1625,39 +1642,90 @@ function formatCategoryLabel(resourceType) {
1625
1642
  return resourceType.trim().split(/[_\-\s]+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()).join(" ");
1626
1643
  }
1627
1644
  function groupProductsByCategory(products, resources) {
1628
- const resourceTypeById = new Map(
1629
- resources.map((resource) => [resource.id, resource.resourceType])
1645
+ const resourceById = new Map(
1646
+ resources.map((resource) => [resource.id, resource])
1630
1647
  );
1631
1648
  const groups = /* @__PURE__ */ new Map();
1632
1649
  products.forEach((product) => {
1633
- 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));
1634
1658
  const groupId = categoryType?.toLowerCase() ?? UNCATEGORIZED_CATEGORY_KEY;
1635
1659
  const label = categoryType ? formatCategoryLabel(categoryType) : UNCATEGORIZED_CATEGORY_LABEL;
1636
1660
  if (!groups.has(groupId)) {
1637
1661
  groups.set(groupId, {
1638
1662
  id: groupId,
1639
1663
  label,
1664
+ imageUrl: categoryResource?.imageUrl ?? void 0,
1640
1665
  products: []
1641
1666
  });
1642
1667
  }
1643
- 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
+ }
1644
1673
  });
1645
1674
  return Array.from(groups.values());
1646
1675
  }
1647
- 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({
1648
1717
  product,
1649
1718
  isSelected,
1650
- layout,
1651
1719
  locale,
1652
1720
  cardClassName
1653
1721
  }) {
1654
1722
  const currency = product.currency ?? "EUR";
1655
1723
  const priceLabel = product.pricingModel === "per_rider" ? "per rider" : product.pricingModel === "per_person" ? locale.perPerson : locale.perSession;
1656
- let className = `resira-service-card resira-service-card--${layout}`;
1657
- if (isSelected) className += " resira-service-card--selected";
1724
+ let className = "resira-service-overlay-card";
1725
+ if (isSelected) className += " resira-service-overlay-card--selected";
1658
1726
  if (cardClassName) className += ` ${cardClassName}`;
1659
1727
  return /* @__PURE__ */ jsxs("div", { className, children: [
1660
- /* @__PURE__ */ jsx("div", { className: "resira-service-card-image", children: /* @__PURE__ */ jsx(
1728
+ /* @__PURE__ */ jsx("div", { className: "resira-service-overlay-card-bg", children: /* @__PURE__ */ jsx(
1661
1729
  "img",
1662
1730
  {
1663
1731
  src: product.imageUrl ?? PLACEHOLDER_IMG2,
@@ -1665,30 +1733,27 @@ function DefaultServiceCard({
1665
1733
  loading: "lazy"
1666
1734
  }
1667
1735
  ) }),
1668
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-body", children: [
1669
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-top", children: [
1670
- /* @__PURE__ */ jsx("h3", { className: "resira-service-card-name", children: product.name }),
1671
- 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 })
1672
1741
  ] }),
1673
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-bottom", children: [
1674
- /* @__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: [
1675
1744
  formatPrice2(product.priceCents, currency),
1676
- /* @__PURE__ */ jsxs("span", { className: "resira-service-card-price-unit", children: [
1745
+ /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-price-unit", children: [
1677
1746
  "/",
1678
1747
  priceLabel
1679
1748
  ] })
1680
1749
  ] }),
1681
- /* @__PURE__ */ jsxs("div", { className: "resira-service-card-pills", children: [
1682
- product.durationMinutes > 0 && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1683
- /* @__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 }),
1684
1753
  formatDuration2(product.durationMinutes)
1685
1754
  ] }),
1686
- product.pricingModel === "per_person" && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1687
- /* @__PURE__ */ jsx(UsersIcon, { size: 12 }),
1688
- product.maxPartySize ? `1\u2013${product.maxPartySize}` : locale.perPerson
1689
- ] }),
1690
- product.maxPartySize && product.pricingModel !== "per_person" && /* @__PURE__ */ jsxs("span", { className: "resira-service-card-pill", children: [
1691
- /* @__PURE__ */ jsx(UsersIcon, { size: 12 }),
1755
+ product.maxPartySize && /* @__PURE__ */ jsxs("span", { className: "resira-service-overlay-card-pill", children: [
1756
+ /* @__PURE__ */ jsx(UsersIcon, { size: 11 }),
1692
1757
  "max ",
1693
1758
  product.maxPartySize
1694
1759
  ] })
@@ -1716,7 +1781,8 @@ function ProductSelector({
1716
1781
  error = null,
1717
1782
  layout: layoutProp,
1718
1783
  visibleCount: visibleCountProp,
1719
- groupByCategory: groupByCategoryProp
1784
+ groupByCategory: groupByCategoryProp,
1785
+ initialCategory
1720
1786
  }) {
1721
1787
  const {
1722
1788
  locale,
@@ -1730,81 +1796,107 @@ function ProductSelector({
1730
1796
  const visibleCount = visibleCountProp ?? providerCount;
1731
1797
  const groupByCategory = groupByCategoryProp ?? providerGroupByCategory;
1732
1798
  const containerRef = useRef(null);
1799
+ const [activeCategory, setActiveCategory] = useState(initialCategory ?? null);
1733
1800
  const productGroups = useMemo(
1734
- () => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", products }],
1801
+ () => groupByCategory ? groupProductsByCategory(products, resources) : [{ id: "all", label: "", imageUrl: void 0, products }],
1735
1802
  [groupByCategory, products, resources]
1736
1803
  );
1737
- const containerStyle = useMemo(() => {
1738
- if (layout === "horizontal") {
1739
- 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);
1740
1813
  }
1741
- const cardHeight = 88;
1742
- const gap = 10;
1743
- const sectionHeaderHeight = groupByCategory ? productGroups.length * 30 : 0;
1744
- const sectionGap = groupByCategory ? Math.max(productGroups.length - 1, 0) * 12 : 0;
1745
- 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;
1746
1828
  return { maxHeight, overflowY: "auto" };
1747
- }, [groupByCategory, layout, productGroups.length, visibleCount]);
1829
+ }, [layout, visibleCount]);
1748
1830
  if (loading) {
1749
- return /* @__PURE__ */ jsxs("div", { className: "resira-loading", children: [
1750
- /* @__PURE__ */ jsx("div", { className: "resira-spinner" }),
1831
+ return /* @__PURE__ */ jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
1832
+ /* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
1751
1833
  /* @__PURE__ */ jsx("span", { className: "resira-loading-text", children: locale.loading })
1752
1834
  ] });
1753
1835
  }
1754
1836
  if (error) {
1755
- return /* @__PURE__ */ jsx("div", { className: "resira-error", children: /* @__PURE__ */ jsx("p", { className: "resira-error-message", children: error }) });
1837
+ return /* @__PURE__ */ jsx("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsx("p", { className: "resira-error-message", children: error }) });
1756
1838
  }
1757
1839
  if (products.length === 0) {
1758
1840
  return /* @__PURE__ */ jsx("div", { className: "resira-empty", children: /* @__PURE__ */ jsx("p", { children: "No services available at the moment." }) });
1759
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
+ }
1760
1852
  const listClassName = [
1761
- "resira-service-list",
1762
- `resira-service-list--${layout}`,
1853
+ "resira-service-overlay-list",
1763
1854
  classNames.serviceList
1764
1855
  ].filter(Boolean).join(" ");
1765
- const groupedListClassName = [
1766
- listClassName,
1767
- groupByCategory ? "resira-service-list--grouped" : ""
1768
- ].filter(Boolean).join(" ");
1769
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
+ ),
1770
1869
  /* @__PURE__ */ jsx(
1771
1870
  "div",
1772
1871
  {
1773
1872
  ref: containerRef,
1774
- className: groupByCategory ? "resira-service-group-list" : listClassName,
1873
+ className: listClassName,
1775
1874
  style: containerStyle,
1776
- children: productGroups.map((group) => /* @__PURE__ */ jsxs("section", { className: "resira-service-group", "aria-label": group.label || locale.chooseService, children: [
1777
- groupByCategory && group.label && /* @__PURE__ */ jsxs("div", { className: "resira-service-group-header", children: [
1778
- /* @__PURE__ */ jsx("h3", { className: "resira-service-group-title", children: group.label }),
1779
- /* @__PURE__ */ jsx("span", { className: "resira-service-group-count", children: group.products.length })
1780
- ] }),
1781
- /* @__PURE__ */ jsx("div", { className: groupByCategory ? groupedListClassName : listClassName, children: group.products.map((product) => {
1782
- const isSelected = selectedId === product.id;
1783
- return /* @__PURE__ */ jsx(
1784
- "button",
1785
- {
1786
- type: "button",
1787
- className: "resira-service-card-btn",
1788
- onClick: () => onSelect(product),
1789
- "aria-pressed": isSelected,
1790
- children: renderServiceCard ? renderServiceCard(product, isSelected) : /* @__PURE__ */ jsx(
1791
- DefaultServiceCard,
1792
- {
1793
- product,
1794
- isSelected,
1795
- layout,
1796
- locale,
1797
- cardClassName: isSelected ? classNames.serviceCardSelected : classNames.serviceCard
1798
- }
1799
- )
1800
- },
1801
- product.id
1802
- );
1803
- }) })
1804
- ] }, 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
+ })
1805
1897
  }
1806
1898
  ),
1807
- 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: [
1808
1900
  /* @__PURE__ */ jsx("span", { children: "Scroll for more" }),
1809
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" }) })
1810
1902
  ] })
@@ -2328,8 +2420,8 @@ function PaymentForm({
2328
2420
  ] })
2329
2421
  ] }) }),
2330
2422
  /* @__PURE__ */ jsxs("div", { className: "resira-payment-element-container", children: [
2331
- !ready && !error && /* @__PURE__ */ jsxs("div", { className: "resira-loading", style: { padding: "24px 0" }, children: [
2332
- /* @__PURE__ */ jsx("div", { className: "resira-spinner" }),
2423
+ !ready && !error && /* @__PURE__ */ jsxs("div", { className: "resira-loading", style: { padding: "24px 0" }, role: "status", "aria-live": "polite", "aria-busy": "true", children: [
2424
+ /* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
2333
2425
  /* @__PURE__ */ jsx("span", { className: "resira-loading-text", children: locale.loading })
2334
2426
  ] }),
2335
2427
  /* @__PURE__ */ jsx(
@@ -2341,7 +2433,7 @@ function PaymentForm({
2341
2433
  }
2342
2434
  )
2343
2435
  ] }),
2344
- error && /* @__PURE__ */ jsxs("div", { className: "resira-payment-error", children: [
2436
+ error && /* @__PURE__ */ jsxs("div", { className: "resira-payment-error", role: "alert", "aria-live": "assertive", children: [
2345
2437
  /* @__PURE__ */ jsx(AlertCircleIcon, { size: 16 }),
2346
2438
  /* @__PURE__ */ jsx("span", { children: error })
2347
2439
  ] }),
@@ -2599,7 +2691,13 @@ function ResiraBookingWidget() {
2599
2691
  showWaiver,
2600
2692
  showRemainingSpots,
2601
2693
  depositPercent,
2602
- onClose
2694
+ onClose,
2695
+ showStepIndicator,
2696
+ deeplink,
2697
+ deeplinkGuest,
2698
+ onStepChange,
2699
+ onBookingComplete,
2700
+ onError
2603
2701
  } = useResira();
2604
2702
  const isDateBased = domain === "rental";
2605
2703
  const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
@@ -2957,8 +3055,9 @@ function ResiraBookingWidget() {
2957
3055
  setStep("confirmation");
2958
3056
  }
2959
3057
  }, [createPayment, paymentPayload, confirmPayment]);
2960
- const handlePaymentError = useCallback((_msg) => {
2961
- }, []);
3058
+ const handlePaymentError = useCallback((msg) => {
3059
+ onError?.("payment_error", msg);
3060
+ }, [onError]);
2962
3061
  const handleSubmitNoPayment = useCallback(async () => {
2963
3062
  if (showTerms && !termsAccepted) {
2964
3063
  setTermsError(locale.termsRequired);
@@ -2988,9 +3087,63 @@ function ResiraBookingWidget() {
2988
3087
  }, [guest, selection, locale, submit, termsAccepted, waiverAccepted, showTerms, showWaiver, activeResourceId, selectedProduct]);
2989
3088
  const footerBusy = submitting || creatingPayment || confirmingPayment || paymentFormSubmitting;
2990
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]);
2991
3144
  return /* @__PURE__ */ jsxs("div", { className: "resira-widget", children: [
2992
3145
  step !== "confirmation" && /* @__PURE__ */ jsxs("div", { className: "resira-widget-topbar", children: [
2993
- /* @__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) => {
2994
3147
  const isCompleted = stepIndex(step, STEPS) > i;
2995
3148
  const isActive = s === step;
2996
3149
  return /* @__PURE__ */ jsxs(
@@ -3012,7 +3165,7 @@ function ResiraBookingWidget() {
3012
3165
  stepSubtitle && /* @__PURE__ */ jsx("p", { className: "resira-widget-subtitle", children: stepSubtitle })
3013
3166
  ] })
3014
3167
  ] }),
3015
- /* @__PURE__ */ jsxs("div", { className: "resira-widget-body", children: [
3168
+ /* @__PURE__ */ jsxs("div", { className: "resira-widget-body", "aria-busy": loading || calendarLoading || resourcesLoading || productsLoading || footerBusy, children: [
3016
3169
  step === "resource" && isServiceBased && /* @__PURE__ */ jsx(
3017
3170
  ProductSelector,
3018
3171
  {
@@ -3035,10 +3188,10 @@ function ResiraBookingWidget() {
3035
3188
  error: resourcesError
3036
3189
  }
3037
3190
  ),
3038
- step === "availability" && /* @__PURE__ */ jsx(Fragment, { children: (loading || calendarLoading) && !availability && !calendarData ? /* @__PURE__ */ jsxs("div", { className: "resira-loading", children: [
3039
- /* @__PURE__ */ jsx("div", { className: "resira-spinner" }),
3191
+ step === "availability" && /* @__PURE__ */ jsx(Fragment, { children: (loading || calendarLoading) && !availability && !calendarData ? /* @__PURE__ */ jsxs("div", { className: "resira-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
3192
+ /* @__PURE__ */ jsx("div", { className: "resira-spinner", "aria-hidden": "true" }),
3040
3193
  /* @__PURE__ */ jsx("span", { className: "resira-loading-text", children: locale.loading })
3041
- ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "resira-error", children: [
3194
+ ] }) : error ? /* @__PURE__ */ jsxs("div", { className: "resira-error", role: "alert", "aria-live": "assertive", children: [
3042
3195
  /* @__PURE__ */ jsx(AlertCircleIcon, { size: 24 }),
3043
3196
  /* @__PURE__ */ jsx("p", { className: "resira-error-message", children: error }),
3044
3197
  /* @__PURE__ */ jsx(
@@ -3509,12 +3662,17 @@ function supportsArQuickLook() {
3509
3662
  const a = document.createElement("a");
3510
3663
  return a.relList?.supports?.("ar") ?? false;
3511
3664
  }
3665
+ function supportsModelViewer() {
3666
+ if (typeof window === "undefined") return false;
3667
+ return typeof customElements !== "undefined" && customElements.get("model-viewer") !== void 0;
3668
+ }
3512
3669
  function ModelViewer3D({
3513
3670
  dish,
3514
3671
  onClose
3515
3672
  }) {
3516
3673
  const model = dish.model;
3517
3674
  const arSupported = supportsArQuickLook();
3675
+ const modelViewerSupported = supportsModelViewer();
3518
3676
  const arLinkRef = useRef(null);
3519
3677
  const handleArClick = useCallback(() => {
3520
3678
  arLinkRef.current?.click();
@@ -3536,7 +3694,7 @@ function ModelViewer3D({
3536
3694
  ),
3537
3695
  /* @__PURE__ */ jsx("h3", { className: "resira-dish-viewer-title", children: dish.name })
3538
3696
  ] }),
3539
- /* @__PURE__ */ jsx("div", { className: "resira-dish-viewer-canvas", children: model?.glbUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
3697
+ /* @__PURE__ */ jsx("div", { className: "resira-dish-viewer-canvas", children: model?.glbUrl && modelViewerSupported ? /* @__PURE__ */ jsxs(Fragment, { children: [
3540
3698
  /* @__PURE__ */ jsx(
3541
3699
  "model-viewer",
3542
3700
  {
@@ -3578,7 +3736,7 @@ function ModelViewer3D({
3578
3736
  className: "resira-dish-viewer-fallback-img"
3579
3737
  }
3580
3738
  ),
3581
- /* @__PURE__ */ jsx("p", { className: "resira-dish-viewer-no3d", children: "No 3D model available" })
3739
+ /* @__PURE__ */ jsx("p", { className: "resira-dish-viewer-no3d", children: model?.glbUrl ? "3D preview is unavailable here. Showing image instead." : "No 3D model available" })
3582
3740
  ] }) : /* @__PURE__ */ jsxs("div", { className: "resira-dish-viewer-fallback", children: [
3583
3741
  /* @__PURE__ */ jsx(CubeIcon, { size: 48 }),
3584
3742
  /* @__PURE__ */ jsx("p", { className: "resira-dish-viewer-no3d", children: "No preview available" })
@@ -3592,7 +3750,7 @@ function ModelViewer3D({
3592
3750
  dish.allergens && dish.allergens.length > 0 && /* @__PURE__ */ jsx("div", { className: "resira-dish-viewer-allergens", children: dish.allergens.map((a) => /* @__PURE__ */ jsx("span", { className: "resira-dish-viewer-allergen", children: a }, a)) })
3593
3751
  ] }),
3594
3752
  /* @__PURE__ */ jsxs("div", { className: "resira-dish-viewer-actions", children: [
3595
- model?.glbUrl && /* @__PURE__ */ jsxs(
3753
+ model?.glbUrl && modelViewerSupported && /* @__PURE__ */ jsxs(
3596
3754
  "button",
3597
3755
  {
3598
3756
  type: "button",
@@ -3671,7 +3829,8 @@ function DishShowcase({
3671
3829
  const { dish: singleDish, loading: singleLoading, error: singleError } = useDish(
3672
3830
  open && dishId ? dishId : void 0
3673
3831
  );
3674
- const { dishes: allDishes, loading: allLoading, error: allError } = useDishes();
3832
+ const shouldLoadAllDishes = open && (!dishId || showAll);
3833
+ const { dishes: allDishes, loading: allLoading, error: allError } = useDishes(shouldLoadAllDishes);
3675
3834
  const isSingleMode = !!dishId;
3676
3835
  const loading = isSingleMode ? singleLoading : allLoading;
3677
3836
  const error = isSingleMode ? singleError : allError;
@@ -3758,6 +3917,8 @@ function DishShowcase({
3758
3917
  className: className || "resira-dish-showcase-btn",
3759
3918
  style,
3760
3919
  onClick: handleOpen,
3920
+ "aria-haspopup": "dialog",
3921
+ "aria-expanded": open,
3761
3922
  children
3762
3923
  }
3763
3924
  ),
@@ -3775,7 +3936,7 @@ function DishShowcase({
3775
3936
  onClick: (e) => e.stopPropagation(),
3776
3937
  role: "dialog",
3777
3938
  "aria-modal": "true",
3778
- "aria-label": "Dish showcase",
3939
+ "aria-label": selectedDish ? `${selectedDish.name} dish preview` : "Dish showcase",
3779
3940
  children: [
3780
3941
  /* @__PURE__ */ jsx(
3781
3942
  "button",
@@ -3787,11 +3948,11 @@ function DishShowcase({
3787
3948
  children: /* @__PURE__ */ jsx(XIcon, { size: 18 })
3788
3949
  }
3789
3950
  ),
3790
- loading && /* @__PURE__ */ jsxs("div", { className: "resira-dish-loading", children: [
3791
- /* @__PURE__ */ jsx("div", { className: "resira-dish-spinner" }),
3951
+ loading && /* @__PURE__ */ jsxs("div", { className: "resira-dish-loading", role: "status", "aria-live": "polite", "aria-busy": "true", children: [
3952
+ /* @__PURE__ */ jsx("div", { className: "resira-dish-spinner", "aria-hidden": "true" }),
3792
3953
  /* @__PURE__ */ jsx("span", { children: "Loading dishes\u2026" })
3793
3954
  ] }),
3794
- error && !loading && /* @__PURE__ */ jsx("div", { className: "resira-dish-error", children: /* @__PURE__ */ jsx("p", { children: error }) }),
3955
+ error && !loading && /* @__PURE__ */ jsx("div", { className: "resira-dish-error", role: "alert", "aria-live": "assertive", children: /* @__PURE__ */ jsx("p", { children: error }) }),
3795
3956
  !loading && !error && /* @__PURE__ */ jsx(Fragment, { children: selectedDish ? /* @__PURE__ */ jsx(
3796
3957
  ModelViewer3D,
3797
3958
  {