@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,72 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, 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 { Input } from "@voyant-travel/ui/components/input";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
7
+ import { Separator } from "@voyant-travel/ui/components/separator";
8
+ import { PlusCircle } from "lucide-react";
9
+ import { useEffect, useState } from "react";
10
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
11
+ /**
12
+ * Numeric-range filter — popover trigger styled like the faceted filter
13
+ * for visual consistency. When active, the trigger shows the range as a
14
+ * compact badge ("≥ €100", "≤ €500", or "€100 – €500").
15
+ */
16
+ export function CatalogRangeFilter({ label, value, onChange, step = 1, minPlaceholder, maxPlaceholder, format = "number", currency, }) {
17
+ const messages = useCatalogUiMessagesOrDefault().catalogPage.filtersUi;
18
+ const resolvedMinPlaceholder = minPlaceholder ?? messages.min;
19
+ const resolvedMaxPlaceholder = maxPlaceholder ?? messages.max;
20
+ // Track local string state so users can type freely without the parent
21
+ // re-rendering between keystrokes.
22
+ const [minText, setMinText] = useState(value?.gte != null ? String(value.gte) : "");
23
+ const [maxText, setMaxText] = useState(value?.lte != null ? String(value.lte) : "");
24
+ useEffect(() => {
25
+ setMinText(value?.gte != null ? String(value.gte) : "");
26
+ setMaxText(value?.lte != null ? String(value.lte) : "");
27
+ }, [value]);
28
+ const apply = () => {
29
+ const gte = parseNumber(minText);
30
+ const lte = parseNumber(maxText);
31
+ if (gte == null && lte == null) {
32
+ onChange(undefined);
33
+ return;
34
+ }
35
+ onChange({ gte: gte ?? undefined, lte: lte ?? undefined });
36
+ };
37
+ const clear = () => {
38
+ setMinText("");
39
+ setMaxText("");
40
+ onChange(undefined);
41
+ };
42
+ const active = value?.gte != null || value?.lte != null;
43
+ const display = active ? formatRange(value, format, currency) : null;
44
+ return (_jsxs(Popover, { children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { variant: "outline", size: "sm", className: "h-8 border-dashed" }), children: [_jsx(PlusCircle, { className: "mr-2 h-4 w-4" }), label, active && display && (_jsxs(_Fragment, { children: [_jsx(Separator, { orientation: "vertical", className: "mx-2 h-4" }), _jsx(Badge, { variant: "secondary", className: "rounded-sm px-1 font-normal", children: display })] }))] }), _jsx(PopoverContent, { className: "w-[260px] p-3", align: "start", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", inputMode: "decimal", step: step, placeholder: resolvedMinPlaceholder, value: minText, onChange: (e) => setMinText(e.target.value), className: "h-8" }), _jsx("span", { className: "text-muted-foreground text-xs", children: messages.to }), _jsx(Input, { type: "number", inputMode: "decimal", step: step, placeholder: resolvedMaxPlaceholder, value: maxText, onChange: (e) => setMaxText(e.target.value), className: "h-8" })] }), _jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: clear, disabled: !active && !minText && !maxText, children: messages.clear }), _jsx(Button, { size: "sm", onClick: apply, children: messages.apply })] })] }) })] }));
45
+ }
46
+ function parseNumber(s) {
47
+ if (!s.trim())
48
+ return null;
49
+ const n = Number(s);
50
+ return Number.isFinite(n) ? n : null;
51
+ }
52
+ function formatRange(v, format, currency) {
53
+ if (!v)
54
+ return null;
55
+ const fmt = (n) => {
56
+ if (format === "currency" && currency) {
57
+ return new Intl.NumberFormat(undefined, {
58
+ style: "currency",
59
+ currency,
60
+ maximumFractionDigits: 0,
61
+ }).format(n);
62
+ }
63
+ return new Intl.NumberFormat().format(n);
64
+ };
65
+ if (v.gte != null && v.lte != null)
66
+ return `${fmt(v.gte)} – ${fmt(v.lte)}`;
67
+ if (v.gte != null)
68
+ return `≥ ${fmt(v.gte)}`;
69
+ if (v.lte != null)
70
+ return `≤ ${fmt(v.lte)}`;
71
+ return null;
72
+ }
@@ -0,0 +1,239 @@
1
+ import type { ColumnDef } from "@tanstack/react-table";
2
+ import { type ReactNode } from "react";
3
+ import type { CatalogSearchHit } from "../index.js";
4
+ import type { CatalogCardConfig } from "./catalog-card.js";
5
+ import type { CatalogDetailAction, CatalogDetailEnrichment, CatalogDetailRenderSlot, CatalogDetailSheetProps } from "./catalog-detail-sheet.js";
6
+ import { type CatalogFilterSelections } from "./catalog-search-tab-panel.js";
7
+ /**
8
+ * Result sort options. Mirrors `CatalogSearchSort` from `@voyant-travel/catalog-react`
9
+ * (not re-exported there, so kept as a literal union here). The shell maps the
10
+ * selection straight onto the search request's `sort`.
11
+ */
12
+ export type CatalogSortOption = "relevance" | "price-asc" | "price-desc" | "departure-asc" | "newest";
13
+ /**
14
+ * Declares a filter on a tab. Two kinds:
15
+ * - `facet` (default) — multi-select against live facet buckets. Hidden
16
+ * when the search response yields no buckets and
17
+ * nothing is currently selected.
18
+ * - `range` — numeric min/max range. Always visible (no facet response
19
+ * needed). Use `format: "currency"` for money fields
20
+ * stored as integer cents (e.g. `sellAmountCents`).
21
+ */
22
+ export type CatalogFilterField = CatalogFacetFilterField | CatalogRangeFilterField;
23
+ export interface CatalogFacetFilterField {
24
+ kind?: "facet";
25
+ /** Field name on the indexer document (e.g. "status", "bookingMode"). */
26
+ field: string;
27
+ /** Human-readable group label (already localized). */
28
+ label: string;
29
+ /**
30
+ * Optional value formatter. Use it when the underlying field is an ID and
31
+ * you want the dropdown to show the human-readable label instead — e.g.
32
+ * resolving `lineSupplierId` against a `Map<id, name>`. Returns the raw
33
+ * value as a string by default.
34
+ */
35
+ formatValue?: (value: string | number) => string;
36
+ /**
37
+ * How to order the facet buckets. Default `"count"` keeps the index's
38
+ * descending-by-count order. `"value-desc"`/`"value-asc"` sort by the bucket
39
+ * value numerically when possible (e.g. star ratings 5 → 0), else
40
+ * lexicographically.
41
+ */
42
+ sortValues?: "count" | "value-desc" | "value-asc";
43
+ }
44
+ export interface CatalogRangeFilterField {
45
+ kind: "range";
46
+ field: string;
47
+ label: string;
48
+ step?: number;
49
+ minPlaceholder?: string;
50
+ maxPlaceholder?: string;
51
+ /** When the field stores cents, set `"currency"` + `currency`. */
52
+ format?: "number" | "currency";
53
+ currency?: string;
54
+ }
55
+ /**
56
+ * One tab in the catalog search page. Each tab maps to a single vertical
57
+ * (`products`, `cruises`, `accommodations`, etc.) and supplies its own column
58
+ * definitions so per-vertical UI packages own their own visual language.
59
+ */
60
+ export interface CatalogSearchTab {
61
+ /** Stable id used as the TabsTrigger value + queryKey segment. */
62
+ id: string;
63
+ /** Human-readable tab label (already localized). */
64
+ label: string;
65
+ /** The catalog vertical to query — mapped to the slice's `vertical`. */
66
+ vertical: string;
67
+ /**
68
+ * Optional row/card open override. When set, clicking a result calls this
69
+ * instead of opening the in-page detail sheet — use it to route to a
70
+ * full, URL-addressable detail page (e.g. open in a new tab). Verticals
71
+ * without a dedicated detail page omit it and keep the sheet.
72
+ */
73
+ onOpenDetail?: (hit: CatalogSearchHit) => void;
74
+ /**
75
+ * Per-tab column definitions for the results data table. Each column
76
+ * receives the full `CatalogSearchHit` and projects whatever fields the
77
+ * vertical's indexer document carries.
78
+ */
79
+ columns: ColumnDef<CatalogSearchHit, unknown>[];
80
+ /**
81
+ * Optional facet declarations. When set, the page requests these as
82
+ * `facets` in the search request and renders a chip group per field
83
+ * above the results. Click-to-toggle multi-select; selections are
84
+ * passed back as `filters[]`.
85
+ */
86
+ filterFields?: CatalogFilterField[];
87
+ /**
88
+ * Optional empty-state ReactNode — shown when the tab has no hits for
89
+ * the current query. Defaults to a simple "no results" message.
90
+ */
91
+ emptyState?: ReactNode;
92
+ /**
93
+ * Indexer field that carries the entity's primary image URL. Defaults
94
+ * to `thumbnailUrl`. Cruises / charters use `heroImageUrl`.
95
+ */
96
+ imageField?: string;
97
+ /**
98
+ * Optional merchandising-card mapping. When set, the tab gains a grid
99
+ * (card) view alongside the table, and the grid/list toggle appears. The
100
+ * card projects indexed fields only (no extra fetch) — see `CatalogCard`.
101
+ */
102
+ card?: CatalogCardConfig;
103
+ /**
104
+ * Non-relevance sort options to offer for this tab. `relevance` is always
105
+ * first. Defaults to `["price-asc","price-desc","newest"]` when the card
106
+ * declares a price field, else `["newest"]`.
107
+ */
108
+ sorts?: CatalogSortOption[];
109
+ /**
110
+ * Optional per-field formatters used by the detail sheet to render
111
+ * human-readable values (e.g. resolve `lineSupplierId` → supplier name).
112
+ * The same `formatValue` you pass on a `CatalogFacetFilterField` should
113
+ * usually appear here too.
114
+ */
115
+ detailFormatters?: Record<string, (value: unknown) => ReactNode>;
116
+ /**
117
+ * Optional footer actions shown at the bottom of the detail sheet
118
+ * (e.g. "Open in editor" → router push). Use them when you want a row
119
+ * click to do more than just "view details."
120
+ */
121
+ detailActions?: CatalogDetailAction[];
122
+ /** Optional sheet width override for this tab's detail surface. */
123
+ detailSheetWidth?: CatalogDetailSheetProps["width"];
124
+ /** Optional header action area for this tab's detail sheet. */
125
+ detailHeaderExtras?: CatalogDetailSheetProps["headerExtras"];
126
+ /** Optional dedicated brochure section for this tab's detail sheet. */
127
+ renderDetailBrochure?: CatalogDetailRenderSlot;
128
+ /** Optional media renderer for this tab's detail sheet. */
129
+ renderDetailMedia?: CatalogDetailRenderSlot;
130
+ /** Optional richer itinerary day renderer for this tab's detail sheet. */
131
+ renderDetailItineraryDay?: CatalogDetailSheetProps["renderItineraryDay"];
132
+ /** Optional extra sections rendered above this tab's detail footer actions. */
133
+ renderDetailExtraSections?: CatalogDetailRenderSlot;
134
+ /**
135
+ * Optional enrichment loader. Called when the detail sheet opens for
136
+ * a hit. Returns the rich content shape (description, itinerary,
137
+ * media, options, policies, supplier) so the sheet can render full
138
+ * detail without expanding the search-time projection — keeps the
139
+ * search index lean and lets every entity carry rich content via
140
+ * the catalog content service.
141
+ *
142
+ * Templates wire this per-vertical to call `/v1/admin/<vertical>/:id/content`
143
+ * (which `getProductContent` / `getCruiseContent` / etc back).
144
+ * Returns null when the entity has no content (rare — surfaces as a
145
+ * subtle "no extra detail" hint in the sheet).
146
+ */
147
+ onLoadDetail?: (hit: CatalogSearchHit) => Promise<CatalogDetailEnrichment | null>;
148
+ /** Lazy per-cabin pricing loader for cruise departures (see detail sheet). */
149
+ onLoadDeparturePricing?: CatalogDetailSheetProps["onLoadDeparturePricing"];
150
+ /**
151
+ * Called when the operator clicks a per-departure Book button on a
152
+ * catalog row. Templates typically navigate to the catalog booking
153
+ * journey with the departure id pinned.
154
+ */
155
+ onBookDeparture?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number]) => void;
156
+ /** Per-option Book button inside the expanded departure panel. */
157
+ onBookOption?: (hit: CatalogSearchHit, departure: NonNullable<CatalogDetailEnrichment["departures"]>[number], option: NonNullable<CatalogDetailEnrichment["options"]>[number]) => void;
158
+ }
159
+ export interface CatalogSearchPageProps {
160
+ tabs: CatalogSearchTab[];
161
+ /** Default tab id; falls back to the first tab. */
162
+ defaultTab?: string;
163
+ /** Items per page in the list view (grid view uses a smaller fixed size). Default `40`. */
164
+ pageSize?: number;
165
+ /**
166
+ * Optional title above the search bar. Templates that use TanStack
167
+ * Start's page-level title elements should pass null and render their
168
+ * own.
169
+ */
170
+ title?: ReactNode;
171
+ /** Placeholder text for the search input. */
172
+ searchPlaceholder?: string;
173
+ /**
174
+ * Hide the built-in search input. Use when an embedding surface provides its
175
+ * own unified search box and drives `query`/`onQueryChange` externally.
176
+ */
177
+ hideSearchInput?: boolean;
178
+ /** Debounce on keystrokes, milliseconds. Default 200. */
179
+ queryDebounceMs?: number;
180
+ /**
181
+ * Controlled active-tab id. When provided, callers must also pass
182
+ * `onActiveTabChange` and the tab state is owned by the parent (e.g. a
183
+ * router-driven URL state). Omit for uncontrolled internal state.
184
+ */
185
+ activeTab?: string;
186
+ onActiveTabChange?: (tabId: string) => void;
187
+ /** Hide the tab switcher when the parent route already selects one vertical. */
188
+ showTabs?: boolean;
189
+ /** Controlled query string (already debounced if you want to skip the debounce here). */
190
+ query?: string;
191
+ onQueryChange?: (q: string) => void;
192
+ /** Controlled current page (1-indexed) for the active tab. */
193
+ page?: number;
194
+ onPageChange?: (page: number) => void;
195
+ /** Controlled grid/list view mode (e.g. URL-persisted). Uncontrolled when omitted. */
196
+ view?: "grid" | "list";
197
+ onViewChange?: (view: "grid" | "list") => void;
198
+ /** Controlled sort option (e.g. URL-persisted). Uncontrolled when omitted. */
199
+ sort?: CatalogSortOption;
200
+ onSortChange?: (sort: CatalogSortOption) => void;
201
+ /** Controlled filter selections (e.g. URL-persisted). Uncontrolled when omitted. */
202
+ filters?: CatalogFilterSelections;
203
+ onFiltersChange?: (filters: CatalogFilterSelections) => void;
204
+ /** Catalog slice market override. */
205
+ market?: string;
206
+ /** Catalog slice locale override. */
207
+ locale?: string;
208
+ /** Optional controls rendered next to the search input. */
209
+ toolbarEnd?: ReactNode;
210
+ /** Optional default detail sheet width for all tabs. Tabs may override it. */
211
+ detailSheetWidth?: CatalogDetailSheetProps["width"];
212
+ /** Optional default header action area for all tab detail sheets. */
213
+ detailHeaderExtras?: CatalogDetailSheetProps["headerExtras"];
214
+ /** Optional default brochure section for all tab detail sheets. */
215
+ renderDetailBrochure?: CatalogDetailRenderSlot;
216
+ /** Optional default media renderer for all tab detail sheets. */
217
+ renderDetailMedia?: CatalogDetailRenderSlot;
218
+ /** Optional default richer itinerary day renderer for all tab detail sheets. */
219
+ renderDetailItineraryDay?: CatalogDetailSheetProps["renderItineraryDay"];
220
+ /** Optional default extra sections rendered above detail footer actions. */
221
+ renderDetailExtraSections?: CatalogDetailRenderSlot;
222
+ /** Optional supplier-link renderer for the detail sheet's Attributes tab. */
223
+ renderSupplierLink?: CatalogDetailSheetProps["renderSupplierLink"];
224
+ /**
225
+ * Optional inline tags editor handler for the detail sheet. When set,
226
+ * the sheet's Tags row becomes editable; the callback persists the
227
+ * next tag list (typically a PATCH to the entity).
228
+ */
229
+ onTagsChange?: CatalogDetailSheetProps["onTagsChange"];
230
+ }
231
+ /**
232
+ * Generic tabbed search shell. Owns the search input, mode toggle,
233
+ * tab state, and per-tab data fetching (search + facets + pagination).
234
+ * Per-vertical visuals come from the tab's column definitions so this
235
+ * shell stays vertical-agnostic.
236
+ */
237
+ export declare function CatalogSearchPage({ tabs, defaultTab, pageSize, title, searchPlaceholder, hideSearchInput, queryDebounceMs, activeTab: activeTabProp, onActiveTabChange, showTabs, query: queryProp, onQueryChange, page: pageProp, onPageChange, view, onViewChange, sort, onSortChange, filters, onFiltersChange, market, locale, toolbarEnd, detailSheetWidth, detailHeaderExtras, renderDetailBrochure, renderDetailMedia, renderDetailItineraryDay, renderDetailExtraSections, renderSupplierLink, onTagsChange, }: CatalogSearchPageProps): import("react/jsx-runtime").JSX.Element;
238
+ export type { CatalogFilterSelections } from "./catalog-search-tab-panel.js";
239
+ //# sourceMappingURL=catalog-search-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-search-page.d.ts","sourceRoot":"","sources":["../../src/components/catalog-search-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAItD,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,OAAO,CAAA;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,KAAK,uBAAuB,EAAmB,MAAM,+BAA+B,CAAA;AAE7F;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,WAAW,GACX,YAAY,GACZ,eAAe,GACf,QAAQ,CAAA;AAEZ;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,uBAAuB,GAAG,uBAAuB,CAAA;AAElF,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM,CAAA;IAChD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,WAAW,CAAA;CAClD;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kEAAkE;IAClE,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAA;IACV,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC9C;;;;OAIG;IACH,OAAO,EAAE,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,kBAAkB,EAAE,CAAA;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC3B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,CAAC,CAAA;IAChE;;;;OAIG;IACH,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAA;IACrC,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACnD,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,uBAAuB,CAAC,cAAc,CAAC,CAAA;IAC5D,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,uBAAuB,CAAA;IAC9C,2DAA2D;IAC3D,iBAAiB,CAAC,EAAE,uBAAuB,CAAA;IAC3C,0EAA0E;IAC1E,wBAAwB,CAAC,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAA;IACxE,+EAA+E;IAC/E,yBAAyB,CAAC,EAAE,uBAAuB,CAAA;IACnD;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IACjF,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,uBAAuB,CAAC,wBAAwB,CAAC,CAAA;IAC1E;;;;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,kEAAkE;IAClE,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;CACV;AAID,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,gBAAgB,EAAE,CAAA;IACxB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,6CAA6C;IAC7C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,yDAAyD;IACzD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC9C,8EAA8E;IAC9E,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,oFAAoF;IACpF,OAAO,CAAC,EAAE,uBAAuB,CAAA;IACjC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC5D,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACnD,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,uBAAuB,CAAC,cAAc,CAAC,CAAA;IAC5D,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,uBAAuB,CAAA;IAC9C,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,uBAAuB,CAAA;IAC3C,gFAAgF;IAChF,wBAAwB,CAAC,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAA;IACxE,4EAA4E;IAC5E,yBAAyB,CAAC,EAAE,uBAAuB,CAAA;IACnD,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAA;IAClE;;;;OAIG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAC,cAAc,CAAC,CAAA;CACvD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,UAAU,EACV,QAAa,EACb,KAAK,EACL,iBAAiB,EACjB,eAAuB,EACvB,eAAqB,EACrB,SAAS,EAAE,aAAa,EACxB,iBAAiB,EACjB,QAAe,EACf,KAAK,EAAE,SAAS,EAChB,aAAa,EACb,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,eAAe,EACf,MAAM,EACN,MAAM,EACN,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,YAAY,GACb,EAAE,sBAAsB,2CA0HxB;AAED,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA"}
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Input } from "@voyant-travel/ui/components/input";
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyant-travel/ui/components/tabs";
5
+ import { Search } from "lucide-react";
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { CatalogTabPanel } from "./catalog-search-tab-panel.js";
9
+ /**
10
+ * Generic tabbed search shell. Owns the search input, mode toggle,
11
+ * tab state, and per-tab data fetching (search + facets + pagination).
12
+ * Per-vertical visuals come from the tab's column definitions so this
13
+ * shell stays vertical-agnostic.
14
+ */
15
+ export function CatalogSearchPage({ tabs, defaultTab, pageSize = 40, title, searchPlaceholder, hideSearchInput = false, queryDebounceMs = 200, activeTab: activeTabProp, onActiveTabChange, showTabs = true, query: queryProp, onQueryChange, page: pageProp, onPageChange, view, onViewChange, sort, onSortChange, filters, onFiltersChange, market, locale, toolbarEnd, detailSheetWidth, detailHeaderExtras, renderDetailBrochure, renderDetailMedia, renderDetailItineraryDay, renderDetailExtraSections, renderSupplierLink, onTagsChange, }) {
16
+ const messages = useCatalogUiMessagesOrDefault().catalogPage;
17
+ const resolvedSearchPlaceholder = searchPlaceholder ?? messages.searchPlaceholder;
18
+ const [internalActiveTab, setInternalActiveTab] = useState(defaultTab ?? tabs[0]?.id ?? "");
19
+ const activeTab = activeTabProp ?? internalActiveTab;
20
+ const setActiveTab = (next) => {
21
+ if (onActiveTabChange)
22
+ onActiveTabChange(next);
23
+ if (activeTabProp == null)
24
+ setInternalActiveTab(next);
25
+ };
26
+ // The query input is always driven by the local typing buffer so
27
+ // keystrokes never get clobbered by a re-render triggered by our own
28
+ // debounced URL push. `queryProp` only resets the buffer when it
29
+ // changes from a value we did *not* emit (e.g. browser back/forward,
30
+ // or an external clear).
31
+ const [internalRawQuery, setInternalRawQuery] = useState(queryProp ?? "");
32
+ const [debouncedInternal, setDebouncedInternal] = useState(queryProp ?? "");
33
+ const lastEmittedRef = useRef(queryProp ?? "");
34
+ const rawQuery = internalRawQuery;
35
+ const debouncedQuery = queryProp != null ? queryProp : debouncedInternal;
36
+ useEffect(() => {
37
+ if (queryProp != null && queryProp !== lastEmittedRef.current) {
38
+ // External update (e.g. URL back/forward) — accept it and re-seed
39
+ // the typing buffer.
40
+ lastEmittedRef.current = queryProp;
41
+ setInternalRawQuery(queryProp);
42
+ }
43
+ }, [queryProp]);
44
+ useEffect(() => {
45
+ const t = setTimeout(() => {
46
+ if (internalRawQuery === lastEmittedRef.current)
47
+ return;
48
+ lastEmittedRef.current = internalRawQuery;
49
+ if (onQueryChange)
50
+ onQueryChange(internalRawQuery);
51
+ else
52
+ setDebouncedInternal(internalRawQuery);
53
+ }, queryDebounceMs);
54
+ return () => clearTimeout(t);
55
+ }, [internalRawQuery, queryDebounceMs, onQueryChange]);
56
+ if (tabs.length === 0) {
57
+ return (_jsx("div", { className: "rounded-md border border-dashed p-6 text-sm text-muted-foreground", children: messages.search.noTabsConfigured }));
58
+ }
59
+ const activeTabConfig = tabs.find((tab) => tab.id === activeTab) ?? tabs[0];
60
+ const shouldRenderTabs = showTabs && tabs.length > 1;
61
+ const renderTabBody = (tab) => (_jsxs(_Fragment, { children: [!hideSearchInput && (_jsxs("div", { className: "relative max-w-xl", children: [_jsx(Search, { className: "-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-muted-foreground" }), _jsx(Input, { type: "search", value: rawQuery, onChange: (e) => setInternalRawQuery(e.target.value), placeholder: resolvedSearchPlaceholder, className: "pl-9" })] })), _jsx(CatalogTabPanel, { tab: tab, query: debouncedQuery, market: market, locale: locale, pageSize: pageSize, page: tab.id === activeTab ? pageProp : undefined, onPageChange: tab.id === activeTab ? onPageChange : undefined, view: view, onViewChange: onViewChange, sort: sort, onSortChange: onSortChange, filters: filters, onFiltersChange: onFiltersChange, detailSheetWidth: detailSheetWidth, detailHeaderExtras: detailHeaderExtras, renderDetailBrochure: renderDetailBrochure, renderDetailMedia: renderDetailMedia, renderDetailItineraryDay: renderDetailItineraryDay, renderDetailExtraSections: renderDetailExtraSections, renderSupplierLink: renderSupplierLink, onTagsChange: onTagsChange, messages: messages })] }));
62
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [title, toolbarEnd ? (_jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: toolbarEnd })) : null, shouldRenderTabs ? (_jsxs(Tabs, { value: activeTab, onValueChange: setActiveTab, children: [_jsx(TabsList, { children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, children: tab.label }, tab.id))) }), tabs.map((tab) => (_jsx(TabsContent, { value: tab.id, className: "mt-4 flex flex-col gap-4", children: renderTabBody(tab) }, tab.id)))] })) : (_jsx("div", { className: "flex flex-col gap-4", children: renderTabBody(activeTabConfig) }))] }));
63
+ }
@@ -0,0 +1,42 @@
1
+ import type { useCatalogUiMessagesOrDefault } from "../i18n/index.js";
2
+ import { type CatalogDetailRenderSlot, type CatalogDetailSheetProps } from "./catalog-detail-sheet.js";
3
+ import type { CatalogRangeFilterValue } from "./catalog-range-filter.js";
4
+ import type { CatalogSearchTab, CatalogSortOption } from "./catalog-search-page.js";
5
+ interface CatalogTabPanelProps {
6
+ tab: CatalogSearchTab;
7
+ query: string;
8
+ market?: string;
9
+ locale?: string;
10
+ pageSize: number;
11
+ /** Controlled page (1-indexed). Falls back to internal state when omitted. */
12
+ page?: number;
13
+ onPageChange?: (page: number) => void;
14
+ /** Controlled view mode / sort / filters. Fall back to internal state when omitted. */
15
+ view?: "grid" | "list";
16
+ onViewChange?: (view: "grid" | "list") => void;
17
+ sort?: CatalogSortOption;
18
+ onSortChange?: (sort: CatalogSortOption) => void;
19
+ filters?: CatalogFilterSelections;
20
+ onFiltersChange?: (filters: CatalogFilterSelections) => void;
21
+ detailSheetWidth?: CatalogDetailSheetProps["width"];
22
+ detailHeaderExtras?: CatalogDetailSheetProps["headerExtras"];
23
+ renderDetailBrochure?: CatalogDetailRenderSlot;
24
+ renderDetailMedia?: CatalogDetailRenderSlot;
25
+ renderDetailItineraryDay?: CatalogDetailSheetProps["renderItineraryDay"];
26
+ renderDetailExtraSections?: CatalogDetailRenderSlot;
27
+ renderSupplierLink?: CatalogDetailSheetProps["renderSupplierLink"];
28
+ onTagsChange?: CatalogDetailSheetProps["onTagsChange"];
29
+ messages: ReturnType<typeof useCatalogUiMessagesOrDefault>["catalogPage"];
30
+ }
31
+ /**
32
+ * Public (controlled) filter-selection shape. Both maps are optional so
33
+ * callers persisting to URL state can omit empties; the panel normalizes to
34
+ * `FilterSelections` internally.
35
+ */
36
+ export interface CatalogFilterSelections {
37
+ facets?: Record<string, Array<string | number>>;
38
+ ranges?: Record<string, CatalogRangeFilterValue>;
39
+ }
40
+ export declare function CatalogTabPanel({ tab, query, market, locale, pageSize, page: pageProp, onPageChange, view: viewProp, onViewChange, sort: sortProp, onSortChange, filters: filtersProp, onFiltersChange, detailSheetWidth, detailHeaderExtras, renderDetailBrochure, renderDetailMedia, renderDetailItineraryDay, renderDetailExtraSections, renderSupplierLink, onTagsChange, messages, }: CatalogTabPanelProps): import("react/jsx-runtime").JSX.Element;
41
+ export {};
42
+ //# sourceMappingURL=catalog-search-tab-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-search-tab-panel.d.ts","sourceRoot":"","sources":["../../src/components/catalog-search-tab-panel.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAA;AAGrE,OAAO,EACL,KAAK,uBAAuB,EAE5B,KAAK,uBAAuB,EAC7B,MAAM,2BAA2B,CAAA;AAElC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACxE,OAAO,KAAK,EAEV,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,0BAA0B,CAAA;AAKjC,UAAU,oBAAoB;IAC5B,GAAG,EAAE,gBAAgB,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAA;IAC9C,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,OAAO,CAAC,EAAE,uBAAuB,CAAA;IACjC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC5D,gBAAgB,CAAC,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACnD,kBAAkB,CAAC,EAAE,uBAAuB,CAAC,cAAc,CAAC,CAAA;IAC5D,oBAAoB,CAAC,EAAE,uBAAuB,CAAA;IAC9C,iBAAiB,CAAC,EAAE,uBAAuB,CAAA;IAC3C,wBAAwB,CAAC,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAA;IACxE,yBAAyB,CAAC,EAAE,uBAAuB,CAAA;IACnD,kBAAkB,CAAC,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAA;IAClE,YAAY,CAAC,EAAE,uBAAuB,CAAC,cAAc,CAAC,CAAA;IACtD,QAAQ,EAAE,UAAU,CAAC,OAAO,6BAA6B,CAAC,CAAC,aAAa,CAAC,CAAA;CAC1E;AAcD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAA;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;CACjD;AAED,wBAAgB,eAAe,CAAC,EAC9B,GAAG,EACH,KAAK,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EACR,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,OAAO,EAAE,WAAW,EACpB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,YAAY,EACZ,QAAQ,GACT,EAAE,oBAAoB,2CA+RtB"}
@@ -0,0 +1,199 @@
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 { DataTable } from "@voyant-travel/ui/components/data-table";
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components/select";
6
+ import { ToggleGroup, ToggleGroupItem } from "@voyant-travel/ui/components/toggle-group";
7
+ import { ChevronLeft, ChevronRight, LayoutGrid, List } from "lucide-react";
8
+ import { useEffect, useMemo, useState } from "react";
9
+ import { useCatalogSearch } from "../index.js";
10
+ import { CatalogCard } from "./catalog-card.js";
11
+ import { CatalogDetailSheet, } from "./catalog-detail-sheet.js";
12
+ import { CatalogFilterRail } from "./catalog-filter-rail.js";
13
+ // Grid cards are bigger, so show fewer per page; the dense list view shows more.
14
+ const GRID_PAGE_SIZE = 15;
15
+ const EMPTY_SELECTIONS = { facets: {}, ranges: {} };
16
+ export function CatalogTabPanel({ tab, query, market, locale, pageSize, page: pageProp, onPageChange, view: viewProp, onViewChange, sort: sortProp, onSortChange, filters: filtersProp, onFiltersChange, detailSheetWidth, detailHeaderExtras, renderDetailBrochure, renderDetailMedia, renderDetailItineraryDay, renderDetailExtraSections, renderSupplierLink, onTagsChange, messages, }) {
17
+ // Filter selections can be controlled (URL-persisted) by the page, else
18
+ // internal. `setSelections` keeps the value-or-updater signature so the
19
+ // existing call sites work for both modes.
20
+ const [internalSelections, setInternalSelections] = useState(EMPTY_SELECTIONS);
21
+ const selections = useMemo(() => filtersProp
22
+ ? { facets: filtersProp.facets ?? {}, ranges: filtersProp.ranges ?? {} }
23
+ : internalSelections, [filtersProp, internalSelections]);
24
+ const setSelections = (next) => {
25
+ const resolved = typeof next === "function" ? next(selections) : next;
26
+ onFiltersChange?.(resolved);
27
+ if (filtersProp == null)
28
+ setInternalSelections(resolved);
29
+ };
30
+ const [internalPage, setInternalPage] = useState(1);
31
+ const page = pageProp ?? internalPage;
32
+ const setPage = (next) => {
33
+ if (onPageChange)
34
+ onPageChange(next);
35
+ if (pageProp == null)
36
+ setInternalPage(next);
37
+ };
38
+ const [openHit, setOpenHit] = useState(null);
39
+ // View + sort can be controlled (URL-persisted) by the page, else internal.
40
+ const [internalView, setInternalView] = useState(tab.card ? "grid" : "list");
41
+ const viewMode = viewProp ?? internalView;
42
+ const setViewMode = (next) => {
43
+ onViewChange?.(next);
44
+ if (viewProp == null)
45
+ setInternalView(next);
46
+ };
47
+ // Grid shows fewer (bigger cards), list shows more (dense rows).
48
+ const effectivePageSize = viewMode === "grid" ? GRID_PAGE_SIZE : pageSize;
49
+ const [internalSort, setInternalSort] = useState("relevance");
50
+ const sort = sortProp ?? internalSort;
51
+ const setSort = (next) => {
52
+ onSortChange?.(next);
53
+ if (sortProp == null)
54
+ setInternalSort(next);
55
+ };
56
+ // Reset page when query / filters / sort change. Keeps "Next" honest.
57
+ // biome-ignore lint/correctness/useExhaustiveDependencies: tab.id / query / selections / sort / scope all reset page intentionally
58
+ useEffect(() => {
59
+ setPage(1);
60
+ }, [tab.id, query, selections, sort, market, locale, viewMode]);
61
+ const filters = useMemo(() => buildFilters(selections), [selections]);
62
+ const facetRequests = useMemo(() => (tab.filterFields ?? [])
63
+ .filter((f) => (f.kind ?? "facet") === "facet")
64
+ .map((f) => ({ field: f.field })), [tab.filterFields]);
65
+ // The deployment picks the actual mode at the route — `hybrid` here means
66
+ // "use the best mode this deployment supports." Operators with embeddings
67
+ // configured get vector + keyword fusion; those without silently get pure
68
+ // keyword. The end user shouldn't have to think about it.
69
+ const { data, isLoading, isFetching, isPlaceholderData, error } = useCatalogSearch({
70
+ vertical: tab.vertical,
71
+ query,
72
+ mode: "hybrid",
73
+ market,
74
+ locale,
75
+ filters,
76
+ facets: facetRequests,
77
+ sort,
78
+ pagination: { limit: effectivePageSize, cursor: page > 1 ? String(page) : undefined },
79
+ });
80
+ const total = data?.total ?? 0;
81
+ const totalPages = Math.max(1, Math.ceil(total / effectivePageSize));
82
+ const facetGroups = data?.facets ?? {};
83
+ const hits = data?.hits ?? [];
84
+ const sortOptions = useMemo(() => {
85
+ const opts = ["relevance"];
86
+ if (tab.sorts) {
87
+ opts.push(...tab.sorts);
88
+ }
89
+ else {
90
+ if (tab.card?.priceAmountField)
91
+ opts.push("price-asc", "price-desc");
92
+ opts.push("newest");
93
+ }
94
+ return opts;
95
+ }, [tab.sorts, tab.card]);
96
+ const sortItems = sortOptions.map((value) => ({ value, label: sortLabel(value, messages) }));
97
+ const cardConfig = tab.card;
98
+ if (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ return (_jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive", children: message }));
101
+ }
102
+ const hasFacetSelections = Object.values(selections.facets).some((v) => v.length > 0);
103
+ const hasRangeSelections = Object.values(selections.ranges).some((v) => v.gte != null || v.lte != null);
104
+ const hasSelections = hasFacetSelections || hasRangeSelections;
105
+ const visibleFilterFields = (tab.filterFields ?? []).filter((f) => {
106
+ if ((f.kind ?? "facet") === "range")
107
+ return true;
108
+ return (facetGroups[f.field]?.length ?? 0) > 0 || (selections.facets[f.field]?.length ?? 0) > 0;
109
+ });
110
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-start", children: [visibleFilterFields.length > 0 && (_jsx("aside", { className: "lg:w-60 lg:shrink-0", children: _jsx(CatalogFilterRail, { fields: visibleFilterFields, facetGroups: facetGroups, selectedFacets: selections.facets, selectedRanges: selections.ranges, onToggleFacet: (field, value) => setSelections((prev) => toggleFacet(prev, field, value)), onClearFacet: (field) => setSelections((prev) => clearFacet(prev, field)), onSetRange: (field, next) => setSelections((prev) => setRange(prev, field, next)), onClearAll: () => setSelections(EMPTY_SELECTIONS), hasSelections: hasSelections }) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: isLoading
111
+ ? null
112
+ : `${total} ${total === 1 ? messages.search.resultSingular : messages.search.resultPlural}` }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Select, { items: sortItems, value: sort, onValueChange: (value) => setSort(value), children: [_jsx(SelectTrigger, { className: "h-9 w-[190px] rounded-md data-[size=default]:h-9", "aria-label": messages.view.sort, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: sortItems.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] }), cardConfig && (_jsxs(ToggleGroup, { value: [viewMode], onValueChange: (values) => {
113
+ const next = values[values.length - 1];
114
+ if (next === "grid" || next === "list")
115
+ setViewMode(next);
116
+ }, variant: "outline", "aria-label": messages.view.filters, children: [_jsx(ToggleGroupItem, { value: "grid", "aria-label": messages.view.grid, children: _jsx(LayoutGrid, { className: "size-4" }) }), _jsx(ToggleGroupItem, { value: "list", "aria-label": messages.view.list, children: _jsx(List, { className: "size-4" }) })] }))] })] }), isLoading ? (_jsx(ResultsSkeleton, { grid: viewMode === "grid" && Boolean(cardConfig), count: effectivePageSize })) : (_jsxs(_Fragment, { children: [_jsx("div", { "aria-busy": isFetching, className: `transition-opacity duration-150 ${isFetching && isPlaceholderData ? "pointer-events-none opacity-60" : ""}`, children: hits.length === 0 ? ((tab.emptyState ?? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-center text-muted-foreground text-sm", children: formatTemplate(messages.search.noResults, {
117
+ query: query ? `"${query}"` : messages.search.yourFilters,
118
+ tab: tab.label.toLowerCase(),
119
+ }) })))) : viewMode === "grid" && cardConfig ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: hits.map((hit) => (_jsx(CatalogCard, { hit: hit, config: cardConfig, imageFallbackField: tab.imageField, fallbackTitle: messages.fallbacks.detailName, onOpen: tab.onOpenDetail ?? setOpenHit }, hit.id))) })) : (_jsx(DataTable, { columns: tab.columns, data: hits, getRowId: (row) => row.id, onRowClick: (row) => (tab.onOpenDetail ?? setOpenHit)(row.original), showPagination: false, pageSize: effectivePageSize })) }), totalPages > 1 && (_jsxs("div", { className: "mt-1 flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: formatTemplate(messages.search.showing, {
120
+ from: (page - 1) * effectivePageSize + 1,
121
+ to: Math.min(page * effectivePageSize, total),
122
+ total,
123
+ }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage(Math.max(1, page - 1)), disabled: page <= 1, children: [_jsx(ChevronLeft, { className: "mr-1 h-4 w-4" }), " ", messages.search.previous] }), _jsx("span", { className: "text-muted-foreground text-sm", children: formatTemplate(messages.search.page, { page, totalPages }) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setPage(Math.min(totalPages, page + 1)), disabled: page >= totalPages, children: [messages.search.next, " ", _jsx(ChevronRight, { className: "ml-1 h-4 w-4" })] })] })] }))] }))] })] }), _jsx(CatalogDetailSheet, { hit: openHit, onOpenChange: (open) => {
124
+ if (!open)
125
+ setOpenHit(null);
126
+ }, formatters: tab.detailFormatters, actions: tab.detailActions, imageField: tab.imageField ?? "thumbnailUrl", vertical: tab.vertical, width: tab.detailSheetWidth ?? detailSheetWidth, headerExtras: tab.detailHeaderExtras ?? detailHeaderExtras, onLoadDetail: tab.onLoadDetail, onLoadDeparturePricing: tab.onLoadDeparturePricing, onBookDeparture: tab.onBookDeparture, onBookOption: tab.onBookOption, renderBrochure: tab.renderDetailBrochure ?? renderDetailBrochure, renderMedia: tab.renderDetailMedia ?? renderDetailMedia, renderItineraryDay: tab.renderDetailItineraryDay ?? renderDetailItineraryDay, renderExtraSections: tab.renderDetailExtraSections ?? renderDetailExtraSections, renderSupplierLink: renderSupplierLink, onTagsChange: onTagsChange })] }));
127
+ }
128
+ function buildFilters(selections) {
129
+ const filters = [];
130
+ for (const [field, values] of Object.entries(selections.facets)) {
131
+ if (values.length === 0)
132
+ continue;
133
+ if (values.length === 1) {
134
+ filters.push({ kind: "eq", field, value: values[0] });
135
+ }
136
+ else {
137
+ filters.push({ kind: "in", field, values });
138
+ }
139
+ }
140
+ for (const [field, range] of Object.entries(selections.ranges)) {
141
+ if (range.gte == null && range.lte == null)
142
+ continue;
143
+ filters.push({
144
+ kind: "range",
145
+ field,
146
+ ...(range.gte != null ? { gte: range.gte } : {}),
147
+ ...(range.lte != null ? { lte: range.lte } : {}),
148
+ });
149
+ }
150
+ return filters.length > 0 ? filters : undefined;
151
+ }
152
+ function toggleFacet(prev, field, value) {
153
+ const current = prev.facets[field] ?? [];
154
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
155
+ return { ...prev, facets: { ...prev.facets, [field]: next } };
156
+ }
157
+ function clearFacet(prev, field) {
158
+ const facets = { ...prev.facets };
159
+ delete facets[field];
160
+ return { ...prev, facets };
161
+ }
162
+ function setRange(prev, field, next) {
163
+ const ranges = { ...prev.ranges };
164
+ if (!next || (next.gte == null && next.lte == null)) {
165
+ delete ranges[field];
166
+ }
167
+ else {
168
+ ranges[field] = next;
169
+ }
170
+ return { ...prev, ranges };
171
+ }
172
+ function formatTemplate(template, values) {
173
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
174
+ const value = values[key];
175
+ return value === undefined ? "" : String(value);
176
+ });
177
+ }
178
+ function sortLabel(option, messages) {
179
+ switch (option) {
180
+ case "price-asc":
181
+ return messages.view.sortPriceAsc;
182
+ case "price-desc":
183
+ return messages.view.sortPriceDesc;
184
+ case "departure-asc":
185
+ return messages.view.sortSoonest;
186
+ case "newest":
187
+ return messages.view.sortNewest;
188
+ default:
189
+ return messages.view.sortRelevance;
190
+ }
191
+ }
192
+ function ResultsSkeleton({ grid, count = 12 }) {
193
+ // Match the real layout + roughly the page size so the first load doesn't
194
+ // jump when results arrive. Cap grid cards so the skeleton isn't absurdly tall.
195
+ if (grid) {
196
+ return (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-3", children: Array.from({ length: Math.min(count, 15) }).map((_, i) => (_jsxs("div", { className: "overflow-hidden rounded-lg border", children: [_jsx("div", { className: "aspect-[4/3] w-full animate-pulse bg-muted/40" }), _jsxs("div", { className: "flex flex-col gap-2 p-3", children: [_jsx("div", { className: "h-4 w-3/4 animate-pulse rounded bg-muted/40" }), _jsx("div", { className: "h-3 w-1/2 animate-pulse rounded bg-muted/20" }), _jsx("div", { className: "h-3 w-1/3 animate-pulse rounded bg-muted/20" })] })] }, i))) }));
197
+ }
198
+ return (_jsxs("div", { className: "overflow-hidden rounded-md border", children: [_jsx("div", { className: "h-12 animate-pulse border-b bg-muted/40" }), Array.from({ length: Math.min(count, 40) }).map((_, i) => (_jsx("div", { className: "h-14 animate-pulse border-b bg-muted/20 last:border-b-0" }, i)))] }));
199
+ }