@voyant-travel/catalog-react 0.117.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +36 -0
  3. package/dist/admin/catalog-vertical-host.d.ts +45 -0
  4. package/dist/admin/catalog-vertical-host.d.ts.map +1 -0
  5. package/dist/admin/catalog-vertical-host.js +230 -0
  6. package/dist/admin/cruise-detail-host.d.ts +11 -0
  7. package/dist/admin/cruise-detail-host.d.ts.map +1 -0
  8. package/dist/admin/cruise-detail-host.js +33 -0
  9. package/dist/admin/dynamic-catalog-host.d.ts +13 -0
  10. package/dist/admin/dynamic-catalog-host.d.ts.map +1 -0
  11. package/dist/admin/dynamic-catalog-host.js +17 -0
  12. package/dist/admin/index.d.ts +133 -0
  13. package/dist/admin/index.d.ts.map +1 -0
  14. package/dist/admin/index.js +144 -0
  15. package/dist/admin/open-in-new-tab.d.ts +7 -0
  16. package/dist/admin/open-in-new-tab.d.ts.map +1 -0
  17. package/dist/admin/open-in-new-tab.js +10 -0
  18. package/dist/admin/pages/catalog-accommodations-detail-page.d.ts +4 -0
  19. package/dist/admin/pages/catalog-accommodations-detail-page.d.ts.map +1 -0
  20. package/dist/admin/pages/catalog-accommodations-detail-page.js +7 -0
  21. package/dist/admin/pages/catalog-accommodations-index-page.d.ts +8 -0
  22. package/dist/admin/pages/catalog-accommodations-index-page.d.ts.map +1 -0
  23. package/dist/admin/pages/catalog-accommodations-index-page.js +17 -0
  24. package/dist/admin/pages/catalog-cruises-detail-page.d.ts +4 -0
  25. package/dist/admin/pages/catalog-cruises-detail-page.d.ts.map +1 -0
  26. package/dist/admin/pages/catalog-cruises-detail-page.js +7 -0
  27. package/dist/admin/pages/catalog-cruises-index-page.d.ts +8 -0
  28. package/dist/admin/pages/catalog-cruises-index-page.d.ts.map +1 -0
  29. package/dist/admin/pages/catalog-cruises-index-page.js +19 -0
  30. package/dist/admin/pages/catalog-excursions-detail-page.d.ts +4 -0
  31. package/dist/admin/pages/catalog-excursions-detail-page.d.ts.map +1 -0
  32. package/dist/admin/pages/catalog-excursions-detail-page.js +7 -0
  33. package/dist/admin/pages/catalog-excursions-index-page.d.ts +8 -0
  34. package/dist/admin/pages/catalog-excursions-index-page.d.ts.map +1 -0
  35. package/dist/admin/pages/catalog-excursions-index-page.js +12 -0
  36. package/dist/admin/pages/catalog-products-detail-page.d.ts +8 -0
  37. package/dist/admin/pages/catalog-products-detail-page.d.ts.map +1 -0
  38. package/dist/admin/pages/catalog-products-detail-page.js +12 -0
  39. package/dist/admin/pages/catalog-products-index-page.d.ts +8 -0
  40. package/dist/admin/pages/catalog-products-index-page.d.ts.map +1 -0
  41. package/dist/admin/pages/catalog-products-index-page.js +12 -0
  42. package/dist/admin/pages/catalog-tours-detail-page.d.ts +4 -0
  43. package/dist/admin/pages/catalog-tours-detail-page.d.ts.map +1 -0
  44. package/dist/admin/pages/catalog-tours-detail-page.js +7 -0
  45. package/dist/admin/pages/catalog-tours-index-page.d.ts +8 -0
  46. package/dist/admin/pages/catalog-tours-index-page.d.ts.map +1 -0
  47. package/dist/admin/pages/catalog-tours-index-page.js +12 -0
  48. package/dist/admin/product-detail-host.d.ts +18 -0
  49. package/dist/admin/product-detail-host.d.ts.map +1 -0
  50. package/dist/admin/product-detail-host.js +40 -0
  51. package/dist/admin/scheduled-catalog-host.d.ts +15 -0
  52. package/dist/admin/scheduled-catalog-host.d.ts.map +1 -0
  53. package/dist/admin/scheduled-catalog-host.js +19 -0
  54. package/dist/admin/vertical-detail-host.d.ts +13 -0
  55. package/dist/admin/vertical-detail-host.d.ts.map +1 -0
  56. package/dist/admin/vertical-detail-host.js +62 -0
  57. package/dist/booking-engine/index.d.ts +26 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +25 -0
  60. package/dist/booking-engine/use-booking-commit.d.ts +61 -0
  61. package/dist/booking-engine/use-booking-commit.d.ts.map +1 -0
  62. package/dist/booking-engine/use-booking-commit.js +47 -0
  63. package/dist/booking-engine/use-booking-draft-shape.d.ts +20 -0
  64. package/dist/booking-engine/use-booking-draft-shape.d.ts.map +1 -0
  65. package/dist/booking-engine/use-booking-draft-shape.js +9 -0
  66. package/dist/booking-engine/use-booking-draft.d.ts +102 -0
  67. package/dist/booking-engine/use-booking-draft.d.ts.map +1 -0
  68. package/dist/booking-engine/use-booking-draft.js +93 -0
  69. package/dist/booking-engine/use-booking-hold.d.ts +30 -0
  70. package/dist/booking-engine/use-booking-hold.d.ts.map +1 -0
  71. package/dist/booking-engine/use-booking-hold.js +44 -0
  72. package/dist/booking-engine/use-booking-journey-api.d.ts +23 -0
  73. package/dist/booking-engine/use-booking-journey-api.d.ts.map +1 -0
  74. package/dist/booking-engine/use-booking-journey-api.js +24 -0
  75. package/dist/booking-engine/use-booking-quote.d.ts +711 -0
  76. package/dist/booking-engine/use-booking-quote.d.ts.map +1 -0
  77. package/dist/booking-engine/use-booking-quote.js +122 -0
  78. package/dist/catalog-enrichment-mappers.d.ts +162 -0
  79. package/dist/catalog-enrichment-mappers.d.ts.map +1 -0
  80. package/dist/catalog-enrichment-mappers.js +190 -0
  81. package/dist/catalog-enrichment.d.ts +203 -0
  82. package/dist/catalog-enrichment.d.ts.map +1 -0
  83. package/dist/catalog-enrichment.js +130 -0
  84. package/dist/catalog-offers-client.d.ts +58 -0
  85. package/dist/catalog-offers-client.d.ts.map +1 -0
  86. package/dist/catalog-offers-client.js +61 -0
  87. package/dist/catalog-search-params.d.ts +45 -0
  88. package/dist/catalog-search-params.d.ts.map +1 -0
  89. package/dist/catalog-search-params.js +30 -0
  90. package/dist/catalog-surfaces.d.ts +17 -0
  91. package/dist/catalog-surfaces.d.ts.map +1 -0
  92. package/dist/catalog-surfaces.js +26 -0
  93. package/dist/client.d.ts +20 -0
  94. package/dist/client.d.ts.map +1 -0
  95. package/dist/client.js +65 -0
  96. package/dist/components/availability-calendar.d.ts +33 -0
  97. package/dist/components/availability-calendar.d.ts.map +1 -0
  98. package/dist/components/availability-calendar.js +65 -0
  99. package/dist/components/catalog-browse-page.d.ts +41 -0
  100. package/dist/components/catalog-browse-page.d.ts.map +1 -0
  101. package/dist/components/catalog-browse-page.js +47 -0
  102. package/dist/components/catalog-card.d.ts +68 -0
  103. package/dist/components/catalog-card.d.ts.map +1 -0
  104. package/dist/components/catalog-card.js +52 -0
  105. package/dist/components/catalog-detail-cruise-cards.d.ts +16 -0
  106. package/dist/components/catalog-detail-cruise-cards.d.ts.map +1 -0
  107. package/dist/components/catalog-detail-cruise-cards.js +54 -0
  108. package/dist/components/catalog-detail-departures.d.ts +25 -0
  109. package/dist/components/catalog-detail-departures.d.ts.map +1 -0
  110. package/dist/components/catalog-detail-departures.js +240 -0
  111. package/dist/components/catalog-detail-parts.d.ts +70 -0
  112. package/dist/components/catalog-detail-parts.d.ts.map +1 -0
  113. package/dist/components/catalog-detail-parts.js +282 -0
  114. package/dist/components/catalog-detail-sheet.d.ts +93 -0
  115. package/dist/components/catalog-detail-sheet.d.ts.map +1 -0
  116. package/dist/components/catalog-detail-sheet.js +68 -0
  117. package/dist/components/catalog-detail-view.d.ts +39 -0
  118. package/dist/components/catalog-detail-view.d.ts.map +1 -0
  119. package/dist/components/catalog-detail-view.js +157 -0
  120. package/dist/components/catalog-enrichment-fetchers.d.ts +8 -0
  121. package/dist/components/catalog-enrichment-fetchers.d.ts.map +1 -0
  122. package/dist/components/catalog-enrichment-fetchers.js +7 -0
  123. package/dist/components/catalog-faceted-filter.d.ts +30 -0
  124. package/dist/components/catalog-faceted-filter.d.ts.map +1 -0
  125. package/dist/components/catalog-faceted-filter.js +24 -0
  126. package/dist/components/catalog-filter-rail.d.ts +25 -0
  127. package/dist/components/catalog-filter-rail.d.ts.map +1 -0
  128. package/dist/components/catalog-filter-rail.js +88 -0
  129. package/dist/components/catalog-gallery.d.ts +27 -0
  130. package/dist/components/catalog-gallery.d.ts.map +1 -0
  131. package/dist/components/catalog-gallery.js +66 -0
  132. package/dist/components/catalog-hit.d.ts +27 -0
  133. package/dist/components/catalog-hit.d.ts.map +1 -0
  134. package/dist/components/catalog-hit.js +57 -0
  135. package/dist/components/catalog-page-cards.d.ts +21 -0
  136. package/dist/components/catalog-page-cards.d.ts.map +1 -0
  137. package/dist/components/catalog-page-cards.js +174 -0
  138. package/dist/components/catalog-page-config.d.ts +17 -0
  139. package/dist/components/catalog-page-config.d.ts.map +1 -0
  140. package/dist/components/catalog-page-config.js +369 -0
  141. package/dist/components/catalog-page.d.ts +88 -0
  142. package/dist/components/catalog-page.d.ts.map +1 -0
  143. package/dist/components/catalog-page.js +148 -0
  144. package/dist/components/catalog-range-filter.d.ts +34 -0
  145. package/dist/components/catalog-range-filter.d.ts.map +1 -0
  146. package/dist/components/catalog-range-filter.js +72 -0
  147. package/dist/components/catalog-search-page.d.ts +239 -0
  148. package/dist/components/catalog-search-page.d.ts.map +1 -0
  149. package/dist/components/catalog-search-page.js +63 -0
  150. package/dist/components/catalog-search-tab-panel.d.ts +42 -0
  151. package/dist/components/catalog-search-tab-panel.d.ts.map +1 -0
  152. package/dist/components/catalog-search-tab-panel.js +199 -0
  153. package/dist/components/catalog-vertical-detail-page.d.ts +33 -0
  154. package/dist/components/catalog-vertical-detail-page.d.ts.map +1 -0
  155. package/dist/components/catalog-vertical-detail-page.js +100 -0
  156. package/dist/components/cruise-detail-page-parts.d.ts +72 -0
  157. package/dist/components/cruise-detail-page-parts.d.ts.map +1 -0
  158. package/dist/components/cruise-detail-page-parts.js +146 -0
  159. package/dist/components/cruise-detail-page.d.ts +21 -0
  160. package/dist/components/cruise-detail-page.d.ts.map +1 -0
  161. package/dist/components/cruise-detail-page.js +201 -0
  162. package/dist/components/dynamic-catalog-page-parts.d.ts +40 -0
  163. package/dist/components/dynamic-catalog-page-parts.d.ts.map +1 -0
  164. package/dist/components/dynamic-catalog-page-parts.js +43 -0
  165. package/dist/components/dynamic-catalog-page.d.ts +23 -0
  166. package/dist/components/dynamic-catalog-page.d.ts.map +1 -0
  167. package/dist/components/dynamic-catalog-page.js +270 -0
  168. package/dist/components/media-gallery.d.ts +13 -0
  169. package/dist/components/media-gallery.d.ts.map +1 -0
  170. package/dist/components/media-gallery.js +42 -0
  171. package/dist/components/product-detail-page-parts.d.ts +106 -0
  172. package/dist/components/product-detail-page-parts.d.ts.map +1 -0
  173. package/dist/components/product-detail-page-parts.js +130 -0
  174. package/dist/components/product-detail-page.d.ts +57 -0
  175. package/dist/components/product-detail-page.d.ts.map +1 -0
  176. package/dist/components/product-detail-page.js +175 -0
  177. package/dist/components/scheduled-catalog-page.d.ts +34 -0
  178. package/dist/components/scheduled-catalog-page.d.ts.map +1 -0
  179. package/dist/components/scheduled-catalog-page.js +6 -0
  180. package/dist/hooks/index.d.ts +3 -0
  181. package/dist/hooks/index.d.ts.map +1 -0
  182. package/dist/hooks/index.js +2 -0
  183. package/dist/hooks/use-catalog-offers.d.ts +186 -0
  184. package/dist/hooks/use-catalog-offers.d.ts.map +1 -0
  185. package/dist/hooks/use-catalog-offers.js +105 -0
  186. package/dist/hooks/use-catalog-search.d.ts +109 -0
  187. package/dist/hooks/use-catalog-search.d.ts.map +1 -0
  188. package/dist/hooks/use-catalog-search.js +52 -0
  189. package/dist/i18n/en.d.ts +397 -0
  190. package/dist/i18n/en.d.ts.map +1 -0
  191. package/dist/i18n/en.js +396 -0
  192. package/dist/i18n/index.d.ts +5 -0
  193. package/dist/i18n/index.d.ts.map +1 -0
  194. package/dist/i18n/index.js +3 -0
  195. package/dist/i18n/messages.d.ts +335 -0
  196. package/dist/i18n/messages.d.ts.map +1 -0
  197. package/dist/i18n/messages.js +1 -0
  198. package/dist/i18n/provider.d.ts +816 -0
  199. package/dist/i18n/provider.d.ts.map +1 -0
  200. package/dist/i18n/provider.js +44 -0
  201. package/dist/i18n/ro.d.ts +397 -0
  202. package/dist/i18n/ro.d.ts.map +1 -0
  203. package/dist/i18n/ro.js +396 -0
  204. package/dist/index.d.ts +9 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +17 -0
  207. package/dist/provider.d.ts +2 -0
  208. package/dist/provider.d.ts.map +1 -0
  209. package/dist/provider.js +1 -0
  210. package/dist/schemas-catalog-offers.d.ts +290 -0
  211. package/dist/schemas-catalog-offers.d.ts.map +1 -0
  212. package/dist/schemas-catalog-offers.js +155 -0
  213. package/dist/schemas.d.ts +143 -0
  214. package/dist/schemas.d.ts.map +1 -0
  215. package/dist/schemas.js +76 -0
  216. package/dist/ui.d.ts +19 -0
  217. package/dist/ui.d.ts.map +1 -0
  218. package/dist/ui.js +18 -0
  219. package/package.json +150 -0
  220. package/src/styles.css +11 -0
@@ -0,0 +1,93 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CatalogDeparturePricingRow, CatalogDetailEnrichment, CatalogSearchHit } from "../index.js";
3
+ export { CatalogDetailView, type CatalogDetailViewProps } from "./catalog-detail-view.js";
4
+ export interface CatalogDetailAction {
5
+ label: string;
6
+ onClick: (hit: CatalogSearchHit) => void;
7
+ variant?: "default" | "secondary" | "outline" | "ghost";
8
+ /**
9
+ * Optional predicate to hide the action for specific hits — e.g. only
10
+ * show "Open editor" when the hit is an owned product. Defaults to
11
+ * always-visible when omitted.
12
+ */
13
+ visible?: (hit: CatalogSearchHit) => boolean;
14
+ }
15
+ export type { CatalogDetailEnrichment };
16
+ export type CatalogDetailSheetWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | string;
17
+ export type CatalogDetailItineraryDay = NonNullable<CatalogDetailEnrichment["itinerary"]>[number];
18
+ export type CatalogDetailRenderSlot = (hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment | null) => ReactNode;
19
+ export interface CatalogDetailSheetProps {
20
+ hit: CatalogSearchHit | null;
21
+ onOpenChange: (open: boolean) => void;
22
+ formatters?: Record<string, (value: unknown) => ReactNode>;
23
+ actions?: CatalogDetailAction[];
24
+ imageField?: string;
25
+ /**
26
+ * The catalog vertical this sheet renders. Used for per-vertical labels —
27
+ * e.g. cruises label the "Options" tab "Cabins" since the options are cabin
28
+ * categories.
29
+ */
30
+ vertical?: string;
31
+ /**
32
+ * Sheet max-width token or class name. Defaults to `5xl` so rich catalog
33
+ * details can use two columns on wide operator screens.
34
+ */
35
+ width?: CatalogDetailSheetWidth;
36
+ /** Secondary header content such as print/download icon buttons. */
37
+ headerExtras?: ReactNode | CatalogDetailRenderSlot;
38
+ /**
39
+ * Called once when the sheet opens for a hit. The result is rendered
40
+ * in dedicated sections (description, highlights, itinerary, media,
41
+ * options, policies). Errors swallowed silently — the sheet still
42
+ * shows the indexed-projection fallback.
43
+ */
44
+ onLoadDetail?: (hit: CatalogSearchHit) => Promise<CatalogDetailEnrichment | null>;
45
+ /**
46
+ * Called lazily when a departure (cruise sailing) row expands, to fetch live
47
+ * per-cabin pricing + availability. Returns `null` when unavailable. The
48
+ * caller binds the vertical; the sheet passes the cruise hit + sailing ref.
49
+ */
50
+ onLoadDeparturePricing?: (hit: CatalogSearchHit, sailingRef: string) => Promise<CatalogDeparturePricingRow[] | null>;
51
+ /**
52
+ * Called when the operator clicks a per-departure Book button. When
53
+ * omitted, departures render without a book affordance — the
54
+ * sheet-level `actions` (e.g. "Book this") still apply.
55
+ */
56
+ onBookDeparture?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number]) => void;
57
+ /**
58
+ * Per-option book affordance. When set, the expanded departure panel
59
+ * renders a Book button on each option row; the callback receives the
60
+ * departure plus the chosen option so the booking journey can
61
+ * pre-select it. Falls back to `onBookDeparture` when omitted.
62
+ */
63
+ onBookOption?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number], option: NonNullable<CatalogDetailEnrichment["options"]>[number]) => void;
64
+ /** Dedicated brochure/print section rendered above media. */
65
+ renderBrochure?: CatalogDetailRenderSlot;
66
+ /** Consumer-provided media rendering, replacing the default thumbnail grid. */
67
+ renderMedia?: CatalogDetailRenderSlot;
68
+ /** Consumer-provided itinerary day renderer for richer day cards. */
69
+ renderItineraryDay?: (day: CatalogDetailItineraryDay, hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment) => ReactNode;
70
+ /** Additional consumer sections rendered above the footer actions. */
71
+ renderExtraSections?: CatalogDetailRenderSlot;
72
+ /**
73
+ * Render a clickable supplier link in the Attributes tab. When set,
74
+ * the `supplierId` row uses this renderer instead of the plain
75
+ * formatter — typically a router-aware Link to the supplier detail
76
+ * page. Receives the supplier id and the display name (already
77
+ * resolved by `formatters.supplierId`).
78
+ */
79
+ renderSupplierLink?: (supplierId: string, displayName: string) => ReactNode;
80
+ /**
81
+ * When provided, the Tags row in the Overview tab swaps its read-only
82
+ * chips for an inline add/remove editor. The callback is invoked with
83
+ * the *next* tag list (after the local optimistic update); rejecting
84
+ * the promise reverts to the indexed tags.
85
+ *
86
+ * Only the `tags` field — the operator-authored jsonb list — is
87
+ * editable. Index-derived arrays (categories, regions, etc.) stay
88
+ * read-only because they're computed from elsewhere.
89
+ */
90
+ onTagsChange?: (hit: CatalogSearchHit, tags: string[]) => Promise<void> | void;
91
+ }
92
+ export declare function CatalogDetailSheet({ hit, onOpenChange, formatters, actions, imageField, width, vertical, headerExtras, onLoadDetail, onLoadDeparturePricing, onBookDeparture, onBookOption, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onTagsChange, }: CatalogDetailSheetProps): import("react/jsx-runtime").JSX.Element;
93
+ //# sourceMappingURL=catalog-detail-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-detail-sheet.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-sheet.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAA;AAG3D,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,aAAa,CAAA;AAWpB,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AAEzF,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAA;IACvD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAA;CAC7C;AAKD,YAAY,EAAE,uBAAuB,EAAE,CAAA;AAEvC,MAAM,MAAM,uBAAuB,GAC/B,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,MAAM,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG,WAAW,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAEjG,MAAM,MAAM,uBAAuB,GAAG,CACpC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,GAAG,IAAI,KACvC,SAAS,CAAA;AAEd,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAA;IAC1D,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,uBAAuB,CAAA;IAC/B,oEAAoE;IACpE,YAAY,CAAC,EAAE,SAAS,GAAG,uBAAuB,CAAA;IAClD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IACjF;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,CACvB,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAChB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAClE,IAAI,CAAA;IACT;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,EACrE,MAAM,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAC5D,IAAI,CAAA;IACT,6DAA6D;IAC7D,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,uBAAuB,CAAA;IACrC,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,yBAAyB,EAC9B,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,KAChC,SAAS,CAAA;IACd,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,SAAS,CAAA;IAC3E;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC/E;AAED,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,YAAY,EACZ,UAAU,EACV,OAAO,EACP,UAA2B,EAC3B,KAAa,EACb,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,GACb,EAAE,uBAAuB,2CAyKzB"}
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge } from "@voyant-travel/ui/components/badge";
4
+ import { Button } from "@voyant-travel/ui/components/button";
5
+ import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyant-travel/ui/components/sheet";
6
+ import { cn } from "@voyant-travel/ui/lib/utils";
7
+ import { useEffect, useState } from "react";
8
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
9
+ import { formatTemplate, IdChip, initialsOf, ProductPriceFrom, sheetWidthClass, stringOr, } from "./catalog-detail-parts.js";
10
+ import { CatalogDetailView } from "./catalog-detail-view.js";
11
+ export { CatalogDetailView } from "./catalog-detail-view.js";
12
+ export function CatalogDetailSheet({ hit, onOpenChange, formatters, actions, imageField = "thumbnailUrl", width = "5xl", vertical, headerExtras, onLoadDetail, onLoadDeparturePricing, onBookDeparture, onBookOption, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onTagsChange, }) {
13
+ const catalogMessages = useCatalogUiMessagesOrDefault().catalogPage;
14
+ const messages = catalogMessages.detail;
15
+ const open = hit != null;
16
+ const fields = hit?.document.fields ?? {};
17
+ const name = stringOr(fields.name, catalogMessages.fallbacks.detailName);
18
+ const status = stringOr(fields.status, null);
19
+ // Enrichment fetch — fires when the sheet opens for a hit. The
20
+ // useEffect dep list includes hit.id so re-opening a different row
21
+ // refetches; the result is local to the open instance, so closing
22
+ // the sheet clears it.
23
+ const [enrichment, setEnrichment] = useState(null);
24
+ const [enrichmentLoading, setEnrichmentLoading] = useState(false);
25
+ useEffect(() => {
26
+ if (!hit || !onLoadDetail) {
27
+ setEnrichment(null);
28
+ setEnrichmentLoading(false);
29
+ return;
30
+ }
31
+ let cancelled = false;
32
+ setEnrichmentLoading(true);
33
+ setEnrichment(null);
34
+ onLoadDetail(hit)
35
+ .then((result) => {
36
+ if (!cancelled) {
37
+ setEnrichment(result ?? null);
38
+ setEnrichmentLoading(false);
39
+ }
40
+ })
41
+ .catch(() => {
42
+ if (!cancelled) {
43
+ setEnrichment(null);
44
+ setEnrichmentLoading(false);
45
+ }
46
+ });
47
+ return () => {
48
+ cancelled = true;
49
+ };
50
+ }, [hit, onLoadDetail]);
51
+ const imageUrl = stringOr(enrichment?.heroImageUrl, null) ?? stringOr(fields[imageField], null);
52
+ const resolvedHeaderExtras = hit && headerExtras
53
+ ? typeof headerExtras === "function"
54
+ ? headerExtras(hit, enrichment)
55
+ : headerExtras
56
+ : null;
57
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsx(SheetContent, { className: cn("w-full p-0", sheetWidthClass(width)), children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsx(SheetHeader, { className: "border-b bg-muted/20 px-6 py-5", children: _jsxs("div", { className: "flex items-start justify-between gap-4 pr-8", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-4", children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: name, className: "h-16 w-16 shrink-0 rounded-lg object-cover ring-1 ring-border", loading: "lazy" })) : (_jsx("div", { className: "flex h-16 w-16 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-sky-500 to-indigo-600 text-lg font-medium text-white ring-1 ring-border", children: initialsOf(name) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsx(SheetTitle, { className: "text-base leading-snug", children: name }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [status && (_jsx(Badge, { variant: status === "active" || status === "live" ? "default" : "secondary", className: "capitalize", children: status })), hit?.id && _jsx(IdChip, { id: hit.id }), enrichment?.servedLocale && (_jsx(Badge, { variant: "outline", className: "font-normal", children: enrichment.servedLocale })), enrichment?.matchKind && enrichment.matchKind !== "exact" && (_jsx(Badge, { variant: "outline", className: "font-normal", children: formatTemplate(messages.matchPrefix, { kind: enrichment.matchKind }) })), enrichment?.source && (_jsx(Badge, { variant: enrichment.source === "synthesized" ? "outline" : "secondary", className: "font-normal", children: enrichment.source })), enrichment?.servedStale && (_jsx(Badge, { variant: "outline", className: "font-normal", children: messages.stale })), enrichment?.machineTranslated && (_jsx(Badge, { variant: "outline", className: "font-normal", children: "MT" }))] })] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [_jsx(ProductPriceFrom, { enrichment: enrichment, fields: fields, messages: messages }), resolvedHeaderExtras && (_jsx("div", { className: "flex items-center gap-2", children: resolvedHeaderExtras }))] })] }) }), _jsx(CatalogDetailView, { hit: hit, enrichment: enrichment, enrichmentLoading: enrichmentLoading, vertical: vertical, formatters: formatters, renderBrochure: renderBrochure, renderMedia: renderMedia, renderItineraryDay: renderItineraryDay, renderExtraSections: renderExtraSections, renderSupplierLink: renderSupplierLink, onLoadDeparturePricing: onLoadDeparturePricing, onBookDeparture: onBookDeparture, onBookOption: onBookOption, onTagsChange: onTagsChange, className: "flex-1 overflow-y-auto px-6 py-5" }), (() => {
58
+ if (!hit || !actions || actions.length === 0)
59
+ return null;
60
+ const visibleActions = actions.filter((a) => (a.visible ? a.visible(hit) : true));
61
+ if (visibleActions.length === 0)
62
+ return null;
63
+ return (_jsx(SheetFooter, { className: "border-t bg-muted/20 px-6 py-3", children: _jsx("div", { className: "flex flex-wrap justify-end gap-2", children: visibleActions.map((a) => (_jsx(Button, { variant: a.variant ?? "default", size: "sm", onClick: () => {
64
+ a.onClick(hit);
65
+ onOpenChange(false);
66
+ }, children: a.label }, a.label))) }) }));
67
+ })()] }) }) }));
68
+ }
@@ -0,0 +1,39 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CatalogDeparturePricingRow, CatalogDetailEnrichment, CatalogSearchHit } from "../index.js";
3
+ import type { CatalogDetailItineraryDay, CatalogDetailRenderSlot } from "./catalog-detail-sheet.js";
4
+ /**
5
+ * Right-side detail sheet for any catalog hit. Header shows the entity's
6
+ * primary image, name, status, and id. Body is split into Description,
7
+ * Highlights/Tags (when present), and a clean two-column attribute grid.
8
+ * System provenance fields are tucked into a collapsible at the bottom.
9
+ */
10
+ export interface CatalogDetailViewProps {
11
+ hit: CatalogSearchHit | null;
12
+ enrichment: CatalogDetailEnrichment | null;
13
+ /** Show the "loading full content" hint above the tabs. */
14
+ enrichmentLoading?: boolean;
15
+ vertical?: string;
16
+ formatters?: Record<string, (value: unknown) => ReactNode>;
17
+ renderBrochure?: CatalogDetailRenderSlot;
18
+ renderMedia?: CatalogDetailRenderSlot;
19
+ renderItineraryDay?: (day: CatalogDetailItineraryDay, hit: CatalogSearchHit, enrichment: CatalogDetailEnrichment) => ReactNode;
20
+ renderExtraSections?: CatalogDetailRenderSlot;
21
+ renderSupplierLink?: (supplierId: string, displayName: string) => ReactNode;
22
+ onLoadDeparturePricing?: (hit: CatalogSearchHit, sailingRef: string) => Promise<CatalogDeparturePricingRow[] | null>;
23
+ onBookDeparture?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number]) => void;
24
+ onBookOption?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number], option: NonNullable<CatalogDetailEnrichment["options"]>[number]) => void;
25
+ onTagsChange?: (hit: CatalogSearchHit, tags: string[]) => Promise<void> | void;
26
+ /** Wrapper className — the sheet scrolls (`flex-1 overflow-y-auto px-6 py-5`),
27
+ * a full-page host can pass its own (or nothing). */
28
+ className?: string;
29
+ }
30
+ /**
31
+ * The tabbed detail BODY for a catalog hit — overview, itinerary, ship,
32
+ * cabins/rooms/options, departures/sailings (with live cabin pricing), media,
33
+ * policies, attributes. Layout-agnostic: it owns no modal/page chrome, so it's
34
+ * shared by {@link CatalogDetailSheet} (in a Sheet) and the operator's
35
+ * full-page, new-tab detail route. Enrichment is passed in (the host owns the
36
+ * fetch) since the host header also needs it.
37
+ */
38
+ export declare function CatalogDetailView({ hit, enrichment, enrichmentLoading, vertical, formatters, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onLoadDeparturePricing, onBookDeparture, onBookOption, onTagsChange, className, }: CatalogDetailViewProps): import("react/jsx-runtime").JSX.Element;
39
+ //# sourceMappingURL=catalog-detail-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-detail-view.d.ts","sourceRoot":"","sources":["../../src/components/catalog-detail-view.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAA;AAE/C,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,aAAa,CAAA;AAcpB,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AA6DnG;;;;;GAKG;AAEH,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,uBAAuB,GAAG,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAA;IAC1D,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,WAAW,CAAC,EAAE,uBAAuB,CAAA;IACrC,kBAAkB,CAAC,EAAE,CACnB,GAAG,EAAE,yBAAyB,EAC9B,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,uBAAuB,KAChC,SAAS,CAAA;IACd,mBAAmB,CAAC,EAAE,uBAAuB,CAAA;IAC7C,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,SAAS,CAAA;IAC3E,sBAAsB,CAAC,EAAE,CACvB,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,0BAA0B,EAAE,GAAG,IAAI,CAAC,CAAA;IACjD,eAAe,CAAC,EAAE,CAChB,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAClE,IAAI,CAAA;IACT,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,gBAAgB,EACrB,SAAS,EAAE,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,EACrE,MAAM,EAAE,WAAW,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAC5D,IAAI,CAAA;IACT,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC9E;0DACsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,UAAU,EACV,iBAAyB,EACzB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,SAAS,GACV,EAAE,sBAAsB,2CAmRxB"}
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyant-travel/ui/components/tabs";
4
+ import { Loader2 } from "lucide-react";
5
+ import { useMemo } from "react";
6
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
7
+ import { CabinCard, ShipCard } from "./catalog-detail-cruise-cards.js";
8
+ import { DeparturesTable } from "./catalog-detail-departures.js";
9
+ import { ArrayBadges, AttributeList, DefaultMediaGrid, formatTemplate, humanize, InlineTagsEditor, Section, stringOr, toStringArray, } from "./catalog-detail-parts.js";
10
+ import { MediaGallery } from "./media-gallery.js";
11
+ const HIDDEN_FIELDS = new Set([
12
+ "id",
13
+ "name",
14
+ "description",
15
+ "shortDescription",
16
+ "status",
17
+ "text_embedding",
18
+ "embedding_model_id",
19
+ ]);
20
+ const SYSTEM_FIELD_PREFIXES = ["source.", "seller."];
21
+ const ARRAY_FIELDS = new Set(["tags", "highlights", "regions", "themes", "defaultBookingModes"]);
22
+ /**
23
+ * Array fields the index emits that don't carry operator-facing value in
24
+ * the Tags & themes section — either duplicates of a friendlier sibling
25
+ * (`destinationSlugs` ↔ `regions`/`countries`/`cities`; `categorySlugs`
26
+ * ↔ `categoryIds`; `tagIds` ↔ `tagLabels`/`tags`) or noise derived from
27
+ * other rendered fields (`departureMonths` is a roll-up of
28
+ * `departureDates`). Hidden at render time; the underlying index still
29
+ * carries them for facets/filters.
30
+ */
31
+ const HIDDEN_ARRAY_FIELDS = new Set([
32
+ // Departure surface lives in its own Departures tab now — the
33
+ // raw date / month chip lists in Tags & themes are redundant.
34
+ "departureDates",
35
+ "departureMonths",
36
+ "destinationIds",
37
+ "destinationSlugs",
38
+ // Three category projections cover the same relation: keep the
39
+ // friendly localized `categories` list, drop the id + slug mirrors.
40
+ "categoryIds",
41
+ "categorySlugs",
42
+ // `tagIds` + `tagLabels` mirror the relational tags table; keep only
43
+ // the operator-authored `tags` jsonb column.
44
+ "tagIds",
45
+ "tagLabels",
46
+ // Canonical geography id mirrors — the resolved name lists
47
+ // (`countries`/`regions`/`ports`/`waterways`) carry the display values, so
48
+ // the raw id columns are noise in the overview.
49
+ "country_iso",
50
+ "region_ids",
51
+ "port_ids",
52
+ "waterway_ids",
53
+ ]);
54
+ /**
55
+ * Array-field label overrides for the Tags & themes section. Falls
56
+ * through to `humanize(key)` when not present. Localized variants come
57
+ * in via `messages.detail.arrayLabels` per locale — this stays as a
58
+ * stable fallback for any call path that hasn't been wired yet.
59
+ */
60
+ const ARRAY_LABEL_OVERRIDES = {
61
+ // i18n-literal-ok: fallback when no localized override is provided
62
+ categories: "Category",
63
+ };
64
+ /**
65
+ * The tabbed detail BODY for a catalog hit — overview, itinerary, ship,
66
+ * cabins/rooms/options, departures/sailings (with live cabin pricing), media,
67
+ * policies, attributes. Layout-agnostic: it owns no modal/page chrome, so it's
68
+ * shared by {@link CatalogDetailSheet} (in a Sheet) and the operator's
69
+ * full-page, new-tab detail route. Enrichment is passed in (the host owns the
70
+ * fetch) since the host header also needs it.
71
+ */
72
+ export function CatalogDetailView({ hit, enrichment, enrichmentLoading = false, vertical, formatters, renderBrochure, renderMedia, renderItineraryDay, renderExtraSections, renderSupplierLink, onLoadDeparturePricing, onBookDeparture, onBookOption, onTagsChange, className, }) {
73
+ const catalogMessages = useCatalogUiMessagesOrDefault().catalogPage;
74
+ const messages = catalogMessages.detail;
75
+ // Cruises sell cabin categories, not generic options — label the tab "Cabins".
76
+ const optionsLabel = vertical === "cruises" ? messages.cabins : messages.options;
77
+ // In the cruise industry a scheduled departure is a "sailing".
78
+ const departuresLabel = vertical === "cruises" ? messages.sailings : messages.departures;
79
+ const fields = hit?.document.fields ?? {};
80
+ const name = stringOr(fields.name, catalogMessages.fallbacks.detailName);
81
+ const description = stringOr(enrichment?.description, null) ?? stringOr(fields.description, null);
82
+ const shortDescription = stringOr(enrichment?.shortDescription, null) ?? stringOr(fields.shortDescription, null);
83
+ const brochureContent = hit && renderBrochure ? renderBrochure(hit, enrichment) : null;
84
+ const hasCustomMediaRenderer = hit != null && renderMedia != null;
85
+ const mediaContent = hit && renderMedia ? renderMedia(hit, enrichment) : null;
86
+ const shouldRenderMediaSection = hasCustomMediaRenderer
87
+ ? mediaContent !== null && mediaContent !== undefined && mediaContent !== false
88
+ : enrichment?.media != null && enrichment.media.length > 0;
89
+ const extraSections = hit && renderExtraSections ? renderExtraSections(hit, enrichment) : null;
90
+ const { arrayEntries, attributeEntries, systemEntries } = useMemo(() => {
91
+ const allEntries = Object.entries(fields).filter(([k]) => !HIDDEN_FIELDS.has(k));
92
+ const array = [];
93
+ const attrs = [];
94
+ const system = [];
95
+ for (const [k, v] of allEntries) {
96
+ if (SYSTEM_FIELD_PREFIXES.some((p) => k.startsWith(p)))
97
+ system.push([k, v]);
98
+ else if (ARRAY_FIELDS.has(k) || Array.isArray(v)) {
99
+ if (HIDDEN_ARRAY_FIELDS.has(k))
100
+ continue;
101
+ if (k !== "tags" && Array.isArray(v) && v.length === 0)
102
+ continue;
103
+ array.push([k, v]);
104
+ }
105
+ else
106
+ attrs.push([k, v]);
107
+ }
108
+ return { arrayEntries: array, attributeEntries: attrs, systemEntries: system };
109
+ }, [fields]);
110
+ const overviewGalleryImages = useMemo(() => {
111
+ const urls = [];
112
+ if (enrichment?.heroImageUrl)
113
+ urls.push(enrichment.heroImageUrl);
114
+ for (const option of enrichment?.options ?? []) {
115
+ const cover = option.images?.[0];
116
+ if (cover)
117
+ urls.push(cover);
118
+ }
119
+ return Array.from(new Set(urls));
120
+ }, [enrichment]);
121
+ const reshapedAttributeEntries = (() => {
122
+ const map = new Map(attributeEntries);
123
+ const out = [];
124
+ let didAmount = false;
125
+ for (const [k, v] of attributeEntries) {
126
+ if (k === "pax")
127
+ continue;
128
+ if (k === "sellCurrency")
129
+ continue;
130
+ if (k === "sellAmountCents") {
131
+ const currency = stringOr(map.get("sellCurrency"), "USD");
132
+ out.push(["sellAmount", { amountCents: v, currency }]);
133
+ didAmount = true;
134
+ continue;
135
+ }
136
+ out.push([k, v]);
137
+ }
138
+ if (!didAmount && map.has("sellCurrency")) {
139
+ out.push(["sellCurrency", map.get("sellCurrency")]);
140
+ }
141
+ return out;
142
+ })();
143
+ const hasItinerary = (enrichment?.itinerary?.length ?? 0) > 0;
144
+ const hasShip = enrichment?.ship != null;
145
+ const hasOptions = (enrichment?.options?.length ?? 0) > 0;
146
+ const hasDepartures = (enrichment?.departures?.length ?? 0) > 0;
147
+ const hasPolicies = (enrichment?.policies?.length ?? 0) > 0;
148
+ const hasAttributes = reshapedAttributeEntries.length > 0 || arrayEntries.length > 0 || systemEntries.length > 0;
149
+ return (_jsxs("div", { className: className, children: [enrichmentLoading && (_jsxs("div", { className: "mb-4 flex items-center gap-2 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), messages.loadingFullContent] })), _jsxs(Tabs, { defaultValue: "overview", className: "gap-4", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "overview", children: messages.tabs.overview }), hasItinerary && _jsx(TabsTrigger, { value: "itinerary", children: messages.itinerary }), hasShip && _jsx(TabsTrigger, { value: "ship", children: messages.ship }), hasOptions && _jsx(TabsTrigger, { value: "options", children: optionsLabel }), hasDepartures && _jsx(TabsTrigger, { value: "departures", children: departuresLabel }), shouldRenderMediaSection && _jsx(TabsTrigger, { value: "media", children: messages.media }), hasPolicies && _jsx(TabsTrigger, { value: "policies", children: messages.policies }), hasAttributes && _jsx(TabsTrigger, { value: "attributes", children: messages.attributes })] }), _jsxs(TabsContent, { value: "overview", className: "flex flex-col gap-6", children: [overviewGalleryImages.length > 1 && (_jsx(Section, { title: messages.media, children: _jsx(MediaGallery, { images: overviewGalleryImages, alt: name, className: "w-full max-w-lg", imageClassName: "h-56 w-full" }) })), (shortDescription || description) && (_jsxs(Section, { children: [shortDescription && (_jsx("p", { className: "text-sm font-medium leading-relaxed text-foreground", children: shortDescription })), description && (_jsx("p", { className: "whitespace-pre-line text-sm leading-relaxed text-muted-foreground", children: description }))] })), enrichment?.highlights && enrichment.highlights.length > 0 && (_jsx(Section, { title: messages.highlights, children: _jsx("ul", { className: "ml-4 list-disc space-y-1 text-sm text-muted-foreground", children: enrichment.highlights.map((h) => (_jsx("li", { children: h }, h))) }) })), enrichment?.supplier && (_jsx(Section, { title: messages.supplier, children: _jsx("p", { className: "text-sm text-foreground", children: enrichment.supplier }) })), arrayEntries.length > 0 && (_jsx(Section, { title: messages.tagsThemes, children: _jsx("div", { className: "flex flex-col gap-3", children: arrayEntries.map(([key, value]) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx("span", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: ARRAY_LABEL_OVERRIDES[key] ?? humanize(key) }), key === "tags" && hit && onTagsChange ? (_jsx(InlineTagsEditor, { hit: hit, value: toStringArray(value), onChange: onTagsChange, placeholder: messages.tagsInputPlaceholder })) : (_jsx(ArrayBadges, { value: value }))] }, key))) }) })), brochureContent && _jsx(Section, { title: messages.brochure, children: brochureContent }), extraSections] }), hasItinerary && (_jsx(TabsContent, { value: "itinerary", className: "flex flex-col gap-2", children: _jsx("ol", { className: "space-y-2", children: enrichment?.itinerary?.map((d) => (_jsx("li", { children: renderItineraryDay && hit ? (renderItineraryDay(d, hit, enrichment)) : (_jsx(DefaultItineraryDay, { day: d, dayLabel: messages.day })) }, d.dayNumber))) }) })), hasShip && enrichment?.ship && (_jsx(TabsContent, { value: "ship", children: _jsx(ShipCard, { ship: enrichment.ship, messages: messages }) })), hasOptions && (_jsx(TabsContent, { value: "options", children: _jsx("ul", { className: "space-y-3", children: enrichment?.options?.map((o) => (_jsx(CabinCard, { cabin: o, messages: messages }, o.id))) }) })), hasDepartures && enrichment?.departures && (_jsx(TabsContent, { value: "departures", children: _jsx(DeparturesTable, { hit: hit, vertical: vertical, departures: enrichment.departures, options: enrichment?.options ?? [], onLoadDeparturePricing: onLoadDeparturePricing, productSellAmountCents: typeof fields.sellAmountCents === "number"
150
+ ? fields.sellAmountCents
151
+ : typeof fields.sellAmountCents === "string"
152
+ ? Number(fields.sellAmountCents) || null
153
+ : null, productSellCurrency: typeof fields.sellCurrency === "string" ? fields.sellCurrency : null, onBookDeparture: onBookDeparture, onBookOption: onBookOption, messages: messages }) })), shouldRenderMediaSection && (_jsx(TabsContent, { value: "media", children: mediaContent ?? _jsx(DefaultMediaGrid, { media: enrichment?.media ?? [] }) })), hasPolicies && (_jsx(TabsContent, { value: "policies", children: _jsx("dl", { className: "space-y-2 text-sm", children: enrichment?.policies?.map((p, idx) => (_jsxs("div", { children: [_jsx("dt", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: p.kind.replace(/_/g, " ") }), _jsx("dd", { className: "mt-0.5 whitespace-pre-line text-muted-foreground", children: p.body })] }, `${p.kind}-${idx}`))) }) })), hasAttributes && (_jsxs(TabsContent, { value: "attributes", className: "flex flex-col gap-6", children: [reshapedAttributeEntries.length > 0 && (_jsx(AttributeList, { entries: reshapedAttributeEntries, formatters: formatters, messages: catalogMessages, renderSupplierLink: renderSupplierLink })), systemEntries.length > 0 && (_jsxs("details", { className: "group rounded-lg border bg-muted/20", children: [_jsx("summary", { className: "cursor-pointer list-none px-4 py-2.5 text-xs font-medium uppercase tracking-wider text-muted-foreground hover:bg-muted/40 group-open:border-b", children: messages.system }), _jsx("div", { className: "px-4 py-3", children: _jsx(AttributeList, { entries: systemEntries, formatters: formatters, messages: catalogMessages }) })] }))] }))] })] }));
154
+ }
155
+ function DefaultItineraryDay({ day, dayLabel, }) {
156
+ return (_jsxs("div", { className: "flex gap-3 rounded-md border border-border bg-muted/10 p-3 text-sm", children: [day.heroImageUrl ? (_jsx("img", { src: day.heroImageUrl, alt: day.title ?? formatTemplate(dayLabel, { day: day.dayNumber }), className: "h-20 w-28 shrink-0 rounded-md object-cover ring-1 ring-border", loading: "lazy" })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: formatTemplate(dayLabel, { day: day.dayNumber }) }), day.title && _jsx("span", { className: "font-medium", children: day.title }), day.location && _jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u00B7 ", day.location] })] }), day.description && (_jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: day.description }))] })] }));
157
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Back-compat re-export. The catalog detail enrichment client + its
3
+ * `CatalogDetailEnrichment` view-model moved to the data layer
4
+ * (`@voyant-travel/catalog-react`); this shim preserves the
5
+ * `@voyant-travel/catalog-react/ui` import surface.
6
+ */
7
+ export { __resetEnrichmentFetcherWarnings, type CatalogDeparturePricingRow, type CatalogDetailEnrichment, type CatalogEnrichmentFetchers, type CatalogEnrichmentFetchersOptions, type CatalogSlotAvailability, createCatalogEnrichmentFetchers, } from "../index.js";
8
+ //# sourceMappingURL=catalog-enrichment-fetchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-enrichment-fetchers.d.ts","sourceRoot":"","sources":["../../src/components/catalog-enrichment-fetchers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,gCAAgC,EAChC,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,uBAAuB,EAC5B,+BAA+B,GAChC,MAAM,aAAa,CAAA"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Back-compat re-export. The catalog detail enrichment client + its
3
+ * `CatalogDetailEnrichment` view-model moved to the data layer
4
+ * (`@voyant-travel/catalog-react`); this shim preserves the
5
+ * `@voyant-travel/catalog-react/ui` import surface.
6
+ */
7
+ export { __resetEnrichmentFetcherWarnings, createCatalogEnrichmentFetchers, } from "../index.js";
@@ -0,0 +1,30 @@
1
+ import type { CatalogFacetBucket } from "../index.js";
2
+ export interface CatalogFacetedFilterProps {
3
+ /** Field name (matches the indexer document field). */
4
+ field: string;
5
+ /** Display label for the filter trigger. */
6
+ label: string;
7
+ /** Live facet buckets — values + counts from the search response. */
8
+ buckets: CatalogFacetBucket[];
9
+ /** Currently-selected values for this field. */
10
+ selected: Array<string | number>;
11
+ /** Toggle a value on/off. */
12
+ onToggle: (value: string | number) => void;
13
+ /** Clear all selections for this field. */
14
+ onClear: () => void;
15
+ /**
16
+ * Optional value formatter. Used to display human-readable labels for
17
+ * fields that store IDs — e.g. `lineSupplierId` resolved against a
18
+ * `Map<id, name>`. Falls back to `String(value)` when not provided.
19
+ */
20
+ formatValue?: (value: string | number) => string;
21
+ }
22
+ /**
23
+ * Faceted filter dropdown — modeled after shadcn's data-table-faceted-filter.
24
+ * Renders as a dashed-border button showing the field label and any active
25
+ * selections as compact badges. Opens a searchable command list with
26
+ * checkbox-style selection per bucket; counts come from the live facet
27
+ * response so the UI never lies about what's available.
28
+ */
29
+ export declare function CatalogFacetedFilter({ label, buckets, selected, onToggle, onClear, formatValue, }: CatalogFacetedFilterProps): import("react/jsx-runtime").JSX.Element;
30
+ //# sourceMappingURL=catalog-faceted-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-faceted-filter.d.ts","sourceRoot":"","sources":["../../src/components/catalog-faceted-filter.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,OAAO,EAAE,kBAAkB,EAAE,CAAA;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;IAChC,6BAA6B;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC1C,2CAA2C;IAC3C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM,CAAA;CACjD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,WAAW,GACZ,EAAE,yBAAyB,2CA0D3B"}
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button } from "@voyant-travel/ui/components/button";
4
+ import { Checkbox } from "@voyant-travel/ui/components/checkbox";
5
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@voyant-travel/ui/components/command";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
7
+ import { PlusCircle } from "lucide-react";
8
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
9
+ /**
10
+ * Faceted filter dropdown — modeled after shadcn's data-table-faceted-filter.
11
+ * Renders as a dashed-border button showing the field label and any active
12
+ * selections as compact badges. Opens a searchable command list with
13
+ * checkbox-style selection per bucket; counts come from the live facet
14
+ * response so the UI never lies about what's available.
15
+ */
16
+ export function CatalogFacetedFilter({ label, buckets, selected, onToggle, onClear, formatValue, }) {
17
+ const messages = useCatalogUiMessagesOrDefault().catalogPage.filtersUi;
18
+ const selectedSet = new Set(selected);
19
+ const display = (v) => (formatValue ? formatValue(v) : String(v));
20
+ return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: "h-8 gap-2 border-dashed" }), children: [_jsx(PlusCircle, { className: "h-3.5 w-3.5" }), _jsx("span", { children: label }), selected.length > 0 && (_jsx("span", { className: "-mr-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[11px] font-medium tabular-nums text-primary-foreground", children: selected.length }))] }), _jsx(PopoverContent, { className: "w-[220px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: label }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: messages.noResults }), _jsx(CommandGroup, { children: buckets.map((bucket) => {
21
+ const isSelected = selectedSet.has(bucket.value);
22
+ return (_jsxs(CommandItem, { onSelect: () => onToggle(bucket.value), children: [_jsx(Checkbox, { checked: isSelected, tabIndex: -1, "aria-hidden": true, className: "mr-2 pointer-events-none" }), _jsx("span", { className: "flex-1 truncate capitalize", children: display(bucket.value) }), _jsx("span", { className: "ml-2 text-muted-foreground text-xs", children: bucket.count })] }, String(bucket.value)));
23
+ }) }), selected.length > 0 && (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { onSelect: onClear, className: "justify-center text-center text-muted-foreground", children: messages.clearFilter }) })] }))] })] }) })] }));
24
+ }
@@ -0,0 +1,25 @@
1
+ import type { CatalogFacetBucket } from "../index.js";
2
+ import type { CatalogRangeFilterValue } from "./catalog-range-filter.js";
3
+ import type { CatalogFilterField } from "./catalog-search-page.js";
4
+ export interface CatalogFilterRailProps {
5
+ /** Visible filter fields (facets with buckets/selection + all ranges). */
6
+ fields: CatalogFilterField[];
7
+ /** Live facet buckets keyed by field. */
8
+ facetGroups: Record<string, CatalogFacetBucket[]>;
9
+ selectedFacets: Record<string, Array<string | number>>;
10
+ selectedRanges: Record<string, CatalogRangeFilterValue>;
11
+ onToggleFacet: (field: string, value: string | number) => void;
12
+ onClearFacet: (field: string) => void;
13
+ onSetRange: (field: string, value: CatalogRangeFilterValue | undefined) => void;
14
+ onClearAll: () => void;
15
+ hasSelections: boolean;
16
+ }
17
+ /**
18
+ * Persistent, sectioned left filter rail (Booking.com-style). Facets render
19
+ * as inline checkbox lists with live counts; numeric ranges render as inline
20
+ * min/max inputs. Replaces the wrapping chip row for a scannable, always-on
21
+ * filtering surface. Selection state is owned by the parent
22
+ * (`CatalogSearchPage`); this component is presentational.
23
+ */
24
+ export declare function CatalogFilterRail({ fields, facetGroups, selectedFacets, selectedRanges, onToggleFacet, onClearFacet, onSetRange, onClearAll, hasSelections, }: CatalogFilterRailProps): import("react/jsx-runtime").JSX.Element;
25
+ //# sourceMappingURL=catalog-filter-rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-filter-rail.d.ts","sourceRoot":"","sources":["../../src/components/catalog-filter-rail.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACxE,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,0BAA0B,CAAA;AAKjC,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC5B,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;IACtD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IACvD,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC9D,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,uBAAuB,GAAG,SAAS,KAAK,IAAI,CAAA;IAC/E,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,EACV,aAAa,GACd,EAAE,sBAAsB,2CAyDxB"}
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@voyant-travel/ui/components/button";
4
+ import { Checkbox } from "@voyant-travel/ui/components/checkbox";
5
+ import { Input } from "@voyant-travel/ui/components/input";
6
+ import { Separator } from "@voyant-travel/ui/components/separator";
7
+ import { X } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
10
+ /** How many facet values show before the "Show all" toggle. */
11
+ const DEFAULT_VISIBLE = 6;
12
+ /**
13
+ * Persistent, sectioned left filter rail (Booking.com-style). Facets render
14
+ * as inline checkbox lists with live counts; numeric ranges render as inline
15
+ * min/max inputs. Replaces the wrapping chip row for a scannable, always-on
16
+ * filtering surface. Selection state is owned by the parent
17
+ * (`CatalogSearchPage`); this component is presentational.
18
+ */
19
+ export function CatalogFilterRail({ fields, facetGroups, selectedFacets, selectedRanges, onToggleFacet, onClearFacet, onSetRange, onClearAll, hasSelections, }) {
20
+ const messages = useCatalogUiMessagesOrDefault().catalogPage;
21
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "font-medium text-sm", children: messages.view.filters }), hasSelections && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onClearAll, className: "h-7 px-2 text-muted-foreground text-xs hover:text-foreground", children: [_jsx(X, { className: "mr-1 h-3 w-3" }), messages.search.clearAll] }))] }), fields.map((field, index) => {
22
+ const isRange = (field.kind ?? "facet") === "range";
23
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [index > 0 && _jsx(Separator, { className: "mb-1" }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wide", children: field.label }), !isRange && (selectedFacets[field.field]?.length ?? 0) > 0 && (_jsx("button", { type: "button", onClick: () => onClearFacet(field.field), className: "text-muted-foreground text-xs hover:text-foreground", children: messages.filtersUi.clear }))] }), isRange ? (_jsx(RangeSection, { field: field, value: selectedRanges[field.field], onChange: (next) => onSetRange(field.field, next) })) : (_jsx(FacetSection, { field: field, buckets: facetGroups[field.field] ?? [], selected: selectedFacets[field.field] ?? [], onToggle: (value) => onToggleFacet(field.field, value) }))] }, field.field));
24
+ })] }));
25
+ }
26
+ function FacetSection({ field, buckets, selected, onToggle, }) {
27
+ const messages = useCatalogUiMessagesOrDefault().catalogPage;
28
+ const [expanded, setExpanded] = useState(false);
29
+ const selectedSet = new Set(selected);
30
+ const display = (value) => field.formatValue ? field.formatValue(value) : String(value);
31
+ if (buckets.length === 0) {
32
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: messages.filtersUi.noResults });
33
+ }
34
+ const ordered = sortBuckets(buckets, field.sortValues);
35
+ const visible = expanded ? ordered : ordered.slice(0, DEFAULT_VISIBLE);
36
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [visible.map((bucket) => {
37
+ const checked = selectedSet.has(bucket.value);
38
+ return (_jsxs("button", { type: "button", onClick: () => onToggle(bucket.value), className: "flex w-full items-center gap-2 text-left text-sm", children: [_jsx(Checkbox, { checked: checked, "aria-hidden": true, tabIndex: -1, className: "pointer-events-none" }), _jsx("span", { className: "flex-1 truncate capitalize", children: display(bucket.value) }), _jsx("span", { className: "text-muted-foreground text-xs tabular-nums", children: bucket.count })] }, String(bucket.value)));
39
+ }), buckets.length > DEFAULT_VISIBLE && (_jsx("button", { type: "button", onClick: () => setExpanded((prev) => !prev), className: "self-start text-primary text-xs hover:underline", children: expanded
40
+ ? messages.view.showLess
41
+ : messages.view.showAll.replace("{count}", String(buckets.length)) }))] }));
42
+ }
43
+ // Order facet buckets. Default keeps the index's count-descending order;
44
+ // "value-desc"/"value-asc" sort numerically when the values parse as numbers
45
+ // (star ratings 5 → 0), else lexicographically.
46
+ function sortBuckets(buckets, sort) {
47
+ if (!sort || sort === "count")
48
+ return buckets;
49
+ const dir = sort === "value-asc" ? 1 : -1;
50
+ return [...buckets].sort((a, b) => {
51
+ const an = Number(a.value);
52
+ const bn = Number(b.value);
53
+ if (Number.isFinite(an) && Number.isFinite(bn))
54
+ return (an - bn) * dir;
55
+ return String(a.value).localeCompare(String(b.value)) * dir;
56
+ });
57
+ }
58
+ function RangeSection({ field, value, onChange, }) {
59
+ const messages = useCatalogUiMessagesOrDefault().catalogPage.filtersUi;
60
+ const [minText, setMinText] = useState(value?.gte != null ? String(value.gte) : "");
61
+ const [maxText, setMaxText] = useState(value?.lte != null ? String(value.lte) : "");
62
+ useEffect(() => {
63
+ setMinText(value?.gte != null ? String(value.gte) : "");
64
+ setMaxText(value?.lte != null ? String(value.lte) : "");
65
+ }, [value]);
66
+ const apply = () => {
67
+ const gte = parseNumber(minText);
68
+ const lte = parseNumber(maxText);
69
+ if (gte == null && lte == null) {
70
+ onChange(undefined);
71
+ return;
72
+ }
73
+ onChange({ gte: gte ?? undefined, lte: lte ?? undefined });
74
+ };
75
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", inputMode: "decimal", step: field.step, placeholder: field.minPlaceholder ?? messages.min, value: minText, onChange: (e) => setMinText(e.target.value), onBlur: apply, onKeyDown: (e) => {
76
+ if (e.key === "Enter")
77
+ apply();
78
+ }, className: "h-8" }), _jsx("span", { className: "text-muted-foreground text-xs", children: messages.to }), _jsx(Input, { type: "number", inputMode: "decimal", step: field.step, placeholder: field.maxPlaceholder ?? messages.max, value: maxText, onChange: (e) => setMaxText(e.target.value), onBlur: apply, onKeyDown: (e) => {
79
+ if (e.key === "Enter")
80
+ apply();
81
+ }, className: "h-8" })] }));
82
+ }
83
+ function parseNumber(s) {
84
+ if (!s.trim())
85
+ return null;
86
+ const n = Number(s);
87
+ return Number.isFinite(n) ? n : null;
88
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared catalog gallery + lightbox — the Booking.com-style mosaic and
3
+ * full-screen viewer used by every catalog detail page (packages, cruises, …)
4
+ * so they all look identical. Layout/lightbox logic lives here once.
5
+ */
6
+ export interface GalleryImage {
7
+ src: string;
8
+ caption?: string | null;
9
+ }
10
+ export interface GalleryLightboxLabels {
11
+ close: string;
12
+ prev: string;
13
+ next: string;
14
+ }
15
+ export declare function Gallery({ images, photosLabel, onOpen, }: {
16
+ images: GalleryImage[];
17
+ photosLabel: string;
18
+ onOpen: (index: number) => void;
19
+ }): import("react/jsx-runtime").JSX.Element;
20
+ export declare function GalleryLightbox({ images, index, onIndex, onClose, labels, }: {
21
+ images: GalleryImage[];
22
+ index: number;
23
+ onIndex: (i: number) => void;
24
+ onClose: () => void;
25
+ labels: GalleryLightboxLabels;
26
+ }): import("react/jsx-runtime").JSX.Element | null;
27
+ //# sourceMappingURL=catalog-gallery.d.ts.map