@seekora-ai/ui-sdk-react 0.2.11 → 0.2.13

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 (120) hide show
  1. package/dist/components/InfiniteHits.d.ts +2 -0
  2. package/dist/components/InfiniteHits.d.ts.map +1 -1
  3. package/dist/components/InfiniteHits.js +6 -3
  4. package/dist/components/QuerySuggestions.d.ts +2 -0
  5. package/dist/components/QuerySuggestions.d.ts.map +1 -1
  6. package/dist/components/QuerySuggestions.js +4 -3
  7. package/dist/components/QuerySuggestionsDropdown.d.ts +1 -1
  8. package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
  9. package/dist/components/QuerySuggestionsDropdown.js +4 -4
  10. package/dist/components/Recommendations.d.ts +6 -0
  11. package/dist/components/Recommendations.d.ts.map +1 -1
  12. package/dist/components/Recommendations.js +12 -6
  13. package/dist/components/RichQuerySuggestions.d.ts +4 -0
  14. package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
  15. package/dist/components/RichQuerySuggestions.js +2 -3
  16. package/dist/components/SearchBar.d.ts +2 -0
  17. package/dist/components/SearchBar.d.ts.map +1 -1
  18. package/dist/components/SearchBar.js +5 -9
  19. package/dist/components/SearchResults.d.ts +2 -0
  20. package/dist/components/SearchResults.d.ts.map +1 -1
  21. package/dist/components/SearchResults.js +4 -2
  22. package/dist/components/primitives/ActionButtons.d.ts +27 -0
  23. package/dist/components/primitives/ActionButtons.d.ts.map +1 -0
  24. package/dist/components/primitives/ActionButtons.js +78 -0
  25. package/dist/components/primitives/AnalyticsProvider.d.ts +22 -0
  26. package/dist/components/primitives/AnalyticsProvider.d.ts.map +1 -0
  27. package/dist/components/primitives/AnalyticsProvider.js +87 -0
  28. package/dist/components/primitives/BadgeList.d.ts +14 -0
  29. package/dist/components/primitives/BadgeList.d.ts.map +1 -0
  30. package/dist/components/primitives/BadgeList.js +45 -0
  31. package/dist/components/primitives/ImageDisplay.d.ts +10 -1
  32. package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
  33. package/dist/components/primitives/ImageDisplay.js +49 -9
  34. package/dist/components/primitives/ImageZoom.d.ts +33 -0
  35. package/dist/components/primitives/ImageZoom.d.ts.map +1 -0
  36. package/dist/components/primitives/ImageZoom.js +357 -0
  37. package/dist/components/primitives/PriceDisplay.d.ts +21 -0
  38. package/dist/components/primitives/PriceDisplay.d.ts.map +1 -0
  39. package/dist/components/primitives/PriceDisplay.js +44 -0
  40. package/dist/components/primitives/RatingDisplay.d.ts +43 -0
  41. package/dist/components/primitives/RatingDisplay.d.ts.map +1 -0
  42. package/dist/components/primitives/RatingDisplay.js +114 -0
  43. package/dist/components/primitives/VariantSelector.d.ts +30 -0
  44. package/dist/components/primitives/VariantSelector.d.ts.map +1 -0
  45. package/dist/components/primitives/VariantSelector.js +162 -0
  46. package/dist/components/primitives/VariantSwatches.d.ts +28 -0
  47. package/dist/components/primitives/VariantSwatches.d.ts.map +1 -0
  48. package/dist/components/primitives/VariantSwatches.js +173 -0
  49. package/dist/components/primitives/index.d.ts +9 -0
  50. package/dist/components/primitives/index.d.ts.map +1 -1
  51. package/dist/components/primitives/index.js +9 -0
  52. package/dist/components/primitives/withAnalytics.d.ts +24 -0
  53. package/dist/components/primitives/withAnalytics.d.ts.map +1 -0
  54. package/dist/components/primitives/withAnalytics.js +73 -0
  55. package/dist/components/product-page/ProductInfo.d.ts +25 -2
  56. package/dist/components/product-page/ProductInfo.d.ts.map +1 -1
  57. package/dist/components/product-page/ProductInfo.js +20 -5
  58. package/dist/components/section-primitives/SectionItemGrid.d.ts +3 -1
  59. package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
  60. package/dist/components/section-primitives/SectionItemGrid.js +3 -2
  61. package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
  62. package/dist/components/suggestions/AmazonDropdown.js +2 -2
  63. package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
  64. package/dist/components/suggestions/GoogleDropdown.js +2 -2
  65. package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
  66. package/dist/components/suggestions/MinimalDropdown.js +2 -2
  67. package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
  68. package/dist/components/suggestions/MobileSheetDropdown.js +2 -2
  69. package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
  70. package/dist/components/suggestions/PinterestDropdown.js +2 -2
  71. package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
  72. package/dist/components/suggestions/ShopifyDropdown.js +2 -2
  73. package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
  74. package/dist/components/suggestions/SpotlightDropdown.js +2 -2
  75. package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
  76. package/dist/components/suggestions/SuggestionSearchBar.js +1 -0
  77. package/dist/components/suggestions/types.d.ts +26 -0
  78. package/dist/components/suggestions/types.d.ts.map +1 -1
  79. package/dist/components/suggestions/utils.d.ts +37 -0
  80. package/dist/components/suggestions/utils.d.ts.map +1 -1
  81. package/dist/components/suggestions/utils.js +118 -0
  82. package/dist/components/suggestions-primitives/ItemCard.d.ts +10 -1
  83. package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
  84. package/dist/components/suggestions-primitives/ItemCard.js +20 -6
  85. package/dist/components/suggestions-primitives/ProductCard.d.ts +27 -3
  86. package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
  87. package/dist/components/suggestions-primitives/ProductCard.js +124 -17
  88. package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts +44 -0
  89. package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -0
  90. package/dist/components/suggestions-primitives/ProductCardLayouts.js +105 -0
  91. package/dist/components/suggestions-primitives/ProductGrid.d.ts +6 -1
  92. package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -1
  93. package/dist/components/suggestions-primitives/ProductGrid.js +2 -2
  94. package/dist/components/suggestions-primitives/SuggestionList.d.ts +8 -1
  95. package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
  96. package/dist/components/suggestions-primitives/SuggestionList.js +7 -4
  97. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -1
  98. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +0 -2
  99. package/dist/docsearch/components/Results.d.ts +3 -1
  100. package/dist/docsearch/components/Results.d.ts.map +1 -1
  101. package/dist/docsearch/components/Results.js +6 -2
  102. package/dist/hooks/useProductAnalytics.d.ts +49 -0
  103. package/dist/hooks/useProductAnalytics.d.ts.map +1 -0
  104. package/dist/hooks/useProductAnalytics.js +116 -0
  105. package/dist/hooks/useQuerySuggestionsEnhanced.js +2 -1
  106. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  107. package/dist/hooks/useSuggestionsAnalytics.js +6 -0
  108. package/dist/hooks/useVariantSelection.d.ts +28 -0
  109. package/dist/hooks/useVariantSelection.d.ts.map +1 -0
  110. package/dist/hooks/useVariantSelection.js +44 -0
  111. package/dist/index.d.ts +8 -3
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +5 -1
  114. package/dist/index.umd.js +1 -1
  115. package/dist/src/index.d.ts +1138 -681
  116. package/dist/src/index.esm.js +2407 -723
  117. package/dist/src/index.esm.js.map +1 -1
  118. package/dist/src/index.js +2423 -722
  119. package/dist/src/index.js.map +1 -1
  120. package/package.json +3 -3
package/dist/src/index.js CHANGED
@@ -1513,7 +1513,7 @@ function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else i
1513
1513
  *
1514
1514
  * Interactive search input component with query suggestions support
1515
1515
  */
1516
- const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, renderSuggestion, renderLoading, }) => {
1516
+ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, }) => {
1517
1517
  const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
1518
1518
  const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
1519
1519
  const [isFocused, setIsFocused] = React.useState(false);
@@ -1646,13 +1646,9 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
1646
1646
  const defaultRenderLoading = () => (React.createElement("div", { style: { padding: theme.spacing.medium, textAlign: 'center' } }, "Loading suggestions..."));
1647
1647
  const searchBarTheme = customTheme || {};
1648
1648
  const isLoading = suggestionsLoading || searchLoading;
1649
- // Only show suggestions list if:
1650
- // 1. Input is focused
1651
- // 2. Suggestions are enabled
1652
- // 3. Query is long enough
1653
- // 4. AND (we have suggestions to show OR we're currently loading suggestions)
1649
+ // Show list when we have suggestions (including previous while loading) or when loading and showLoadingState
1654
1650
  const hasSuggestions = displayedSuggestions.length > 0;
1655
- const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || isLoading);
1651
+ const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || (isLoading && showLoadingState));
1656
1652
  // Get processing time from results
1657
1653
  const res = results;
1658
1654
  const processingTime = res?.processingTimeMS
@@ -1703,8 +1699,8 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
1703
1699
  overflowY: 'auto',
1704
1700
  zIndex: 1000,
1705
1701
  } },
1706
- isLoading && (renderLoading ? renderLoading() : defaultRenderLoading()),
1707
- !isLoading && displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
1702
+ isLoading && displayedSuggestions.length === 0 && showLoadingState && (renderLoading ? renderLoading() : defaultRenderLoading()),
1703
+ displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
1708
1704
  const isSelected = index === selectedIndex;
1709
1705
  const renderFn = renderSuggestion || defaultRenderSuggestion;
1710
1706
  return (React.createElement("div", { key: index, className: clsx(searchBarTheme.suggestionItem, isSelected && searchBarTheme.suggestionItemActive), onClick: () => handleSuggestionSelect(suggestion.query), onMouseEnter: () => setSelectedIndex(index), style: {
@@ -1746,7 +1742,7 @@ const formatPrice$1 = (value, currency = '₹') => {
1746
1742
  }
1747
1743
  return String(value);
1748
1744
  };
1749
- const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
1745
+ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
1750
1746
  const { theme, client, enableAnalytics } = useSearchContext();
1751
1747
  const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
1752
1748
  const searchResultsTheme = customTheme || {};
@@ -2113,10 +2109,12 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
2113
2109
  hasError: !!error,
2114
2110
  isLoading: loading,
2115
2111
  });
2116
- if (loading) {
2112
+ // When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
2113
+ if (loading && resultItems.length === 0 && showLoadingState) {
2117
2114
  log.verbose('SearchResults: Rendering loading state');
2118
2115
  return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
2119
2116
  }
2117
+ // When loading with previous results, fall through and render them (no loading screen)
2120
2118
  if (error) {
2121
2119
  log.error('SearchResults: Rendering error state', {
2122
2120
  error: error.message,
@@ -3088,7 +3086,7 @@ const HitsPerPage = ({ items, onHitsPerPageChange, renderSelect, className, styl
3088
3086
  * Displays search results with infinite scroll or "Show More" button
3089
3087
  * Accumulates results as user loads more pages
3090
3088
  */
3091
- const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
3089
+ const InfiniteHits = ({ renderHit, renderEmpty, showInitialLoading = false, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
3092
3090
  const { theme, stateManager } = useSearchContext();
3093
3091
  const { results, loading, currentPage, setPage } = useSearchState();
3094
3092
  const infiniteHitsTheme = customTheme || {};
@@ -3243,10 +3241,13 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
3243
3241
  cursor: isLastPage || isLoadingMore ? 'not-allowed' : 'pointer',
3244
3242
  transition: theme.transitions?.fast || '150ms ease-in-out',
3245
3243
  } }, isLoadingMore ? loadingLabel : isLastPage ? 'No more results' : showMoreLabel));
3246
- // Initial loading state
3247
- if (loading && accumulatedHits.length === 0) {
3244
+ // Initial loading state (only when showInitialLoading: default no loading screen)
3245
+ if (loading && accumulatedHits.length === 0 && showInitialLoading) {
3248
3246
  return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
3249
3247
  }
3248
+ if (loading && accumulatedHits.length === 0) {
3249
+ return React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style });
3250
+ }
3250
3251
  // Empty state
3251
3252
  if (!loading && accumulatedHits.length === 0) {
3252
3253
  return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
@@ -4128,36 +4129,40 @@ const MobileFiltersButton = ({ onClick, text = 'Filters', showCount = true, clas
4128
4129
  * - FrequentlyBoughtTogether: Bundle recommendations
4129
4130
  * - RecentlyViewed: User's recently viewed items
4130
4131
  */
4131
- const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4132
+ const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4132
4133
  const { theme } = useSearchContext();
4133
4134
  const recommendationTheme = customTheme || {};
4134
4135
  // If items are provided, use them directly
4135
4136
  const items = itemsProp?.slice(0, maxItems) || [];
4136
4137
  const loading = loadingProp;
4137
- if (loading) {
4138
+ if (loading && items.length === 0 && showLoadingState) {
4138
4139
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4139
4140
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading related products...")));
4140
4141
  }
4142
+ if (loading && items.length === 0)
4143
+ return null;
4141
4144
  if (items.length === 0) {
4142
4145
  return null;
4143
4146
  }
4144
4147
  return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
4145
4148
  };
4146
- const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4149
+ const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4147
4150
  const { theme } = useSearchContext();
4148
4151
  const recommendationTheme = customTheme || {};
4149
4152
  const items = itemsProp?.slice(0, maxItems) || [];
4150
4153
  const loading = loadingProp;
4151
- if (loading) {
4154
+ if (loading && items.length === 0 && showLoadingState) {
4152
4155
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4153
4156
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading trending items...")));
4154
4157
  }
4158
+ if (loading && items.length === 0)
4159
+ return null;
4155
4160
  if (items.length === 0) {
4156
4161
  return null;
4157
4162
  }
4158
4163
  return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
4159
4164
  };
4160
- const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
4165
+ const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
4161
4166
  const { theme } = useSearchContext();
4162
4167
  const recommendationTheme = customTheme || {};
4163
4168
  const items = itemsProp?.slice(0, maxItems) || [];
@@ -4170,10 +4175,12 @@ const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadin
4170
4175
  return sum + price;
4171
4176
  }, 0);
4172
4177
  }, [items]);
4173
- if (loading) {
4178
+ if (loading && items.length === 0 && showLoadingState) {
4174
4179
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4175
4180
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading recommendations...")));
4176
4181
  }
4182
+ if (loading && items.length === 0)
4183
+ return null;
4177
4184
  if (items.length === 0) {
4178
4185
  return null;
4179
4186
  }
@@ -4392,7 +4399,7 @@ function getLoadingStyle(theme) {
4392
4399
  *
4393
4400
  * Standalone component for displaying query suggestions
4394
4401
  */
4395
- const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
4402
+ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, showLoadingState = false, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
4396
4403
  const { client, theme } = useSearchContext();
4397
4404
  const [selectedIndex, setSelectedIndex] = React.useState(-1);
4398
4405
  const { suggestions, loading, error } = useQuerySuggestions({
@@ -4429,7 +4436,8 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
4429
4436
  if (query.length < minQueryLength) {
4430
4437
  return null;
4431
4438
  }
4432
- if (loading) {
4439
+ // When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
4440
+ if (loading && displayedSuggestions.length === 0 && showLoadingState) {
4433
4441
  return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
4434
4442
  showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
4435
4443
  fontSize: theme.typography.fontSize.large,
@@ -4439,7 +4447,7 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
4439
4447
  } }, title)),
4440
4448
  renderLoading ? renderLoading() : defaultRenderLoading()));
4441
4449
  }
4442
- if (error || displayedSuggestions.length === 0) {
4450
+ if (error || (!loading && displayedSuggestions.length === 0)) {
4443
4451
  return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
4444
4452
  showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
4445
4453
  fontSize: theme.typography.fontSize.large,
@@ -4567,7 +4575,8 @@ function transformFilteredTab(raw) {
4567
4575
  // Main Hook
4568
4576
  // ============================================================================
4569
4577
  function useQuerySuggestionsEnhanced(options) {
4570
- const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets = false, maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
4578
+ const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets = true, // CHANGED: Enable facets by default
4579
+ maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
4571
4580
  // State
4572
4581
  const [suggestions, setSuggestions] = React.useState([]);
4573
4582
  const [loading, setLoading] = React.useState(false);
@@ -5023,7 +5032,7 @@ const LoadingSpinner = ({ style }) => (React.createElement("svg", { style: { ani
5023
5032
  // Component
5024
5033
  // ============================================================================
5025
5034
  const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropdown(props, ref) {
5026
- const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading = true, showEmptyState = true, highlight = { enabled: true, preTag: '<mark>', postTag: '</mark>' }, keyboardNav = { enabled: true }, animation = { enabled: true, duration: 150, entrance: 'fade' }, classNames = {}, style, renderSuggestion, renderRecentSearch, renderLoading, renderEmpty, footer, position = 'absolute', width = '100%', zIndex = 1000, closeOnClickOutside = true, closeOnEscape = true, ariaLabel = 'Search suggestions', onSuggestionSelect, onRecentSearchClick, onRecentSearchRemove, onOpen, onClose, onNavigate, } = props;
5035
+ const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading = false, showEmptyState = true, highlight = { enabled: true, preTag: '<mark>', postTag: '</mark>' }, keyboardNav = { enabled: true }, animation = { enabled: true, duration: 150, entrance: 'fade' }, classNames = {}, style, renderSuggestion, renderRecentSearch, renderLoading, renderEmpty, footer, position = 'absolute', width = '100%', zIndex = 1000, closeOnClickOutside = true, closeOnEscape = true, ariaLabel = 'Search suggestions', onSuggestionSelect, onRecentSearchClick, onRecentSearchRemove, onOpen, onClose, onNavigate, } = props;
5027
5036
  const { client, theme } = useSearchContext();
5028
5037
  const containerRef = React.useRef(null);
5029
5038
  const [activeIndex, setActiveIndex] = React.useState(-1);
@@ -5208,7 +5217,7 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
5208
5217
  loading && showLoading && (React.createElement("div", { className: classNames.loadingState, style: defaultStyles$1.loadingState }, renderLoading ? renderLoading() : (React.createElement(React.Fragment, null,
5209
5218
  React.createElement(LoadingSpinner, null),
5210
5219
  React.createElement("span", null, "Searching..."))))),
5211
- !loading && showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
5220
+ showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
5212
5221
  React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Recent Searches"),
5213
5222
  recentSearches.slice(0, maxRecentSearches).map((search, index) => {
5214
5223
  const isActive = activeIndex === index;
@@ -5216,8 +5225,8 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
5216
5225
  onRecentSearchClick?.(search);
5217
5226
  }, onMouseEnter: () => setActiveIndex(index) }, renderRecentSearchItem(search, index, isActive)));
5218
5227
  }))),
5219
- !loading && showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
5220
- !loading && showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
5228
+ showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
5229
+ showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
5221
5230
  query.length > 0 && (React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Suggestions")),
5222
5231
  suggestions.map((suggestion, index) => {
5223
5232
  const itemIndex = showRecent ? recentSearches.length + index : index;
@@ -5479,7 +5488,7 @@ const CloseIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill
5479
5488
  // Component
5480
5489
  // ============================================================================
5481
5490
  const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(props, ref) {
5482
- const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
5491
+ const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, showLoadingOverlay = false, renderLoading, } = props;
5483
5492
  const { client } = useSearchContext();
5484
5493
  const containerRef = React.useRef(null);
5485
5494
  const [activeIndex, setActiveIndex] = React.useState(-1);
@@ -5698,8 +5707,7 @@ const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(prop
5698
5707
  } },
5699
5708
  header && React.createElement("div", { style: styles$2.header }, header),
5700
5709
  React.createElement("div", { style: { ...styles$2.content, maxHeight } },
5701
- loading && (React.createElement("div", { style: styles$2.loadingOverlay },
5702
- React.createElement("span", null, "Loading..."))),
5710
+ loading && showLoadingOverlay && (React.createElement("div", { style: styles$2.loadingOverlay }, renderLoading ? renderLoading() : React.createElement("span", null, "Loading..."))),
5703
5711
  enabledSections.map((section, index) => {
5704
5712
  let content = null;
5705
5713
  switch (section.id) {
@@ -6263,8 +6271,7 @@ const EVENTS = {
6263
6271
  TRENDING_CLICK: 'suggestions.trending_click',
6264
6272
  SEARCH_SUBMIT: 'suggestions.search_submit',
6265
6273
  DROPDOWN_OPEN: 'suggestions.dropdown_open',
6266
- DROPDOWN_CLOSE: 'suggestions.dropdown_close',
6267
- };
6274
+ DROPDOWN_CLOSE: 'suggestions.dropdown_close'};
6268
6275
  // ============================================================================
6269
6276
  // Hook Implementation
6270
6277
  // ============================================================================
@@ -7185,21 +7192,6 @@ function SuggestionItem({ suggestion, index, isActive, onSelect, className, styl
7185
7192
  suggestion.count != null ? (React.createElement("span", { className: "seekora-suggestions-item-count", style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, suggestion.count)) : null));
7186
7193
  }
7187
7194
 
7188
- /**
7189
- * SuggestionsLoading – loading indicator (primitive)
7190
- */
7191
- function SuggestionsLoading({ className, style, text = 'Loading...' }) {
7192
- const { loading } = useSuggestionsContext();
7193
- if (!loading)
7194
- return null;
7195
- return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
7196
- padding: 16,
7197
- color: 'var(--seekora-text-secondary, #6b7280)',
7198
- fontSize: '0.875rem',
7199
- ...style,
7200
- } }, text));
7201
- }
7202
-
7203
7195
  /**
7204
7196
  * SuggestionList – container for text suggestions (primitive)
7205
7197
  *
@@ -7211,15 +7203,19 @@ const listStyle = {
7211
7203
  margin: 0,
7212
7204
  padding: '4px 0',
7213
7205
  };
7214
- function SuggestionList({ maxItems = 10, className, style, listClassName, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
7206
+ function SuggestionList({ maxItems = 10, className, style, listClassName, showLoadingState = false, renderLoading, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
7215
7207
  const { suggestions, activeIndex, loading, selectSuggestion, getAllNavigableItems, } = useSuggestionsContext();
7216
7208
  const items = suggestions.slice(0, maxItems);
7217
7209
  const navigableItems = getAllNavigableItems();
7218
7210
  const suggestionStartIndex = navigableItems.findIndex((n) => n.type === 'suggestion');
7219
7211
  const activeIsInSuggestions = suggestionStartIndex >= 0 && activeIndex >= suggestionStartIndex && activeIndex < suggestionStartIndex + items.length;
7220
- if (loading) {
7221
- return React.createElement(SuggestionsLoading, { className: className, style: style });
7212
+ // When loading with no previous results, show loading only if showLoadingState (default: don't show loading screen)
7213
+ if (loading && items.length === 0 && showLoadingState) {
7214
+ if (renderLoading)
7215
+ return React.createElement(React.Fragment, null, renderLoading());
7216
+ return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: { padding: 16, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875rem', ...style } }, "Loading..."));
7222
7217
  }
7218
+ // When loading with previous results, show previous results (no loading UI)
7223
7219
  if (items.length === 0)
7224
7220
  return null;
7225
7221
  return (React.createElement("div", { className: clsx('seekora-suggestions-list', className), style: style },
@@ -7234,6 +7230,362 @@ function SuggestionList({ maxItems = 10, className, style, listClassName, enable
7234
7230
  }))));
7235
7231
  }
7236
7232
 
7233
+ /**
7234
+ * ImageZoom – zoom on hover and click (Amazon-style magnifier + lightbox)
7235
+ *
7236
+ * Supports three zoom modes:
7237
+ * - hover: Magnified view in a separate panel on the right (Amazon style)
7238
+ * - lens: Magnifying glass that follows cursor
7239
+ * - click: Full-screen lightbox modal
7240
+ */
7241
+ function ImageZoom({ src, alt = '', mode = 'both', zoomLevel = 2.5, className, style, showZoomIndicator = true, lensSize = 150, zoomPanelSize = { width: 400, height: 400 }, images, currentIndex = 0, }) {
7242
+ const [isHovering, setIsHovering] = React.useState(false);
7243
+ const [isLightboxOpen, setIsLightboxOpen] = React.useState(false);
7244
+ const [lightboxIndex, setLightboxIndex] = React.useState(currentIndex);
7245
+ const [cursorPos, setCursorPos] = React.useState({ x: 0, y: 0 });
7246
+ const [imageLoaded, setImageLoaded] = React.useState(false);
7247
+ const [touchStart, setTouchStart] = React.useState(null);
7248
+ const [touchEnd, setTouchEnd] = React.useState(null);
7249
+ const imageRef = React.useRef(null);
7250
+ const containerRef = React.useRef(null);
7251
+ const allImages = images && images.length > 0 ? images : [src];
7252
+ const hasMultipleImages = allImages.length > 1;
7253
+ // Minimum swipe distance (in px) to trigger navigation
7254
+ const minSwipeDistance = 50;
7255
+ const supportsHover = mode === 'hover' || mode === 'both';
7256
+ const supportsClick = mode === 'click' || mode === 'both';
7257
+ const supportsLens = mode === 'lens';
7258
+ const handleMouseMove = React.useCallback((e) => {
7259
+ if (!containerRef.current)
7260
+ return;
7261
+ const rect = containerRef.current.getBoundingClientRect();
7262
+ const x = e.clientX - rect.left;
7263
+ const y = e.clientY - rect.top;
7264
+ setCursorPos({ x, y });
7265
+ }, []);
7266
+ const handleMouseEnter = React.useCallback(() => {
7267
+ setIsHovering(true);
7268
+ }, []);
7269
+ const handleMouseLeave = React.useCallback(() => {
7270
+ setIsHovering(false);
7271
+ }, []);
7272
+ const handleClick = React.useCallback((e) => {
7273
+ if (supportsClick) {
7274
+ e?.stopPropagation();
7275
+ setLightboxIndex(currentIndex);
7276
+ setIsLightboxOpen(true);
7277
+ }
7278
+ }, [supportsClick, currentIndex]);
7279
+ const closeLightbox = React.useCallback((e) => {
7280
+ e?.stopPropagation();
7281
+ setIsLightboxOpen(false);
7282
+ }, []);
7283
+ const goToNext = React.useCallback(() => {
7284
+ setLightboxIndex((i) => (i + 1) % allImages.length);
7285
+ }, [allImages.length]);
7286
+ const goToPrev = React.useCallback(() => {
7287
+ setLightboxIndex((i) => (i - 1 + allImages.length) % allImages.length);
7288
+ }, [allImages.length]);
7289
+ // Touch event handlers for swipe
7290
+ const handleTouchStart = React.useCallback((e) => {
7291
+ setTouchEnd(null);
7292
+ setTouchStart(e.targetTouches[0].clientX);
7293
+ }, []);
7294
+ const handleTouchMove = React.useCallback((e) => {
7295
+ setTouchEnd(e.targetTouches[0].clientX);
7296
+ }, []);
7297
+ const handleTouchEnd = React.useCallback(() => {
7298
+ if (!touchStart || !touchEnd)
7299
+ return;
7300
+ const distance = touchStart - touchEnd;
7301
+ const isLeftSwipe = distance > minSwipeDistance;
7302
+ const isRightSwipe = distance < -minSwipeDistance;
7303
+ if (isLeftSwipe) {
7304
+ goToNext();
7305
+ }
7306
+ else if (isRightSwipe) {
7307
+ goToPrev();
7308
+ }
7309
+ }, [touchStart, touchEnd, minSwipeDistance, goToNext, goToPrev]);
7310
+ // Close lightbox on Escape key, navigate with arrow keys
7311
+ React.useEffect(() => {
7312
+ if (!isLightboxOpen)
7313
+ return;
7314
+ const handleKeyboard = (e) => {
7315
+ if (e.key === 'Escape')
7316
+ closeLightbox();
7317
+ if (hasMultipleImages) {
7318
+ if (e.key === 'ArrowRight')
7319
+ goToNext();
7320
+ if (e.key === 'ArrowLeft')
7321
+ goToPrev();
7322
+ }
7323
+ };
7324
+ window.addEventListener('keydown', handleKeyboard);
7325
+ return () => window.removeEventListener('keydown', handleKeyboard);
7326
+ }, [isLightboxOpen, closeLightbox, hasMultipleImages, goToNext, goToPrev]);
7327
+ const containerStyle = {
7328
+ position: 'relative',
7329
+ display: 'inline-block',
7330
+ cursor: supportsClick ? 'zoom-in' : supportsHover || supportsLens ? 'crosshair' : 'default',
7331
+ overflow: 'hidden',
7332
+ ...style,
7333
+ };
7334
+ const imageStyle = {
7335
+ width: '100%',
7336
+ height: '100%',
7337
+ objectFit: 'cover',
7338
+ display: 'block',
7339
+ };
7340
+ // Calculate zoom panel background position (Amazon-style)
7341
+ const getZoomPanelStyle = () => {
7342
+ if (!containerRef.current || !imageLoaded)
7343
+ return { display: 'none' };
7344
+ const rect = containerRef.current.getBoundingClientRect();
7345
+ const bgPosX = (cursorPos.x / rect.width) * 100;
7346
+ const bgPosY = (cursorPos.y / rect.height) * 100;
7347
+ return {
7348
+ position: 'fixed', // Changed from absolute to fixed for better positioning
7349
+ top: rect.top,
7350
+ left: rect.right + 16, // 16px gap from the image
7351
+ width: zoomPanelSize.width,
7352
+ height: zoomPanelSize.height,
7353
+ backgroundImage: `url(${src})`,
7354
+ backgroundSize: `${zoomLevel * 100}%`,
7355
+ backgroundPosition: `${bgPosX}% ${bgPosY}%`,
7356
+ backgroundRepeat: 'no-repeat',
7357
+ border: '2px solid var(--seekora-border-color, #e5e7eb)',
7358
+ borderRadius: 8,
7359
+ boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
7360
+ backgroundColor: '#fff',
7361
+ pointerEvents: 'none',
7362
+ zIndex: 9998,
7363
+ };
7364
+ };
7365
+ // Calculate lens position and zoom
7366
+ const getLensStyle = () => {
7367
+ if (!containerRef.current)
7368
+ return { display: 'none' };
7369
+ return {
7370
+ position: 'absolute',
7371
+ width: lensSize,
7372
+ height: lensSize,
7373
+ left: cursorPos.x - lensSize / 2,
7374
+ top: cursorPos.y - lensSize / 2,
7375
+ border: '2px solid rgba(255,255,255,0.8)',
7376
+ borderRadius: '50%',
7377
+ boxShadow: '0 0 0 1px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(0,0,0,0.3)',
7378
+ pointerEvents: 'none',
7379
+ overflow: 'hidden',
7380
+ zIndex: 100,
7381
+ };
7382
+ };
7383
+ const getLensImageStyle = () => {
7384
+ if (!containerRef.current || !imageRef.current)
7385
+ return {};
7386
+ const rect = containerRef.current.getBoundingClientRect();
7387
+ imageRef.current.getBoundingClientRect();
7388
+ return {
7389
+ position: 'absolute',
7390
+ width: rect.width * zoomLevel,
7391
+ height: rect.height * zoomLevel,
7392
+ left: -(cursorPos.x * zoomLevel - lensSize / 2),
7393
+ top: -(cursorPos.y * zoomLevel - lensSize / 2),
7394
+ objectFit: 'cover',
7395
+ };
7396
+ };
7397
+ return (React.createElement(React.Fragment, null,
7398
+ React.createElement("div", { ref: containerRef, className: clsx('seekora-image-zoom', className), style: containerStyle, onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: handleClick },
7399
+ React.createElement("img", { ref: imageRef, src: src, alt: alt, style: imageStyle, onLoad: () => setImageLoaded(true) }),
7400
+ showZoomIndicator && supportsClick && (React.createElement("div", { style: {
7401
+ position: 'absolute',
7402
+ top: 8,
7403
+ right: 8,
7404
+ width: 32,
7405
+ height: 32,
7406
+ borderRadius: '50%',
7407
+ backgroundColor: 'rgba(0,0,0,0.6)',
7408
+ color: '#fff',
7409
+ display: 'flex',
7410
+ alignItems: 'center',
7411
+ justifyContent: 'center',
7412
+ fontSize: '1.25rem',
7413
+ pointerEvents: 'none',
7414
+ opacity: isHovering ? 1 : 0.7,
7415
+ transition: 'opacity 150ms',
7416
+ } }, "\uD83D\uDD0D")),
7417
+ supportsLens && isHovering && imageLoaded && (React.createElement("div", { style: getLensStyle() },
7418
+ React.createElement("img", { src: src, alt: "", style: getLensImageStyle() }))),
7419
+ supportsHover && isHovering && imageLoaded && containerRef.current && (React.createElement("div", { style: {
7420
+ position: 'absolute',
7421
+ left: cursorPos.x - 75,
7422
+ top: cursorPos.y - 75,
7423
+ width: 150,
7424
+ height: 150,
7425
+ border: '2px solid rgba(0,0,0,0.3)',
7426
+ backgroundColor: 'rgba(255,255,255,0.1)',
7427
+ pointerEvents: 'none',
7428
+ zIndex: 50,
7429
+ } })),
7430
+ supportsHover && isHovering && imageLoaded && (React.createElement("div", { style: getZoomPanelStyle() }))),
7431
+ isLightboxOpen && (React.createElement("div", { className: "seekora-image-zoom-lightbox", style: {
7432
+ position: 'fixed',
7433
+ top: 0,
7434
+ left: 0,
7435
+ right: 0,
7436
+ bottom: 0,
7437
+ backgroundColor: 'rgba(0,0,0,0.95)',
7438
+ zIndex: 9999,
7439
+ display: 'flex',
7440
+ alignItems: 'center',
7441
+ justifyContent: 'center',
7442
+ cursor: 'zoom-out',
7443
+ padding: 20,
7444
+ }, onClick: closeLightbox, onTouchStart: hasMultipleImages ? handleTouchStart : undefined, onTouchMove: hasMultipleImages ? handleTouchMove : undefined, onTouchEnd: hasMultipleImages ? handleTouchEnd : undefined },
7445
+ React.createElement("button", { type: "button", "aria-label": "Close zoom", style: {
7446
+ position: 'absolute',
7447
+ top: 20,
7448
+ right: 20,
7449
+ width: 44,
7450
+ height: 44,
7451
+ borderRadius: '50%',
7452
+ border: 'none',
7453
+ backgroundColor: 'rgba(255,255,255,0.2)',
7454
+ color: '#fff',
7455
+ fontSize: '1.5rem',
7456
+ cursor: 'pointer',
7457
+ display: 'flex',
7458
+ alignItems: 'center',
7459
+ justifyContent: 'center',
7460
+ transition: 'background-color 150ms',
7461
+ zIndex: 10001,
7462
+ }, onClick: (e) => {
7463
+ e.stopPropagation();
7464
+ closeLightbox();
7465
+ }, onMouseEnter: (e) => {
7466
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7467
+ }, onMouseLeave: (e) => {
7468
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7469
+ } }, "\u2715"),
7470
+ hasMultipleImages && (React.createElement(React.Fragment, null,
7471
+ React.createElement("button", { type: "button", "aria-label": "Previous image", style: {
7472
+ position: 'absolute',
7473
+ left: 20,
7474
+ top: '50%',
7475
+ transform: 'translateY(-50%)',
7476
+ width: 56,
7477
+ height: 56,
7478
+ borderRadius: '50%',
7479
+ border: 'none',
7480
+ backgroundColor: 'rgba(255,255,255,0.2)',
7481
+ color: '#fff',
7482
+ fontSize: '2rem',
7483
+ fontWeight: 'bold',
7484
+ cursor: 'pointer',
7485
+ display: 'flex',
7486
+ alignItems: 'center',
7487
+ justifyContent: 'center',
7488
+ transition: 'background-color 150ms',
7489
+ zIndex: 10001,
7490
+ }, onClick: (e) => {
7491
+ e.stopPropagation();
7492
+ goToPrev();
7493
+ }, onMouseEnter: (e) => {
7494
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7495
+ }, onMouseLeave: (e) => {
7496
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7497
+ } }, "\u2039"),
7498
+ React.createElement("button", { type: "button", "aria-label": "Next image", style: {
7499
+ position: 'absolute',
7500
+ right: 20,
7501
+ top: '50%',
7502
+ transform: 'translateY(-50%)',
7503
+ width: 56,
7504
+ height: 56,
7505
+ borderRadius: '50%',
7506
+ border: 'none',
7507
+ backgroundColor: 'rgba(255,255,255,0.2)',
7508
+ color: '#fff',
7509
+ fontSize: '2rem',
7510
+ fontWeight: 'bold',
7511
+ cursor: 'pointer',
7512
+ display: 'flex',
7513
+ alignItems: 'center',
7514
+ justifyContent: 'center',
7515
+ transition: 'background-color 150ms',
7516
+ zIndex: 10001,
7517
+ }, onClick: (e) => {
7518
+ e.stopPropagation();
7519
+ goToNext();
7520
+ }, onMouseEnter: (e) => {
7521
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7522
+ }, onMouseLeave: (e) => {
7523
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7524
+ } }, "\u203A"))),
7525
+ React.createElement("img", { src: allImages[lightboxIndex], alt: alt, style: {
7526
+ maxWidth: '90%',
7527
+ maxHeight: '90%',
7528
+ objectFit: 'contain',
7529
+ borderRadius: 4,
7530
+ cursor: 'default',
7531
+ }, onClick: (e) => e.stopPropagation() }),
7532
+ hasMultipleImages && (React.createElement("div", { style: {
7533
+ position: 'absolute',
7534
+ bottom: 20,
7535
+ left: '50%',
7536
+ transform: 'translateX(-50%)',
7537
+ display: 'flex',
7538
+ flexDirection: 'column',
7539
+ alignItems: 'center',
7540
+ gap: 12,
7541
+ }, onClick: (e) => e.stopPropagation() },
7542
+ React.createElement("div", { style: { display: 'flex', gap: 8, overflowX: 'auto', maxWidth: '80vw', padding: '8px 0' } }, allImages.map((img, i) => (React.createElement("button", { key: i, type: "button", onClick: (e) => {
7543
+ e.stopPropagation();
7544
+ setLightboxIndex(i);
7545
+ }, style: {
7546
+ width: 60,
7547
+ height: 60,
7548
+ padding: 0,
7549
+ border: i === lightboxIndex ? '3px solid #fff' : '2px solid rgba(255,255,255,0.3)',
7550
+ borderRadius: 4,
7551
+ overflow: 'hidden',
7552
+ cursor: 'pointer',
7553
+ opacity: i === lightboxIndex ? 1 : 0.6,
7554
+ transition: 'all 150ms ease',
7555
+ flexShrink: 0,
7556
+ background: 'none',
7557
+ }, onMouseEnter: (e) => {
7558
+ e.currentTarget.style.opacity = '1';
7559
+ }, onMouseLeave: (e) => {
7560
+ if (i !== lightboxIndex)
7561
+ e.currentTarget.style.opacity = '0.6';
7562
+ } },
7563
+ React.createElement("img", { src: img, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } }))))),
7564
+ React.createElement("div", { style: {
7565
+ color: 'rgba(255,255,255,0.9)',
7566
+ fontSize: '0.875rem',
7567
+ textAlign: 'center',
7568
+ backgroundColor: 'rgba(0,0,0,0.5)',
7569
+ padding: '4px 12px',
7570
+ borderRadius: 12,
7571
+ } },
7572
+ lightboxIndex + 1,
7573
+ " / ",
7574
+ allImages.length))),
7575
+ React.createElement("div", { style: {
7576
+ position: 'absolute',
7577
+ top: 20,
7578
+ left: '50%',
7579
+ transform: 'translateX(-50%)',
7580
+ color: 'rgba(255,255,255,0.7)',
7581
+ fontSize: '0.875rem',
7582
+ textAlign: 'center',
7583
+ backgroundColor: 'rgba(0,0,0,0.5)',
7584
+ padding: '8px 16px',
7585
+ borderRadius: 12,
7586
+ } }, hasMultipleImages ? 'Use arrow keys or click thumbnails to navigate • ESC to close' : 'Click outside or press ESC to close')))));
7587
+ }
7588
+
7237
7589
  /**
7238
7590
  * ImageDisplay – configurable multi-image display (primitive)
7239
7591
  *
@@ -7247,7 +7599,7 @@ const imgBaseStyle = {
7247
7599
  borderRadius: 4,
7248
7600
  backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7249
7601
  };
7250
- function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
7602
+ function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, enableZoom = false, zoomMode = 'both', zoomLevel = 2.5, showDots = true, }) {
7251
7603
  const [index, setIndex] = React.useState(0);
7252
7604
  const [hovering, setHovering] = React.useState(false);
7253
7605
  const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
@@ -7256,13 +7608,21 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
7256
7608
  return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
7257
7609
  }
7258
7610
  if (variant === 'single') {
7611
+ if (enableZoom) {
7612
+ return (React.createElement(ImageZoom, { src: safeImages[0], alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: 0, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style } }));
7613
+ }
7259
7614
  return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
7260
7615
  }
7261
7616
  if (variant === 'hover') {
7262
7617
  const showSecond = safeImages.length > 1 && hovering;
7263
7618
  const src = showSecond ? safeImages[1] : safeImages[0];
7619
+ const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7620
+ if (enableZoom) {
7621
+ return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
7622
+ React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
7623
+ }
7264
7624
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
7265
- React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: imgBaseStyle, loading: "lazy" })));
7625
+ React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: hoverImgStyle, loading: "lazy" })));
7266
7626
  }
7267
7627
  if (variant === 'carousel') {
7268
7628
  const go = (delta) => {
@@ -7275,16 +7635,41 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
7275
7635
  return next;
7276
7636
  });
7277
7637
  };
7638
+ const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7639
+ const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-carousel-main", style: carouselImgStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: carouselImgStyle, loading: "lazy" }));
7278
7640
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
7279
- React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: imgBaseStyle, loading: "lazy" }),
7641
+ mainImage,
7280
7642
  safeImages.length > 1 && (React.createElement(React.Fragment, null,
7281
- React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
7282
- React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
7643
+ React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(-1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u2039"),
7644
+ React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u203A"),
7645
+ showDots && (React.createElement("div", { className: "seekora-img-carousel-dots", style: {
7646
+ position: 'absolute',
7647
+ bottom: 8,
7648
+ left: '50%',
7649
+ transform: 'translateX(-50%)',
7650
+ display: 'flex',
7651
+ gap: 6,
7652
+ padding: '6px 12px',
7653
+ backgroundColor: 'rgba(0,0,0,0.5)',
7654
+ borderRadius: 12,
7655
+ zIndex: 10,
7656
+ } }, safeImages.map((_, i) => (React.createElement("button", { key: i, type: "button", "aria-label": `Go to image ${i + 1}`, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); }, onClick: (e) => { e.stopPropagation(); setIndex(i); }, style: {
7657
+ width: 8,
7658
+ height: 8,
7659
+ borderRadius: '50%',
7660
+ border: 'none',
7661
+ padding: 0,
7662
+ backgroundColor: i === index ? '#fff' : 'rgba(255,255,255,0.5)',
7663
+ cursor: 'pointer',
7664
+ transition: 'all 150ms ease',
7665
+ } })))))))));
7283
7666
  }
7284
7667
  if (variant === 'thumbStrip' || variant === 'thumbList') {
7668
+ const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7669
+ const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-thumb-main", style: thumbMainStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: thumbMainStyle, loading: "lazy" }));
7285
7670
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
7286
- React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: imgBaseStyle, loading: "lazy" }),
7287
- React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: () => setIndex(i) },
7671
+ mainImage,
7672
+ React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); setIndex(i); }, onClick: (e) => e.stopPropagation() },
7288
7673
  React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
7289
7674
  }
7290
7675
  return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
@@ -7298,13 +7683,96 @@ function arrowStyle(left) {
7298
7683
  width: 32,
7299
7684
  height: 32,
7300
7685
  borderRadius: '50%',
7301
- border: '1px solid var(--seekora-border-color)',
7302
- backgroundColor: 'var(--seekora-bg-surface)',
7686
+ border: 'none',
7687
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
7688
+ color: '#111',
7689
+ fontSize: '1.25rem',
7690
+ fontWeight: 'bold',
7303
7691
  cursor: 'pointer',
7304
7692
  display: 'flex',
7305
7693
  alignItems: 'center',
7306
7694
  justifyContent: 'center',
7695
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
7696
+ zIndex: 10,
7697
+ transition: 'all 150ms ease',
7698
+ };
7699
+ }
7700
+
7701
+ /**
7702
+ * ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
7703
+ *
7704
+ * Renders a set of action buttons for product cards. Can be positioned absolutely
7705
+ * over the image (on hover) or inline below the card content.
7706
+ */
7707
+ const DEFAULT_ICONS = {
7708
+ addToCart: '🛒',
7709
+ wishlist: '♡',
7710
+ buyNow: '⚡',
7711
+ quickView: '👁',
7712
+ compare: '⚖',
7713
+ };
7714
+ const DEFAULT_LABELS = {
7715
+ addToCart: 'Add to Cart',
7716
+ wishlist: 'Wishlist',
7717
+ buyNow: 'Buy Now',
7718
+ quickView: 'Quick View',
7719
+ compare: 'Compare',
7720
+ };
7721
+ const BUTTON_SIZES = {
7722
+ small: { width: 28, height: 28, fontSize: '0.75rem', iconSize: '1rem' },
7723
+ medium: { width: 36, height: 36, fontSize: '0.875rem', iconSize: '1.25rem' },
7724
+ large: { width: 44, height: 44, fontSize: '1rem', iconSize: '1.5rem' },
7725
+ };
7726
+ function ActionButtons({ buttons, layout = 'horizontal', position = 'inline', showLabels = false, size = 'medium', className, style, }) {
7727
+ const sizeConfig = BUTTON_SIZES[size];
7728
+ const isOverlay = position !== 'inline';
7729
+ const containerStyle = {
7730
+ display: 'flex',
7731
+ flexDirection: layout === 'vertical' ? 'column' : 'row',
7732
+ gap: 6,
7733
+ ...(isOverlay ? {
7734
+ position: 'absolute',
7735
+ ...(position === 'top-right' ? { top: 8, right: 8 } : {}),
7736
+ ...(position === 'bottom-center' ? { bottom: 8, left: '50%', transform: 'translateX(-50%)' } : {}),
7737
+ } : {}),
7738
+ ...style,
7739
+ };
7740
+ const buttonBaseStyle = {
7741
+ display: 'flex',
7742
+ alignItems: 'center',
7743
+ justifyContent: 'center',
7744
+ gap: 4,
7745
+ padding: showLabels ? '0 12px' : 0,
7746
+ width: showLabels ? 'auto' : sizeConfig.width,
7747
+ height: sizeConfig.height,
7748
+ fontSize: sizeConfig.fontSize,
7749
+ fontWeight: 500,
7750
+ border: 'none',
7751
+ borderRadius: 6,
7752
+ backgroundColor: 'var(--seekora-bg-surface, #fff)',
7753
+ color: 'var(--seekora-text, #111827)',
7754
+ cursor: 'pointer',
7755
+ transition: 'all 150ms ease',
7756
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
7757
+ };
7758
+ const handleClick = (btn, e) => {
7759
+ e.stopPropagation();
7760
+ e.preventDefault();
7761
+ if (btn.onClick && !btn.disabled && !btn.loading) {
7762
+ btn.onClick(e);
7763
+ }
7307
7764
  };
7765
+ return (React.createElement("div", { className: clsx('seekora-action-buttons', `seekora-action-buttons--${layout}`, className), style: containerStyle }, buttons.map((btn, i) => {
7766
+ const icon = btn.icon ?? DEFAULT_ICONS[btn.type];
7767
+ const label = btn.label ?? DEFAULT_LABELS[btn.type];
7768
+ return (React.createElement("button", { key: i, type: "button", className: clsx('seekora-action-button', `seekora-action-button--${btn.type}`, btn.disabled && 'seekora-action-button--disabled', btn.loading && 'seekora-action-button--loading'), style: {
7769
+ ...buttonBaseStyle,
7770
+ opacity: btn.disabled ? 0.5 : 1,
7771
+ cursor: btn.disabled ? 'not-allowed' : 'pointer',
7772
+ }, onClick: (e) => handleClick(btn, e), disabled: btn.disabled, "aria-label": label, title: label }, btn.loading ? (React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, "\u23F3")) : (React.createElement(React.Fragment, null,
7773
+ React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, icon),
7774
+ showLabels && React.createElement("span", null, label)))));
7775
+ })));
7308
7776
  }
7309
7777
 
7310
7778
  /**
@@ -7335,18 +7803,31 @@ const imgStyle$1 = {
7335
7803
  borderRadius: 4,
7336
7804
  backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7337
7805
  };
7338
- function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', }) {
7806
+ function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', layout = 'vertical', actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, }) {
7339
7807
  const images = item.images?.length ? item.images : item.image ?? item.imageUrl ? [String(item.image ?? item.imageUrl)] : [];
7340
7808
  const title = item.title ?? item.primaryText ?? '';
7341
7809
  const description = item.description ?? item.secondaryText;
7342
7810
  const href = item.url;
7343
- const content = (React.createElement(React.Fragment, null,
7344
- images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" })) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: imgStyle$1, "aria-hidden": true })),
7811
+ const isHorizontal = layout === 'horizontal';
7812
+ const imageBlock = images.length > 0 ? (React.createElement("div", { style: { position: 'relative', ...(isHorizontal ? { width: 80, flexShrink: 0 } : {}) } },
7813
+ React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" }),
7814
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" })))) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: { ...imgStyle$1, ...(isHorizontal ? { width: 80, height: 80, flexShrink: 0 } : {}) }, "aria-hidden": true }));
7815
+ const textBlock = (React.createElement("div", { style: isHorizontal ? { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } : undefined },
7345
7816
  React.createElement("span", { className: "seekora-item-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, String(title)),
7346
- description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null));
7817
+ description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null,
7818
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" }))));
7819
+ const content = isHorizontal ? (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
7820
+ imageBlock,
7821
+ textBlock)) : (React.createElement(React.Fragment, null,
7822
+ imageBlock,
7823
+ textBlock));
7347
7824
  const commonProps = {
7348
- className: clsx('seekora-item-card', className),
7349
- style: { ...cardStyle$1, ...style },
7825
+ className: clsx('seekora-item-card', isHorizontal && 'seekora-item-card--horizontal', className),
7826
+ style: {
7827
+ ...cardStyle$1,
7828
+ ...(isHorizontal ? { flexDirection: 'row' } : {}),
7829
+ ...style,
7830
+ },
7350
7831
  'data-position': position,
7351
7832
  onClick: onSelect,
7352
7833
  onMouseDown: onSelect ? (e) => { e.preventDefault(); onSelect(); } : undefined,
@@ -7398,619 +7879,315 @@ function ItemGrid({ items, onItemClick, getItemId = (i) => i.id, getItemTitle =
7398
7879
  }
7399
7880
 
7400
7881
  /**
7401
- * ProductCard one product tile (primitive)
7402
- *
7403
- * Minimal layout: image (via ImageDisplay when imageVariant set), title, price.
7404
- * onClick calls context selectProduct. Overridable via className/style.
7882
+ * Utility functions for Query Suggestions components
7405
7883
  */
7406
- const cardStyle = {
7407
- display: 'flex',
7408
- flexDirection: 'column',
7409
- gap: 8,
7410
- padding: 8,
7411
- cursor: 'pointer',
7412
- border: 'none',
7413
- borderRadius: 'var(--seekora-border-radius, 6px)',
7414
- backgroundColor: 'transparent',
7415
- textAlign: 'left',
7416
- transition: 'background-color 120ms ease',
7884
+ // ============================================================================
7885
+ // Field Extraction
7886
+ // ============================================================================
7887
+ /**
7888
+ * Get nested value from object using dot notation
7889
+ * @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
7890
+ */
7891
+ const getNestedValue = (obj, path) => {
7892
+ if (!obj || !path)
7893
+ return undefined;
7894
+ return path.split('.').reduce((current, key) => {
7895
+ if (current === null || current === undefined)
7896
+ return undefined;
7897
+ return current[key];
7898
+ }, obj);
7417
7899
  };
7418
- const imgStyle = {
7419
- width: '100%',
7420
- aspectRatio: '1',
7421
- objectFit: 'cover',
7422
- borderRadius: 4,
7423
- backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7900
+ /**
7901
+ * Extract suggestion fields from raw data
7902
+ */
7903
+ const extractSuggestion = (item, mapping = { query: 'query' }) => {
7904
+ return {
7905
+ query: getNestedValue(item, mapping.query) ?? '',
7906
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7907
+ id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
7908
+ categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
7909
+ highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
7910
+ _raw: item,
7911
+ };
7424
7912
  };
7425
- function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', }) {
7426
- const images = product.images?.length
7427
- ? product.images
7428
- : product.image ?? product.imageUrl
7429
- ? [String(product.image ?? product.imageUrl)]
7430
- : [];
7431
- const title = product.title ?? product.name ?? '';
7432
- const price = product.price != null ? (typeof product.price === 'number' ? product.price : Number(product.price)) : null;
7433
- return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
7434
- e.preventDefault();
7435
- onSelect();
7436
- }, "data-position": position, "data-section": section, "data-tab-id": tabId },
7437
- images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image" })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
7438
- React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
7439
- price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
7440
- product.currency ?? '$',
7441
- price.toFixed(2))) : null));
7442
- }
7443
-
7444
7913
  /**
7445
- * ProductGrid – grid of product cards from context (primitive)
7446
- *
7447
- * Uses trendingProducts or active tab products; each click calls context selectProduct.
7914
+ * Extract product fields from raw data
7448
7915
  */
7449
- function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, }) {
7450
- const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
7451
- const products = React.useMemo(() => {
7452
- if (source === 'trending')
7453
- return trendingProducts;
7454
- const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
7455
- return tab?.products ?? [];
7456
- }, [source, activeTabId, trendingProducts, filteredTabs]);
7457
- const items = products.slice(0, maxItems);
7458
- const navigableItems = getAllNavigableItems();
7459
- const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
7460
- if (items.length === 0)
7461
- return null;
7462
- const gridStyle = {
7463
- display: 'grid',
7464
- gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
7465
- gap: 12,
7466
- padding: 12,
7916
+ const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
7917
+ return {
7918
+ id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
7919
+ title: getNestedValue(item, mapping.title) ?? '',
7920
+ image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7921
+ price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
7922
+ comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
7923
+ url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
7924
+ brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
7925
+ category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
7926
+ rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
7927
+ reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
7928
+ discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
7929
+ inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
7930
+ currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
7931
+ images: mapping.images ? getNestedValue(item, mapping.images) : item?.images,
7932
+ originalPrice: mapping.originalPrice ? getNestedValue(item, mapping.originalPrice) : (item?.original_price ?? item?.compare_at_price),
7933
+ available: mapping.available ? getNestedValue(item, mapping.available) : item?.available,
7934
+ options: mapping.options ? getNestedValue(item, mapping.options) : item?.options,
7935
+ variants: mapping.variants ? getNestedValue(item, mapping.variants) : item?.variants,
7936
+ tags: mapping.tags ? getNestedValue(item, mapping.tags) : item?.tags,
7937
+ _raw: item,
7467
7938
  };
7468
- return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
7469
- React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
7470
- const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
7471
- const section = source === 'trending' ? 'products' : 'filtered_tab';
7472
- const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
7473
- return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId) }));
7474
- }))));
7475
- }
7476
-
7939
+ };
7477
7940
  /**
7478
- * CategoriesTabs horizontal tabs (e.g. filtered tabs) (primitive)
7479
- *
7480
- * Active tab from context; on select updates context and tracks analytics.
7941
+ * Extract category fields from raw data
7481
7942
  */
7482
- function CategoriesTabs({ className, style, tabClassName }) {
7483
- const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
7484
- if (filteredTabs.length === 0)
7485
- return null;
7486
- return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
7487
- display: 'flex',
7488
- gap: 4,
7489
- padding: '8px 12px',
7490
- borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
7491
- overflowX: 'auto',
7492
- ...style,
7493
- }, role: "tablist" }, filteredTabs.map((tab) => {
7494
- const isActive = activeTabId === tab.id;
7495
- return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
7496
- padding: '8px 12px',
7497
- border: 'none',
7498
- borderRadius: 'var(--seekora-border-radius, 6px)',
7499
- backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
7500
- color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
7501
- cursor: 'pointer',
7502
- fontSize: '0.875rem',
7503
- fontWeight: isActive ? 600 : 400,
7504
- whiteSpace: 'nowrap',
7505
- transition: 'background-color 120ms ease',
7506
- }, onClick: () => setActiveTab(tab) }, tab.label));
7507
- })));
7508
- }
7509
-
7943
+ const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
7944
+ return {
7945
+ id: getNestedValue(item, mapping.id) ?? item?.id,
7946
+ label: getNestedValue(item, mapping.label) ?? '',
7947
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7948
+ icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
7949
+ image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7950
+ _raw: item,
7951
+ };
7952
+ };
7510
7953
  /**
7511
- * RecentSearchesList list of recent queries (primitive)
7512
- *
7513
- * Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
7954
+ * Extract brand fields from raw data
7514
7955
  */
7515
- const itemStyle$1 = {
7516
- padding: '10px 12px',
7517
- cursor: 'pointer',
7518
- border: 'none',
7519
- width: '100%',
7520
- textAlign: 'left',
7521
- fontSize: 'inherit',
7522
- fontFamily: 'inherit',
7523
- backgroundColor: 'transparent',
7524
- color: 'var(--seekora-text-primary, #111827)',
7525
- transition: 'background-color 120ms ease',
7956
+ const extractBrand = (item, mapping = { name: 'name' }) => {
7957
+ return {
7958
+ id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
7959
+ name: getNestedValue(item, mapping.name) ?? '',
7960
+ logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
7961
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7962
+ _raw: item,
7963
+ };
7526
7964
  };
7527
- function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
7528
- const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
7529
- const items = recentSearches.slice(0, maxItems);
7530
- if (items.length === 0)
7531
- return null;
7532
- return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
7533
- title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
7534
- React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
7535
- const onSelect = () => selectRecentSearch(search);
7536
- if (renderItem) {
7537
- return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
7538
- }
7539
- return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
7540
- React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
7541
- e.preventDefault();
7542
- onSelect();
7543
- } }, search.query)));
7544
- }))));
7545
- }
7546
-
7965
+ // ============================================================================
7966
+ // Formatting
7967
+ // ============================================================================
7547
7968
  /**
7548
- * TrendingList list of trending searches (primitive)
7549
- *
7550
- * Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
7969
+ * Format price with currency
7551
7970
  */
7552
- const itemStyle = {
7553
- padding: '10px 12px',
7554
- cursor: 'pointer',
7555
- border: 'none',
7556
- width: '100%',
7557
- textAlign: 'left',
7558
- fontSize: 'inherit',
7559
- fontFamily: 'inherit',
7560
- backgroundColor: 'transparent',
7561
- color: 'var(--seekora-text-primary, #111827)',
7562
- transition: 'background-color 120ms ease',
7971
+ const formatPrice = (value, config = {}) => {
7972
+ if (value === undefined || value === null)
7973
+ return '';
7974
+ const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
7975
+ const num = typeof value === 'string' ? parseFloat(value) : value;
7976
+ if (isNaN(num))
7977
+ return String(value);
7978
+ const formatted = num.toLocaleString(undefined, {
7979
+ minimumFractionDigits: priceDecimals,
7980
+ maximumFractionDigits: priceDecimals,
7981
+ });
7982
+ return currencyPosition === 'before'
7983
+ ? `${currency}${formatted}`
7984
+ : `${formatted}${currency}`;
7563
7985
  };
7564
- function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
7565
- const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
7566
- const items = trendingSearches.slice(0, maxItems);
7567
- if (items.length === 0)
7568
- return null;
7569
- return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
7570
- title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
7571
- React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
7572
- const onSelect = () => selectTrendingSearch(trending, i);
7573
- if (renderItem) {
7574
- return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
7575
- }
7576
- return (React.createElement("li", { key: `${trending.query}-${i}` },
7577
- React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
7578
- e.preventDefault();
7579
- onSelect();
7580
- } },
7581
- trending.query,
7582
- trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
7583
- }))));
7584
- }
7585
-
7586
7986
  /**
7587
- * SuggestionsError error message (primitive)
7987
+ * Calculate discount percentage
7588
7988
  */
7589
- function SuggestionsError({ className, style, render }) {
7590
- const { error } = useSuggestionsContext();
7591
- if (!error)
7592
- return null;
7593
- if (render)
7594
- return React.createElement(React.Fragment, null, render(error));
7595
- return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
7596
- padding: 16,
7597
- color: 'var(--seekora-error, #dc2626)',
7598
- fontSize: '0.875rem',
7599
- ...style,
7600
- } }, error.message));
7601
- }
7602
-
7989
+ const calculateDiscount = (price, comparePrice) => {
7990
+ if (!price || !comparePrice || comparePrice <= price)
7991
+ return undefined;
7992
+ return Math.round(((comparePrice - price) / comparePrice) * 100);
7993
+ };
7994
+ // ============================================================================
7995
+ // Text Processing
7996
+ // ============================================================================
7603
7997
  /**
7604
- * SuggestionsDropdownComposition reference composition
7605
- *
7606
- * Example layout built from primitives: SearchInput + DropdownPanel containing
7607
- * RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
7608
- * Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
7609
- * with your own arrangement of the same primitives.
7998
+ * Escape HTML special characters
7610
7999
  */
7611
- function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
7612
- return (React.createElement(SuggestionsProvider, { ...providerProps },
7613
- React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
7614
- React.createElement(SearchInput, { placeholder: placeholder }),
7615
- React.createElement(DropdownPanel, null,
7616
- React.createElement(SuggestionsError, null),
7617
- React.createElement(SuggestionsLoading, null),
7618
- showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
7619
- React.createElement(SuggestionList, null),
7620
- showTabs ? React.createElement(CategoriesTabs, null) : null,
7621
- showProducts ? React.createElement(ProductGrid, null) : null,
7622
- showTrending ? React.createElement(TrendingList, null) : null))));
7623
- }
7624
-
8000
+ const escapeHtml = (text) => {
8001
+ const map = {
8002
+ '&': '&amp;',
8003
+ '<': '&lt;',
8004
+ '>': '&gt;',
8005
+ '"': '&quot;',
8006
+ "'": '&#39;',
8007
+ };
8008
+ return text.replace(/[&<>"']/g, (char) => map[char]);
8009
+ };
7625
8010
  /**
7626
- * SectionSearchContext preset query/filter section state
7627
- *
7628
- * For menus, sidebar, front-page blocks. Independent of main search state.
8011
+ * Highlight matching text in a string
7629
8012
  */
7630
- const SectionSearchContext = React.createContext(null);
7631
- function useSectionSearchContext() {
7632
- const context = React.useContext(SectionSearchContext);
7633
- if (!context) {
7634
- const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
7635
- log.error('SectionSearchContext: not available', { error: error.message });
7636
- throw error;
8013
+ const highlightText = (text, query, options = {}) => {
8014
+ if (!query || !text)
8015
+ return escapeHtml(text);
8016
+ const { tag = 'mark', className = '', style } = options;
8017
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8018
+ const styleAttr = style
8019
+ ? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
8020
+ : '';
8021
+ const classAttr = className ? ` class="${className}"` : '';
8022
+ return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
8023
+ };
8024
+ // ============================================================================
8025
+ // Variant Utilities
8026
+ // ============================================================================
8027
+ /**
8028
+ * Extract badges from product tags (Shopify convention: "badge:new", "badge:limited")
8029
+ * and auto-generate sale/soldOut badges from product data.
8030
+ */
8031
+ const extractBadges = (tags, product) => {
8032
+ const badges = [];
8033
+ // Extract from tags (Shopify convention)
8034
+ if (tags) {
8035
+ for (const tag of tags) {
8036
+ const lower = tag.toLowerCase().trim();
8037
+ if (lower.startsWith('badge:') || lower.startsWith('badge: ')) {
8038
+ const text = tag.slice(tag.indexOf(':') + 1).trim();
8039
+ if (text) {
8040
+ badges.push({ text, type: 'custom' });
8041
+ }
8042
+ }
8043
+ else if (lower === 'new' || lower === 'new arrival') {
8044
+ badges.push({ text: 'New', type: 'new' });
8045
+ }
8046
+ else if (lower === 'limited' || lower === 'limited edition') {
8047
+ badges.push({ text: 'Limited', type: 'limited' });
8048
+ }
8049
+ }
7637
8050
  }
7638
- return context;
7639
- }
7640
-
8051
+ // Auto-generate sale badge
8052
+ if (product) {
8053
+ const comparePrice = product.original_price ?? product.compare_at_price;
8054
+ if (comparePrice && product.price && comparePrice > product.price) {
8055
+ const discount = Math.round(((comparePrice - product.price) / comparePrice) * 100);
8056
+ badges.push({ text: `${discount}% Off`, type: 'sale' });
8057
+ }
8058
+ }
8059
+ // Auto-generate sold out badge
8060
+ if (product && product.available === false) {
8061
+ badges.push({ text: 'Sold Out', type: 'soldOut' });
8062
+ }
8063
+ return badges;
8064
+ };
7641
8065
  /**
7642
- * SectionSearchProvider preset query + filter section
7643
- *
7644
- * Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
7645
- * query/filters change. Does not use global SearchStateManager. Use for menus,
7646
- * sidebar, front-page blocks (e.g. "New arrivals", "On sale").
8066
+ * Compute min/max price from variants. Returns null if all variants have the same price.
7647
8067
  */
7648
- function extractItems(response) {
7649
- if (!response)
8068
+ const getPriceRange = (variants) => {
8069
+ if (!variants || variants.length === 0)
8070
+ return null;
8071
+ const prices = variants
8072
+ .map((v) => v.price)
8073
+ .filter((p) => p != null && !isNaN(p));
8074
+ if (prices.length === 0)
8075
+ return null;
8076
+ const min = Math.min(...prices);
8077
+ const max = Math.max(...prices);
8078
+ if (min === max)
8079
+ return null;
8080
+ return { min, max };
8081
+ };
8082
+ /**
8083
+ * Format a price range like "$54.00 - $72.00"
8084
+ */
8085
+ const formatPriceRange = (range, config = {}) => {
8086
+ return `${formatPrice(range.min, config)} - ${formatPrice(range.max, config)}`;
8087
+ };
8088
+ /**
8089
+ * Given current selections, return which values for an option are still available
8090
+ * based on variant availability.
8091
+ */
8092
+ const getAvailableValuesForOption = (optionName, options, variants, selections) => {
8093
+ const option = options.find((o) => o.name === optionName);
8094
+ if (!option)
7650
8095
  return [];
7651
- if (Array.isArray(response.results))
7652
- return response.results;
7653
- if (Array.isArray(response.hits))
7654
- return response.hits;
7655
- const data = response.data;
7656
- if (data && Array.isArray(data.results))
7657
- return data.results;
7658
- if (data && Array.isArray(data.data))
7659
- return data.data;
7660
- return [];
7661
- }
7662
- function extractTotal(response) {
7663
- if (!response)
7664
- return 0;
7665
- const n = response.totalResults ?? response.total ?? response.total_results;
7666
- if (typeof n === 'number')
7667
- return n;
7668
- const data = response.data;
7669
- if (data?.total_results != null)
7670
- return Number(data.total_results);
7671
- if (data?.data?.total_results != null)
7672
- return Number(data.data.total_results);
7673
- return 0;
7674
- }
7675
- function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
7676
- const { client } = useSearchContext();
7677
- const [items, setItems] = React.useState([]);
7678
- const [loading, setLoading] = React.useState(true);
7679
- const [error, setError] = React.useState(null);
7680
- const [totalCount, setTotalCount] = React.useState(0);
7681
- React.useEffect(() => {
7682
- if (!enabled || !client?.search) {
7683
- setItems([]);
7684
- setLoading(false);
7685
- setError(null);
7686
- setTotalCount(0);
7687
- return;
7688
- }
7689
- let cancelled = false;
7690
- setLoading(true);
7691
- setError(null);
7692
- const options = {
7693
- per_page: maxItems,
7694
- page: 1,
7695
- };
7696
- if (sortBy)
7697
- options.sort_by = sortBy;
7698
- if (refinements.length > 0) {
7699
- options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
7700
- }
7701
- client
7702
- .search(query, options)
7703
- .then((response) => {
7704
- if (cancelled)
7705
- return;
7706
- setItems(extractItems(response));
7707
- setTotalCount(extractTotal(response));
7708
- setLoading(false);
7709
- })
7710
- .catch((err) => {
7711
- if (cancelled)
7712
- return;
7713
- setError(err instanceof Error ? err : new Error(String(err)));
7714
- setItems([]);
7715
- setLoading(false);
8096
+ const optionIndex = options.indexOf(option);
8097
+ const optionKey = `option${optionIndex + 1}`;
8098
+ return option.values.map((value) => {
8099
+ // Check if any variant with this value and current other selections is available
8100
+ const available = variants.some((variant) => {
8101
+ if (variant[optionKey] !== value)
8102
+ return false;
8103
+ if (variant.available === false)
8104
+ return false;
8105
+ // Check other selections match
8106
+ for (const [selName, selValue] of Object.entries(selections)) {
8107
+ if (selName === optionName)
8108
+ continue;
8109
+ const selOption = options.find((o) => o.name === selName);
8110
+ if (!selOption)
8111
+ continue;
8112
+ const selIdx = options.indexOf(selOption);
8113
+ const selKey = `option${selIdx + 1}`;
8114
+ if (variant[selKey] !== selValue)
8115
+ return false;
8116
+ }
8117
+ return true;
7716
8118
  });
7717
- return () => {
7718
- cancelled = true;
7719
- };
7720
- }, [client, enabled, query, maxItems, sortBy, refinements]);
7721
- const trackClick = React.useCallback((item, position) => {
7722
- if (!client?.trackEvent)
7723
- return;
7724
- const id = item?.id ?? item?.objectID;
7725
- client.trackEvent({
7726
- event_name: 'section_result_click',
7727
- clicked_item_id: id,
7728
- position,
7729
- section: sectionId,
7730
- metadata: { section_id: sectionId },
7731
- }, undefined);
7732
- }, [client, sectionId]);
7733
- const value = React.useMemo(() => ({
7734
- items,
7735
- loading,
7736
- error,
7737
- totalCount,
7738
- sectionId,
7739
- trackClick,
7740
- }), [items, loading, error, totalCount, sectionId, trackClick]);
7741
- return React.createElement(SectionSearchContext.Provider, { value: value }, children);
7742
- }
7743
-
7744
- /**
7745
- * SectionLoading – loading state for section (primitive)
7746
- */
7747
- function SectionLoading({ className, style, text = 'Loading...' }) {
7748
- const { loading } = useSectionSearchContext();
7749
- if (!loading)
7750
- return null;
7751
- return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
7752
- }
7753
-
7754
- /**
7755
- * SectionError – error state for section (primitive)
7756
- */
7757
- function SectionError({ className, style, render }) {
7758
- const { error } = useSectionSearchContext();
7759
- if (!error)
7760
- return null;
7761
- if (render)
7762
- return React.createElement(React.Fragment, null, render(error));
7763
- return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
7764
- }
7765
-
7766
- /**
7767
- * SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
7768
- */
7769
- function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
7770
- const { items, loading, error, trackClick } = useSectionSearchContext();
7771
- if (loading)
7772
- return React.createElement(SectionLoading, { className: className, style: style });
7773
- if (error)
7774
- return React.createElement(SectionError, { className: className, style: style });
7775
- return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
7776
- }
7777
-
7778
- /**
7779
- * ProductGallery – product detail image gallery (primitive)
7780
- *
7781
- * Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
7782
- * For use on individual product page.
7783
- */
7784
- function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
7785
- return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
7786
- React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
7787
- }
7788
-
7789
- /**
7790
- * ProductInfo – product detail block (primitive)
7791
- *
7792
- * Title, description, price, optional variant selector and CTA. Minimal layout;
7793
- * override with className/style. For use on individual product page.
7794
- */
7795
- function ProductInfo({ title, description, price, currency = '$', renderVariantSelector, renderCTA, className, style, }) {
7796
- const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
7797
- return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
7798
- React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
7799
- priceNum != null && !Number.isNaN(priceNum) ? (React.createElement("span", { className: "seekora-product-info-price", style: { fontSize: '1.125rem', fontWeight: 600 } },
7800
- currency,
7801
- priceNum.toFixed(2))) : null,
7802
- description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
7803
- renderVariantSelector?.(),
7804
- renderCTA?.()));
7805
- }
7806
-
7807
- /**
7808
- * ProductRecommendations – related / frequently bought (primitive)
7809
- *
7810
- * Renders a section of recommended items (generic ItemGrid or product list).
7811
- * Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
7812
- * For use on individual product page.
7813
- */
7814
- function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
7815
- if (!items?.length)
7816
- return null;
7817
- return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
7818
- React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
7819
- React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
7820
- }
7821
-
8119
+ return { value, available };
8120
+ });
8121
+ };
7822
8122
  /**
7823
- * Utility functions for Query Suggestions components
8123
+ * Find the exact variant matching all selected options.
7824
8124
  */
8125
+ const findVariantBySelections = (options, variants, selections) => {
8126
+ return variants.find((variant) => {
8127
+ return options.every((option, idx) => {
8128
+ const key = `option${idx + 1}`;
8129
+ const selected = selections[option.name];
8130
+ if (!selected)
8131
+ return true; // not yet selected
8132
+ return variant[key] === selected;
8133
+ });
8134
+ }) ?? null;
8135
+ };
7825
8136
  // ============================================================================
7826
- // Field Extraction
8137
+ // Theme & Styling
7827
8138
  // ============================================================================
7828
8139
  /**
7829
- * Get nested value from object using dot notation
7830
- * @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
7831
- */
7832
- const getNestedValue = (obj, path) => {
7833
- if (!obj || !path)
7834
- return undefined;
7835
- return path.split('.').reduce((current, key) => {
7836
- if (current === null || current === undefined)
7837
- return undefined;
7838
- return current[key];
7839
- }, obj);
7840
- };
7841
- /**
7842
- * Extract suggestion fields from raw data
8140
+ * Generate CSS variables from theme config
7843
8141
  */
7844
- const extractSuggestion = (item, mapping = { query: 'query' }) => {
7845
- return {
7846
- query: getNestedValue(item, mapping.query) ?? '',
7847
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7848
- id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
7849
- categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
7850
- highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
7851
- _raw: item,
7852
- };
8142
+ const generateCSSVariables$1 = (theme, prefix = 'seekora') => {
8143
+ const vars = {};
8144
+ if (theme.primaryColor)
8145
+ vars[`--${prefix}-primary`] = theme.primaryColor;
8146
+ if (theme.backgroundColor)
8147
+ vars[`--${prefix}-bg-surface`] = theme.backgroundColor;
8148
+ if (theme.surfaceColor)
8149
+ vars[`--${prefix}-bg-secondary`] = theme.surfaceColor;
8150
+ if (theme.textColor)
8151
+ vars[`--${prefix}-text-primary`] = theme.textColor;
8152
+ if (theme.textSecondaryColor)
8153
+ vars[`--${prefix}-text-secondary`] = theme.textSecondaryColor;
8154
+ if (theme.borderColor)
8155
+ vars[`--${prefix}-border-color`] = theme.borderColor;
8156
+ if (theme.hoverColor)
8157
+ vars[`--${prefix}-bg-hover`] = theme.hoverColor;
8158
+ if (theme.highlightColor)
8159
+ vars[`--${prefix}-highlight-bg`] = theme.highlightColor;
8160
+ if (theme.successColor)
8161
+ vars[`--${prefix}-success`] = theme.successColor;
8162
+ if (theme.errorColor)
8163
+ vars[`--${prefix}-error`] = theme.errorColor;
8164
+ if (theme.fontFamily)
8165
+ vars[`--${prefix}-font-family`] = theme.fontFamily;
8166
+ if (theme.fontSize)
8167
+ vars[`--${prefix}-font-size`] = typeof theme.fontSize === 'number' ? `${theme.fontSize}px` : theme.fontSize;
8168
+ if (theme.borderRadius)
8169
+ vars[`--${prefix}-border-radius`] = typeof theme.borderRadius === 'number' ? `${theme.borderRadius}px` : theme.borderRadius;
8170
+ if (theme.boxShadow)
8171
+ vars[`--${prefix}-box-shadow`] = theme.boxShadow;
8172
+ // Merge custom CSS variables
8173
+ if (theme.cssVariables) {
8174
+ Object.entries(theme.cssVariables).forEach(([key, value]) => {
8175
+ vars[key.startsWith('--') ? key : `--${key}`] = value;
8176
+ });
8177
+ }
8178
+ return vars;
7853
8179
  };
7854
8180
  /**
7855
- * Extract product fields from raw data
8181
+ * Merge class names, filtering out falsy values
7856
8182
  */
7857
- const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
7858
- return {
7859
- id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
7860
- title: getNestedValue(item, mapping.title) ?? '',
7861
- image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7862
- price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
7863
- comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
7864
- url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
7865
- brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
7866
- category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
7867
- rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
7868
- reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
7869
- discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
7870
- inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
7871
- currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
7872
- _raw: item,
7873
- };
8183
+ const cx = (...classes) => {
8184
+ return classes.filter(Boolean).join(' ');
7874
8185
  };
7875
8186
  /**
7876
- * Extract category fields from raw data
8187
+ * Merge styles, filtering out undefined values
7877
8188
  */
7878
- const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
7879
- return {
7880
- id: getNestedValue(item, mapping.id) ?? item?.id,
7881
- label: getNestedValue(item, mapping.label) ?? '',
7882
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7883
- icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
7884
- image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7885
- _raw: item,
7886
- };
7887
- };
7888
- /**
7889
- * Extract brand fields from raw data
7890
- */
7891
- const extractBrand = (item, mapping = { name: 'name' }) => {
7892
- return {
7893
- id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
7894
- name: getNestedValue(item, mapping.name) ?? '',
7895
- logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
7896
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7897
- _raw: item,
7898
- };
7899
- };
7900
- // ============================================================================
7901
- // Formatting
7902
- // ============================================================================
7903
- /**
7904
- * Format price with currency
7905
- */
7906
- const formatPrice = (value, config = {}) => {
7907
- if (value === undefined || value === null)
7908
- return '';
7909
- const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
7910
- const num = typeof value === 'string' ? parseFloat(value) : value;
7911
- if (isNaN(num))
7912
- return String(value);
7913
- const formatted = num.toLocaleString(undefined, {
7914
- minimumFractionDigits: priceDecimals,
7915
- maximumFractionDigits: priceDecimals,
7916
- });
7917
- return currencyPosition === 'before'
7918
- ? `${currency}${formatted}`
7919
- : `${formatted}${currency}`;
7920
- };
7921
- /**
7922
- * Calculate discount percentage
7923
- */
7924
- const calculateDiscount = (price, comparePrice) => {
7925
- if (!price || !comparePrice || comparePrice <= price)
7926
- return undefined;
7927
- return Math.round(((comparePrice - price) / comparePrice) * 100);
7928
- };
7929
- // ============================================================================
7930
- // Text Processing
7931
- // ============================================================================
7932
- /**
7933
- * Escape HTML special characters
7934
- */
7935
- const escapeHtml = (text) => {
7936
- const map = {
7937
- '&': '&amp;',
7938
- '<': '&lt;',
7939
- '>': '&gt;',
7940
- '"': '&quot;',
7941
- "'": '&#39;',
7942
- };
7943
- return text.replace(/[&<>"']/g, (char) => map[char]);
7944
- };
7945
- /**
7946
- * Highlight matching text in a string
7947
- */
7948
- const highlightText = (text, query, options = {}) => {
7949
- if (!query || !text)
7950
- return escapeHtml(text);
7951
- const { tag = 'mark', className = '', style } = options;
7952
- const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7953
- const styleAttr = style
7954
- ? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
7955
- : '';
7956
- const classAttr = className ? ` class="${className}"` : '';
7957
- return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
7958
- };
7959
- // ============================================================================
7960
- // Theme & Styling
7961
- // ============================================================================
7962
- /**
7963
- * Generate CSS variables from theme config
7964
- */
7965
- const generateCSSVariables$1 = (theme, prefix = 'seekora') => {
7966
- const vars = {};
7967
- if (theme.primaryColor)
7968
- vars[`--${prefix}-primary`] = theme.primaryColor;
7969
- if (theme.backgroundColor)
7970
- vars[`--${prefix}-bg-surface`] = theme.backgroundColor;
7971
- if (theme.surfaceColor)
7972
- vars[`--${prefix}-bg-secondary`] = theme.surfaceColor;
7973
- if (theme.textColor)
7974
- vars[`--${prefix}-text-primary`] = theme.textColor;
7975
- if (theme.textSecondaryColor)
7976
- vars[`--${prefix}-text-secondary`] = theme.textSecondaryColor;
7977
- if (theme.borderColor)
7978
- vars[`--${prefix}-border-color`] = theme.borderColor;
7979
- if (theme.hoverColor)
7980
- vars[`--${prefix}-bg-hover`] = theme.hoverColor;
7981
- if (theme.highlightColor)
7982
- vars[`--${prefix}-highlight-bg`] = theme.highlightColor;
7983
- if (theme.successColor)
7984
- vars[`--${prefix}-success`] = theme.successColor;
7985
- if (theme.errorColor)
7986
- vars[`--${prefix}-error`] = theme.errorColor;
7987
- if (theme.fontFamily)
7988
- vars[`--${prefix}-font-family`] = theme.fontFamily;
7989
- if (theme.fontSize)
7990
- vars[`--${prefix}-font-size`] = typeof theme.fontSize === 'number' ? `${theme.fontSize}px` : theme.fontSize;
7991
- if (theme.borderRadius)
7992
- vars[`--${prefix}-border-radius`] = typeof theme.borderRadius === 'number' ? `${theme.borderRadius}px` : theme.borderRadius;
7993
- if (theme.boxShadow)
7994
- vars[`--${prefix}-box-shadow`] = theme.boxShadow;
7995
- // Merge custom CSS variables
7996
- if (theme.cssVariables) {
7997
- Object.entries(theme.cssVariables).forEach(([key, value]) => {
7998
- vars[key.startsWith('--') ? key : `--${key}`] = value;
7999
- });
8000
- }
8001
- return vars;
8002
- };
8003
- /**
8004
- * Merge class names, filtering out falsy values
8005
- */
8006
- const cx = (...classes) => {
8007
- return classes.filter(Boolean).join(' ');
8008
- };
8009
- /**
8010
- * Merge styles, filtering out undefined values
8011
- */
8012
- const mergeStyles = (...styles) => {
8013
- return Object.assign({}, ...styles.filter(Boolean));
8189
+ const mergeStyles = (...styles) => {
8190
+ return Object.assign({}, ...styles.filter(Boolean));
8014
8191
  };
8015
8192
  // ============================================================================
8016
8193
  // Local Storage (Recent Searches)
@@ -8113,80 +8290,1424 @@ class SuggestionsCache {
8113
8290
  this.cache.delete(key);
8114
8291
  return null;
8115
8292
  }
8116
- return entry.data;
8117
- }
8118
- /**
8119
- * Store data in cache
8120
- */
8121
- set(key, data, ttlMs) {
8122
- // Evict oldest entries if at max size
8123
- if (this.cache.size >= this.maxSize) {
8124
- const oldestKey = this.cache.keys().next().value;
8125
- if (oldestKey)
8126
- this.cache.delete(oldestKey);
8293
+ return entry.data;
8294
+ }
8295
+ /**
8296
+ * Store data in cache
8297
+ */
8298
+ set(key, data, ttlMs) {
8299
+ // Evict oldest entries if at max size
8300
+ if (this.cache.size >= this.maxSize) {
8301
+ const oldestKey = this.cache.keys().next().value;
8302
+ if (oldestKey)
8303
+ this.cache.delete(oldestKey);
8304
+ }
8305
+ this.cache.set(key, {
8306
+ data,
8307
+ timestamp: Date.now(),
8308
+ ttl: ttlMs ?? this.defaultTtl,
8309
+ });
8310
+ }
8311
+ /**
8312
+ * Check if key exists and is valid
8313
+ */
8314
+ has(key) {
8315
+ return this.get(key) !== null;
8316
+ }
8317
+ /**
8318
+ * Clear all cached entries
8319
+ */
8320
+ clear() {
8321
+ this.cache.clear();
8322
+ }
8323
+ /**
8324
+ * Clear expired entries
8325
+ */
8326
+ cleanup() {
8327
+ const now = Date.now();
8328
+ for (const [key, entry] of this.cache.entries()) {
8329
+ if (now - entry.timestamp > entry.ttl) {
8330
+ this.cache.delete(key);
8331
+ }
8332
+ }
8333
+ }
8334
+ /**
8335
+ * Get cache statistics
8336
+ */
8337
+ getStats() {
8338
+ return {
8339
+ size: this.cache.size,
8340
+ maxSize: this.maxSize,
8341
+ };
8342
+ }
8343
+ }
8344
+ // Global cache instance for suggestions (shared across components)
8345
+ let globalSuggestionsCache = null;
8346
+ /**
8347
+ * Get the global suggestions cache instance
8348
+ */
8349
+ const getSuggestionsCache = (options) => {
8350
+ if (!globalSuggestionsCache) {
8351
+ globalSuggestionsCache = new SuggestionsCache(options);
8352
+ }
8353
+ return globalSuggestionsCache;
8354
+ };
8355
+ /**
8356
+ * Create a new cache instance (for isolated caching per component)
8357
+ */
8358
+ const createSuggestionsCache = (options) => {
8359
+ return new SuggestionsCache(options);
8360
+ };
8361
+ /**
8362
+ * Clear the global cache
8363
+ */
8364
+ const clearSuggestionsCache = () => {
8365
+ globalSuggestionsCache?.clear();
8366
+ };
8367
+
8368
+ /**
8369
+ * PriceDisplay – reusable price display primitive
8370
+ *
8371
+ * Handles single price, compare price with strikethrough, discount percentage,
8372
+ * and price range display.
8373
+ */
8374
+ const formatNum = (value, currency, position, decimals) => {
8375
+ const formatted = value.toLocaleString(undefined, {
8376
+ minimumFractionDigits: decimals,
8377
+ maximumFractionDigits: decimals,
8378
+ });
8379
+ return position === 'before' ? `${currency}${formatted}` : `${formatted}${currency}`;
8380
+ };
8381
+ function PriceDisplay({ price, comparePrice, priceRange, currency = '$', currencyPosition = 'before', priceDecimals = 2, showDiscount = true, className, style, }) {
8382
+ const fmt = (v) => formatNum(v, currency, currencyPosition, priceDecimals);
8383
+ // Price range mode
8384
+ if (priceRange) {
8385
+ return (React.createElement("span", { className: clsx('seekora-price-display', 'seekora-price-range', className), style: style },
8386
+ fmt(priceRange.min),
8387
+ " \u2013 ",
8388
+ fmt(priceRange.max)));
8389
+ }
8390
+ if (price == null)
8391
+ return null;
8392
+ const hasCompare = comparePrice != null && comparePrice > price;
8393
+ const discount = hasCompare ? Math.round(((comparePrice - price) / comparePrice) * 100) : 0;
8394
+ return (React.createElement("span", { className: clsx('seekora-price-display', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', ...style } },
8395
+ React.createElement("span", { className: "seekora-price-current", style: { fontWeight: 600 } }, fmt(price)),
8396
+ hasCompare && (React.createElement("span", { className: "seekora-price-compare", style: {
8397
+ textDecoration: 'line-through',
8398
+ color: 'var(--seekora-text-secondary, #6b7280)',
8399
+ fontSize: '0.875em',
8400
+ } }, fmt(comparePrice))),
8401
+ hasCompare && showDiscount && discount > 0 && (React.createElement("span", { className: "seekora-price-discount", style: {
8402
+ color: 'var(--seekora-error, #ef4444)',
8403
+ fontSize: '0.8125em',
8404
+ fontWeight: 600,
8405
+ } },
8406
+ "-",
8407
+ discount,
8408
+ "%"))));
8409
+ }
8410
+
8411
+ /**
8412
+ * BadgeList – renders product badges (sale, new, sold out, custom)
8413
+ */
8414
+ const positionStyles = {
8415
+ 'top-left': { position: 'absolute', top: 6, left: 6 },
8416
+ 'top-right': { position: 'absolute', top: 6, right: 6 },
8417
+ 'bottom-left': { position: 'absolute', bottom: 6, left: 6 },
8418
+ 'bottom-right': { position: 'absolute', bottom: 6, right: 6 },
8419
+ inline: {},
8420
+ };
8421
+ const typeColors = {
8422
+ sale: { bg: '#ef4444', text: '#fff' },
8423
+ new: { bg: '#3b82f6', text: '#fff' },
8424
+ soldOut: { bg: '#6b7280', text: '#fff' },
8425
+ limited: { bg: '#f59e0b', text: '#fff' },
8426
+ custom: { bg: '#111827', text: '#fff' },
8427
+ };
8428
+ function BadgeList({ badges, maxBadges, position = 'top-left', className, style, }) {
8429
+ if (!badges || badges.length === 0)
8430
+ return null;
8431
+ const visible = maxBadges ? badges.slice(0, maxBadges) : badges;
8432
+ return (React.createElement("div", { className: clsx('seekora-badge-list', className), style: {
8433
+ display: 'flex',
8434
+ flexWrap: 'wrap',
8435
+ gap: 4,
8436
+ zIndex: 1,
8437
+ ...positionStyles[position],
8438
+ ...style,
8439
+ } }, visible.map((badge, i) => {
8440
+ const colors = typeColors[badge.type ?? 'custom'];
8441
+ return (React.createElement("span", { key: `${badge.text}-${i}`, className: clsx('seekora-badge', badge.type && `seekora-badge--${badge.type === 'soldOut' ? 'sold-out' : badge.type}`), style: {
8442
+ display: 'inline-block',
8443
+ padding: '2px 8px',
8444
+ borderRadius: 4,
8445
+ fontSize: '0.6875rem',
8446
+ fontWeight: 600,
8447
+ lineHeight: 1.4,
8448
+ backgroundColor: badge.color ?? colors.bg,
8449
+ color: badge.textColor ?? colors.text,
8450
+ whiteSpace: 'nowrap',
8451
+ } }, badge.text));
8452
+ })));
8453
+ }
8454
+
8455
+ /**
8456
+ * RatingDisplay – star rating display with review count
8457
+ *
8458
+ * Supports multiple variants: stars-only, compact, full, inline.
8459
+ * Can be read-only (display) or interactive (for reviews).
8460
+ */
8461
+ const sizeMap = {
8462
+ small: 14,
8463
+ medium: 18,
8464
+ large: 24,
8465
+ };
8466
+ const fontSizeMap = {
8467
+ small: '0.75rem',
8468
+ medium: '0.875rem',
8469
+ large: '1rem',
8470
+ };
8471
+ function StarIcon({ filled, half, size, color, emptyColor, interactive, onHover, onClick, }) {
8472
+ if (half) {
8473
+ return (React.createElement("span", { className: clsx('seekora-rating-star', 'seekora-rating-star--half', interactive && 'seekora-rating-star--interactive'), style: {
8474
+ position: 'relative',
8475
+ display: 'inline-block',
8476
+ width: size,
8477
+ height: size,
8478
+ cursor: interactive ? 'pointer' : 'default',
8479
+ }, onMouseEnter: onHover, onClick: onClick },
8480
+ React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: { position: 'absolute', top: 0, left: 0 } },
8481
+ React.createElement("defs", null,
8482
+ React.createElement("linearGradient", { id: "half-fill" },
8483
+ React.createElement("stop", { offset: "50%", stopColor: color }),
8484
+ React.createElement("stop", { offset: "50%", stopColor: emptyColor }))),
8485
+ React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", fill: "url(#half-fill)", stroke: color, strokeWidth: "1" }))));
8486
+ }
8487
+ return (React.createElement("span", { className: clsx('seekora-rating-star', filled ? 'seekora-rating-star--filled' : 'seekora-rating-star--empty', interactive && 'seekora-rating-star--interactive'), style: {
8488
+ display: 'inline-block',
8489
+ width: size,
8490
+ height: size,
8491
+ cursor: interactive ? 'pointer' : 'default',
8492
+ }, onMouseEnter: onHover, onClick: onClick },
8493
+ React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: filled ? color : 'none', xmlns: "http://www.w3.org/2000/svg" },
8494
+ React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", stroke: filled ? color : emptyColor, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))));
8495
+ }
8496
+ function RatingDisplay({ rating, reviewCount, variant = 'compact', size = 'medium', maxRating = 5, showNumeric = false, showHalfStars = true, interactive = false, onRatingChange, starColor = '#f59e0b', emptyStarColor = '#d1d5db', textColor = 'var(--seekora-text-secondary, #6b7280)', showReviewCount = true, reviewCountFormat, className, style, }) {
8497
+ const [hoverRating, setHoverRating] = React.useState(null);
8498
+ const clampedRating = Math.max(0, Math.min(maxRating, rating));
8499
+ const displayRating = interactive && hoverRating !== null ? hoverRating : clampedRating;
8500
+ const starSize = sizeMap[size];
8501
+ const fontSize = fontSizeMap[size];
8502
+ const formatReviewCount = (count) => {
8503
+ if (reviewCountFormat)
8504
+ return reviewCountFormat(count);
8505
+ if (count >= 1000000)
8506
+ return `${(count / 1000000).toFixed(1)}M`;
8507
+ if (count >= 1000)
8508
+ return `${(count / 1000).toFixed(1)}K`;
8509
+ return count.toString();
8510
+ };
8511
+ const renderStars = () => {
8512
+ const stars = [];
8513
+ for (let i = 1; i <= maxRating; i++) {
8514
+ const filled = i <= Math.floor(displayRating);
8515
+ const half = showHalfStars && i === Math.ceil(displayRating) && displayRating % 1 >= 0.25 && displayRating % 1 < 0.75;
8516
+ stars.push(React.createElement(StarIcon, { key: i, filled: filled, half: half, size: starSize, color: starColor, emptyColor: emptyStarColor, interactive: interactive, onHover: interactive ? () => setHoverRating(i) : undefined, onClick: interactive
8517
+ ? () => {
8518
+ setHoverRating(null);
8519
+ onRatingChange?.(i);
8520
+ }
8521
+ : undefined }));
8522
+ }
8523
+ return stars;
8524
+ };
8525
+ const handleMouseLeave = () => {
8526
+ if (interactive) {
8527
+ setHoverRating(null);
8528
+ }
8529
+ };
8530
+ if (variant === 'stars-only') {
8531
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--stars-only', className), style: { display: 'inline-flex', alignItems: 'center', gap: 2, ...style }, onMouseLeave: handleMouseLeave }, renderStars()));
8532
+ }
8533
+ if (variant === 'compact') {
8534
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--compact', className), style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8535
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8536
+ showNumeric && (React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: textColor } }, clampedRating.toFixed(1))),
8537
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
8538
+ "(",
8539
+ formatReviewCount(reviewCount),
8540
+ ")"))));
8541
+ }
8542
+ if (variant === 'full') {
8543
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--full', className), style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8544
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 6 } },
8545
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8546
+ React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
8547
+ React.createElement("span", { className: "seekora-rating-max", style: { color: textColor } },
8548
+ "/ ",
8549
+ maxRating)),
8550
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-text", style: { fontSize: '0.875em', color: textColor } },
8551
+ "Based on ",
8552
+ formatReviewCount(reviewCount),
8553
+ " ",
8554
+ reviewCount === 1 ? 'review' : 'reviews'))));
8555
+ }
8556
+ if (variant === 'inline') {
8557
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--inline', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8558
+ React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
8559
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8560
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
8561
+ "(",
8562
+ formatReviewCount(reviewCount),
8563
+ ")"))));
8564
+ }
8565
+ return null;
8566
+ }
8567
+
8568
+ /**
8569
+ * VariantSwatches – inline variant indicators for card display
8570
+ *
8571
+ * Shows color dots, size labels, and "+N more" overflow for compact card views.
8572
+ */
8573
+ const COLOR_NAMES$1 = {
8574
+ black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
8575
+ green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
8576
+ pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
8577
+ navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
8578
+ khaki: '#c3b091', olive: '#808000', teal: '#0d9488', coral: '#ff7f50',
8579
+ maroon: '#800000', tan: '#d2b48c', charcoal: '#36454f', burgundy: '#800020',
8580
+ sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
8581
+ plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
8582
+ silver: '#c0c0c0', rose: '#ff007f', mauve: '#e0b0ff', wine: '#722f37',
8583
+ raven: '#0a0a0a', natural: '#f5f0e1', bone: '#e3dac9', sand: '#c2b280',
8584
+ };
8585
+ const isColorOption$1 = (name) => {
8586
+ const lower = name.toLowerCase();
8587
+ return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
8588
+ };
8589
+ const resolveColor$1 = (value, colorMap) => {
8590
+ if (colorMap?.[value])
8591
+ return colorMap[value];
8592
+ const lower = value.toLowerCase();
8593
+ if (colorMap?.[lower])
8594
+ return colorMap[lower];
8595
+ return COLOR_NAMES$1[lower] ?? null;
8596
+ };
8597
+ const getAvailability$1 = (optionName, value, options, variants, selectedValues) => {
8598
+ if (!variants || variants.length === 0)
8599
+ return true;
8600
+ const optionIndex = options.findIndex((o) => o.name === optionName);
8601
+ if (optionIndex === -1)
8602
+ return true;
8603
+ const optionKey = `option${optionIndex + 1}`;
8604
+ return variants.some((variant) => {
8605
+ // Must match the current option value
8606
+ if (variant[optionKey] !== value)
8607
+ return false;
8608
+ // Must be available
8609
+ if (variant.available === false)
8610
+ return false;
8611
+ // Must match all other selected values
8612
+ if (selectedValues) {
8613
+ for (const [selName, selValue] of Object.entries(selectedValues)) {
8614
+ if (selName === optionName)
8615
+ continue; // Skip current option
8616
+ const selIdx = options.findIndex((o) => o.name === selName);
8617
+ if (selIdx === -1)
8618
+ continue;
8619
+ const selKey = `option${selIdx + 1}`;
8620
+ if (variant[selKey] !== selValue)
8621
+ return false;
8622
+ }
8623
+ }
8624
+ return true;
8625
+ });
8626
+ };
8627
+ function VariantSwatches({ options, visibleOptions, maxValues = 5, colorMap, selectedValues, variants, showAvailability = true, onSwatchHover, onSwatchClick, className, style, }) {
8628
+ const [expandedOptions, setExpandedOptions] = React.useState(new Set());
8629
+ if (!options || options.length === 0)
8630
+ return null;
8631
+ const filtered = visibleOptions
8632
+ ? options.filter((o) => visibleOptions.includes(o.name))
8633
+ : options;
8634
+ if (filtered.length === 0)
8635
+ return null;
8636
+ const toggleExpanded = (optionName, e) => {
8637
+ e.stopPropagation();
8638
+ setExpandedOptions((prev) => {
8639
+ const next = new Set(prev);
8640
+ if (next.has(optionName)) {
8641
+ next.delete(optionName);
8642
+ }
8643
+ else {
8644
+ next.add(optionName);
8645
+ }
8646
+ return next;
8647
+ });
8648
+ };
8649
+ return (React.createElement("div", { className: clsx('seekora-variant-swatches', className), style: { display: 'flex', flexDirection: 'column', gap: 4, ...style } }, filtered.map((option) => {
8650
+ const isColor = isColorOption$1(option.name);
8651
+ const isExpanded = expandedOptions.has(option.name);
8652
+ const visible = isExpanded ? option.values : option.values.slice(0, maxValues);
8653
+ const overflow = option.values.length - maxValues;
8654
+ const hasOverflow = overflow > 0;
8655
+ const selectedValue = selectedValues?.[option.name];
8656
+ return (React.createElement("div", { key: option.name, className: "seekora-variant-swatch-group", style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
8657
+ visible.map((value) => {
8658
+ const color = isColor ? resolveColor$1(value, colorMap) : null;
8659
+ const isSelected = selectedValue === value;
8660
+ const isAvailable = showAvailability
8661
+ ? getAvailability$1(option.name, value, options, variants, selectedValues)
8662
+ : true;
8663
+ if (color) {
8664
+ return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--color', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), title: `${value}${!isAvailable ? ' (Unavailable)' : ''}`, style: {
8665
+ display: 'inline-block',
8666
+ width: 14,
8667
+ height: 14,
8668
+ borderRadius: '50%',
8669
+ backgroundColor: color,
8670
+ border: isSelected
8671
+ ? '2px solid var(--seekora-primary, #111827)'
8672
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
8673
+ outline: isSelected ? '2px solid var(--seekora-primary, #111827)' : 'none',
8674
+ outlineOffset: 2,
8675
+ cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
8676
+ flexShrink: 0,
8677
+ boxShadow: isSelected ? '0 0 0 1px #fff' : 'none',
8678
+ opacity: isAvailable ? 1 : 0.3,
8679
+ position: 'relative',
8680
+ }, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
8681
+ e.stopPropagation();
8682
+ e.preventDefault();
8683
+ }, onClick: (e) => {
8684
+ e.stopPropagation();
8685
+ if (isAvailable) {
8686
+ onSwatchClick?.(option.name, value);
8687
+ }
8688
+ } }, !isAvailable && (React.createElement("span", { style: {
8689
+ position: 'absolute',
8690
+ top: '50%',
8691
+ left: '-2px',
8692
+ right: '-2px',
8693
+ height: 1,
8694
+ backgroundColor: '#ef4444',
8695
+ transform: 'translateY(-50%) rotate(-45deg)',
8696
+ } }))));
8697
+ }
8698
+ return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--text', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), style: {
8699
+ display: 'inline-block',
8700
+ padding: '1px 6px',
8701
+ fontSize: '0.6875rem',
8702
+ borderRadius: 3,
8703
+ border: isSelected
8704
+ ? '1px solid var(--seekora-primary, #111827)'
8705
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
8706
+ backgroundColor: isSelected
8707
+ ? 'var(--seekora-primary, #111827)'
8708
+ : 'transparent',
8709
+ color: isSelected
8710
+ ? '#fff'
8711
+ : 'var(--seekora-text-secondary, #6b7280)',
8712
+ cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
8713
+ whiteSpace: 'nowrap',
8714
+ fontWeight: isSelected ? 600 : 400,
8715
+ opacity: isAvailable ? 1 : 0.4,
8716
+ textDecoration: !isAvailable ? 'line-through' : 'none',
8717
+ }, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
8718
+ e.stopPropagation();
8719
+ e.preventDefault();
8720
+ }, onClick: (e) => {
8721
+ e.stopPropagation();
8722
+ if (isAvailable) {
8723
+ onSwatchClick?.(option.name, value);
8724
+ }
8725
+ } }, value));
8726
+ }),
8727
+ hasOverflow && (React.createElement("button", { type: "button", className: "seekora-variant-swatch--overflow", onClick: (e) => toggleExpanded(option.name, e), style: {
8728
+ fontSize: '0.6875rem',
8729
+ color: 'var(--seekora-primary, #2563eb)',
8730
+ background: 'none',
8731
+ border: 'none',
8732
+ padding: 0,
8733
+ cursor: 'pointer',
8734
+ textDecoration: 'underline',
8735
+ fontWeight: 500,
8736
+ } }, isExpanded ? 'Show less' : `+${overflow} more`))));
8737
+ })));
8738
+ }
8739
+
8740
+ /**
8741
+ * ProductCardLayouts – internal layout sub-components for ProductCard
8742
+ *
8743
+ * Not exported from the package. Each layout renders the same product data
8744
+ * with different visual emphasis.
8745
+ */
8746
+ const imgPlaceholderStyle = {
8747
+ width: '100%',
8748
+ aspectRatio: '1',
8749
+ objectFit: 'cover',
8750
+ borderRadius: 4,
8751
+ backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
8752
+ };
8753
+ function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
8754
+ const ar = aspectRatio ? aspectRatio.replace(':', '/') : '1';
8755
+ if (images.length > 0) {
8756
+ return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: 4 } },
8757
+ React.createElement(ImageDisplay, { images: images, variant: images.length > 1 ? imageVariant : 'single', alt: title, className: "seekora-suggestions-product-card-image", style: { aspectRatio: ar }, enableZoom: enableZoom, zoomMode: zoomMode, zoomLevel: zoomLevel })));
8758
+ }
8759
+ return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
8760
+ }
8761
+ /** minimal: image, title, price (current default behavior) */
8762
+ function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
8763
+ return (React.createElement(React.Fragment, null,
8764
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8765
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8766
+ price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8767
+ product.currency ?? '$',
8768
+ price.toFixed(2)))));
8769
+ }
8770
+ /** standard: image, badges, brand, title, price + compare price, color swatches */
8771
+ function StandardLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8772
+ const cfg = displayConfig;
8773
+ return (React.createElement(React.Fragment, null,
8774
+ React.createElement("div", { style: { position: 'relative' } },
8775
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8776
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
8777
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8778
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
8779
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
8780
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8781
+ React.createElement("div", { className: "seekora-product-card__price" },
8782
+ React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
8783
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8784
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8785
+ }
8786
+ /** detailed: image, badges, brand, title, price + compare + discount, rating, all swatches, stock */
8787
+ function DetailedLayout({ images, title, price, comparePrice, brand, badges, priceRange, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8788
+ const cfg = displayConfig;
8789
+ const available = product.available;
8790
+ return (React.createElement(React.Fragment, null,
8791
+ React.createElement("div", { style: { position: 'relative' } },
8792
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8793
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
8794
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8795
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
8796
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
8797
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8798
+ cfg.showRating !== false && product.rating != null && (React.createElement(RatingDisplay, { rating: product.rating, reviewCount: product.reviewCount, variant: "compact", size: "small", showNumeric: false, showReviewCount: true, className: "seekora-product-card__rating" })),
8799
+ React.createElement("div", { className: "seekora-product-card__price" }, cfg.showPriceRange && priceRange ? (React.createElement(PriceDisplay, { priceRange: priceRange, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, style: { fontSize: '0.875rem' } })) : (React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } }))),
8800
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8801
+ cfg.showStock !== false && available != null && (React.createElement("span", { className: "seekora-product-card__stock", style: {
8802
+ fontSize: '0.75rem',
8803
+ color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
8804
+ } }, available ? 'In Stock' : 'Out of Stock')),
8805
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8806
+ }
8807
+ /** compact: smaller image, 1-line title, price */
8808
+ function CompactLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
8809
+ return (React.createElement(React.Fragment, null,
8810
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio ?? '1:1', enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8811
+ React.createElement("span", { className: "seekora-product-card__title", style: {
8812
+ fontSize: '0.8125rem',
8813
+ fontWeight: 500,
8814
+ overflow: 'hidden',
8815
+ textOverflow: 'ellipsis',
8816
+ whiteSpace: 'nowrap',
8817
+ } }, title),
8818
+ price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8819
+ product.currency ?? '$',
8820
+ price.toFixed(2)))));
8821
+ }
8822
+ /** horizontal: image left + content right (title, brand, price, swatches) */
8823
+ function HorizontalLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8824
+ const cfg = displayConfig;
8825
+ return (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
8826
+ React.createElement("div", { style: { position: 'relative', width: 80, flexShrink: 0 } },
8827
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: "1:1", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8828
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 1 })),
8829
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8830
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } },
8831
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, brand)),
8832
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8833
+ React.createElement("div", { className: "seekora-product-card__price" },
8834
+ React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
8835
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues ?? 3, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8836
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8837
+ }
8838
+
8839
+ /**
8840
+ * ProductCard – one product tile (primitive)
8841
+ *
8842
+ * Without displayConfig: renders the original minimal layout (image, title, price).
8843
+ * With displayConfig: renders layout variants (minimal, standard, detailed, compact, horizontal).
8844
+ */
8845
+ const cardStyle = {
8846
+ display: 'flex',
8847
+ flexDirection: 'column',
8848
+ gap: 8,
8849
+ padding: 8,
8850
+ cursor: 'pointer',
8851
+ border: 'none',
8852
+ borderRadius: 'var(--seekora-border-radius, 6px)',
8853
+ backgroundColor: 'transparent',
8854
+ textAlign: 'left',
8855
+ transition: 'background-color 120ms ease',
8856
+ };
8857
+ const imgStyle = {
8858
+ width: '100%',
8859
+ aspectRatio: '1',
8860
+ objectFit: 'cover',
8861
+ borderRadius: 4,
8862
+ backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
8863
+ };
8864
+ function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', displayConfig, onVariantHover, onVariantClick, selectedVariants, asLink, actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, enableImageZoom = false, imageZoomMode = 'both', imageZoomLevel = 2.5, }) {
8865
+ // Find selected variant if selections are provided
8866
+ const selectedVariant = React.useMemo(() => {
8867
+ if (!selectedVariants || !product.options || !product.variants)
8868
+ return null;
8869
+ return findVariantBySelections(product.options, product.variants, selectedVariants);
8870
+ }, [selectedVariants, product.options, product.variants]);
8871
+ // Compute effective display data (use selected variant if available, otherwise product defaults)
8872
+ const effectiveImages = React.useMemo(() => {
8873
+ // Priority: selected variant image > product images > product image
8874
+ if (selectedVariant?.image)
8875
+ return [selectedVariant.image];
8876
+ if (product.images?.length)
8877
+ return product.images;
8878
+ if (product.image ?? product.imageUrl)
8879
+ return [String(product.image ?? product.imageUrl)];
8880
+ return [];
8881
+ }, [selectedVariant, product]);
8882
+ const effectiveTitle = React.useMemo(() => {
8883
+ // Show variant title if available (e.g., "T-Shirt - Black / M")
8884
+ if (selectedVariant?.title && selectedVariant.title !== product.title) {
8885
+ return `${product.title ?? product.name ?? ''} - ${selectedVariant.title}`;
8886
+ }
8887
+ return product.title ?? product.name ?? '';
8888
+ }, [selectedVariant, product]);
8889
+ const effectivePrice = React.useMemo(() => {
8890
+ const variantPrice = selectedVariant?.price;
8891
+ if (variantPrice != null)
8892
+ return typeof variantPrice === 'number' ? variantPrice : Number(variantPrice);
8893
+ const productPrice = product.price;
8894
+ return productPrice != null ? (typeof productPrice === 'number' ? productPrice : Number(productPrice)) : null;
8895
+ }, [selectedVariant, product.price]);
8896
+ const effectiveComparePrice = React.useMemo(() => {
8897
+ const variantCompare = selectedVariant?.comparePrice;
8898
+ if (variantCompare != null)
8899
+ return typeof variantCompare === 'number' ? variantCompare : Number(variantCompare);
8900
+ const productCompare = product.original_price ?? product.compare_at_price;
8901
+ return productCompare != null ? (typeof productCompare === 'number' ? productCompare : Number(productCompare)) : null;
8902
+ }, [selectedVariant, product]);
8903
+ // Legacy vars for backwards compat
8904
+ const images = effectiveImages;
8905
+ const title = effectiveTitle;
8906
+ const price = effectivePrice;
8907
+ // If no displayConfig, render original minimal layout (backwards compat)
8908
+ if (!displayConfig) {
8909
+ return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
8910
+ e.preventDefault();
8911
+ onSelect();
8912
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8913
+ images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
8914
+ React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8915
+ price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8916
+ product.currency ?? '$',
8917
+ price.toFixed(2))) : null));
8918
+ }
8919
+ // Enhanced layout with displayConfig
8920
+ const layoutStyle = displayConfig.style ?? 'minimal';
8921
+ const comparePrice = effectiveComparePrice;
8922
+ const brand = product.brand ?? null;
8923
+ const options = product.options ?? [];
8924
+ const variants = product.variants ?? [];
8925
+ const badges = React.useMemo(() => {
8926
+ if (displayConfig.showBadges === false)
8927
+ return [];
8928
+ if (displayConfig.badgeExtractor)
8929
+ return displayConfig.badgeExtractor(product.tags, product);
8930
+ return extractBadges(product.tags, product);
8931
+ }, [displayConfig, product]);
8932
+ const priceRange = React.useMemo(() => {
8933
+ if (!displayConfig.showPriceRange)
8934
+ return null;
8935
+ return getPriceRange(product.variants);
8936
+ }, [displayConfig.showPriceRange, product.variants]);
8937
+ const layoutProps = {
8938
+ product,
8939
+ images,
8940
+ title,
8941
+ price,
8942
+ comparePrice,
8943
+ brand,
8944
+ badges,
8945
+ priceRange,
8946
+ options,
8947
+ variants,
8948
+ displayConfig,
8949
+ imageVariant,
8950
+ onVariantHover,
8951
+ onVariantClick,
8952
+ selectedVariants,
8953
+ actionButtons,
8954
+ actionButtonsPosition,
8955
+ showActionLabels,
8956
+ enableImageZoom,
8957
+ imageZoomMode,
8958
+ imageZoomLevel,
8959
+ };
8960
+ const layoutMap = {
8961
+ minimal: MinimalLayout,
8962
+ standard: StandardLayout,
8963
+ detailed: DetailedLayout,
8964
+ compact: CompactLayout,
8965
+ horizontal: HorizontalLayout,
8966
+ };
8967
+ const LayoutComponent = layoutMap[layoutStyle] ?? MinimalLayout;
8968
+ const rootClassName = clsx('seekora-product-card', `seekora-product-card--${layoutStyle}`, 'seekora-suggestions-product-card', className);
8969
+ const rootStyle = {
8970
+ ...cardStyle,
8971
+ ...(layoutStyle === 'horizontal' ? { flexDirection: 'row' } : {}),
8972
+ ...style,
8973
+ };
8974
+ if (asLink && product.url) {
8975
+ return (React.createElement("a", { href: product.url, className: rootClassName, style: { ...rootStyle, textDecoration: 'none', color: 'inherit' }, onClick: (e) => {
8976
+ e.preventDefault();
8977
+ onSelect();
8978
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8979
+ React.createElement(LayoutComponent, { ...layoutProps })));
8980
+ }
8981
+ return (React.createElement("button", { type: "button", className: rootClassName, style: rootStyle, onMouseDown: (e) => {
8982
+ e.preventDefault();
8983
+ onSelect();
8984
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8985
+ React.createElement(LayoutComponent, { ...layoutProps })));
8986
+ }
8987
+
8988
+ /**
8989
+ * ProductGrid – grid of product cards from context (primitive)
8990
+ *
8991
+ * Uses trendingProducts or active tab products; each click calls context selectProduct.
8992
+ */
8993
+ function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, displayConfig, onVariantHover, }) {
8994
+ const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
8995
+ const products = React.useMemo(() => {
8996
+ if (source === 'trending')
8997
+ return trendingProducts;
8998
+ const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
8999
+ return tab?.products ?? [];
9000
+ }, [source, activeTabId, trendingProducts, filteredTabs]);
9001
+ const items = products.slice(0, maxItems);
9002
+ const navigableItems = getAllNavigableItems();
9003
+ const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
9004
+ if (items.length === 0)
9005
+ return null;
9006
+ const gridStyle = {
9007
+ display: 'grid',
9008
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
9009
+ gap: 12,
9010
+ padding: 12,
9011
+ };
9012
+ return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
9013
+ React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
9014
+ const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
9015
+ const section = source === 'trending' ? 'products' : 'filtered_tab';
9016
+ const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
9017
+ return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId), displayConfig: displayConfig, onVariantHover: onVariantHover }));
9018
+ }))));
9019
+ }
9020
+
9021
+ /**
9022
+ * CategoriesTabs – horizontal tabs (e.g. filtered tabs) (primitive)
9023
+ *
9024
+ * Active tab from context; on select updates context and tracks analytics.
9025
+ */
9026
+ function CategoriesTabs({ className, style, tabClassName }) {
9027
+ const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
9028
+ if (filteredTabs.length === 0)
9029
+ return null;
9030
+ return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
9031
+ display: 'flex',
9032
+ gap: 4,
9033
+ padding: '8px 12px',
9034
+ borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
9035
+ overflowX: 'auto',
9036
+ ...style,
9037
+ }, role: "tablist" }, filteredTabs.map((tab) => {
9038
+ const isActive = activeTabId === tab.id;
9039
+ return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
9040
+ padding: '8px 12px',
9041
+ border: 'none',
9042
+ borderRadius: 'var(--seekora-border-radius, 6px)',
9043
+ backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
9044
+ color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
9045
+ cursor: 'pointer',
9046
+ fontSize: '0.875rem',
9047
+ fontWeight: isActive ? 600 : 400,
9048
+ whiteSpace: 'nowrap',
9049
+ transition: 'background-color 120ms ease',
9050
+ }, onClick: () => setActiveTab(tab) }, tab.label));
9051
+ })));
9052
+ }
9053
+
9054
+ /**
9055
+ * RecentSearchesList – list of recent queries (primitive)
9056
+ *
9057
+ * Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
9058
+ */
9059
+ const itemStyle$1 = {
9060
+ padding: '10px 12px',
9061
+ cursor: 'pointer',
9062
+ border: 'none',
9063
+ width: '100%',
9064
+ textAlign: 'left',
9065
+ fontSize: 'inherit',
9066
+ fontFamily: 'inherit',
9067
+ backgroundColor: 'transparent',
9068
+ color: 'var(--seekora-text-primary, #111827)',
9069
+ transition: 'background-color 120ms ease',
9070
+ };
9071
+ function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
9072
+ const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
9073
+ const items = recentSearches.slice(0, maxItems);
9074
+ if (items.length === 0)
9075
+ return null;
9076
+ return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
9077
+ title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
9078
+ React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
9079
+ const onSelect = () => selectRecentSearch(search);
9080
+ if (renderItem) {
9081
+ return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
9082
+ }
9083
+ return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
9084
+ React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
9085
+ e.preventDefault();
9086
+ onSelect();
9087
+ } }, search.query)));
9088
+ }))));
9089
+ }
9090
+
9091
+ /**
9092
+ * TrendingList – list of trending searches (primitive)
9093
+ *
9094
+ * Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
9095
+ */
9096
+ const itemStyle = {
9097
+ padding: '10px 12px',
9098
+ cursor: 'pointer',
9099
+ border: 'none',
9100
+ width: '100%',
9101
+ textAlign: 'left',
9102
+ fontSize: 'inherit',
9103
+ fontFamily: 'inherit',
9104
+ backgroundColor: 'transparent',
9105
+ color: 'var(--seekora-text-primary, #111827)',
9106
+ transition: 'background-color 120ms ease',
9107
+ };
9108
+ function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
9109
+ const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
9110
+ const items = trendingSearches.slice(0, maxItems);
9111
+ if (items.length === 0)
9112
+ return null;
9113
+ return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
9114
+ title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
9115
+ React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
9116
+ const onSelect = () => selectTrendingSearch(trending, i);
9117
+ if (renderItem) {
9118
+ return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
9119
+ }
9120
+ return (React.createElement("li", { key: `${trending.query}-${i}` },
9121
+ React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
9122
+ e.preventDefault();
9123
+ onSelect();
9124
+ } },
9125
+ trending.query,
9126
+ trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
9127
+ }))));
9128
+ }
9129
+
9130
+ /**
9131
+ * SuggestionsLoading – loading indicator (primitive)
9132
+ */
9133
+ function SuggestionsLoading({ className, style, text = 'Loading...' }) {
9134
+ const { loading } = useSuggestionsContext();
9135
+ if (!loading)
9136
+ return null;
9137
+ return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
9138
+ padding: 16,
9139
+ color: 'var(--seekora-text-secondary, #6b7280)',
9140
+ fontSize: '0.875rem',
9141
+ ...style,
9142
+ } }, text));
9143
+ }
9144
+
9145
+ /**
9146
+ * SuggestionsError – error message (primitive)
9147
+ */
9148
+ function SuggestionsError({ className, style, render }) {
9149
+ const { error } = useSuggestionsContext();
9150
+ if (!error)
9151
+ return null;
9152
+ if (render)
9153
+ return React.createElement(React.Fragment, null, render(error));
9154
+ return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
9155
+ padding: 16,
9156
+ color: 'var(--seekora-error, #dc2626)',
9157
+ fontSize: '0.875rem',
9158
+ ...style,
9159
+ } }, error.message));
9160
+ }
9161
+
9162
+ /**
9163
+ * SuggestionsDropdownComposition – reference composition
9164
+ *
9165
+ * Example layout built from primitives: SearchInput + DropdownPanel containing
9166
+ * RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
9167
+ * Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
9168
+ * with your own arrangement of the same primitives.
9169
+ */
9170
+ function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
9171
+ return (React.createElement(SuggestionsProvider, { ...providerProps },
9172
+ React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
9173
+ React.createElement(SearchInput, { placeholder: placeholder }),
9174
+ React.createElement(DropdownPanel, null,
9175
+ React.createElement(SuggestionsError, null),
9176
+ showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
9177
+ React.createElement(SuggestionList, null),
9178
+ showTabs ? React.createElement(CategoriesTabs, null) : null,
9179
+ showProducts ? React.createElement(ProductGrid, null) : null,
9180
+ showTrending ? React.createElement(TrendingList, null) : null))));
9181
+ }
9182
+
9183
+ /**
9184
+ * VariantSelector – full variant selector for product detail pages
9185
+ *
9186
+ * Three render modes per option:
9187
+ * - swatch: color circles (auto-detected for "Color" option or when colorMap provided)
9188
+ * - button: rectangular buttons (default for most options)
9189
+ * - dropdown: <select> for options with many values
9190
+ */
9191
+ const COLOR_NAMES = {
9192
+ black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
9193
+ green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
9194
+ pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
9195
+ navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
9196
+ teal: '#0d9488', coral: '#ff7f50', maroon: '#800000', charcoal: '#36454f',
9197
+ sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
9198
+ plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
9199
+ silver: '#c0c0c0', rose: '#ff007f', raven: '#0a0a0a', natural: '#f5f0e1',
9200
+ bone: '#e3dac9', sand: '#c2b280', olive: '#808000', khaki: '#c3b091',
9201
+ burgundy: '#800020', wine: '#722f37', mauve: '#e0b0ff', tan: '#d2b48c',
9202
+ };
9203
+ const isColorOption = (name) => {
9204
+ const lower = name.toLowerCase();
9205
+ return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
9206
+ };
9207
+ const resolveColor = (value, colorMap) => {
9208
+ if (colorMap?.[value])
9209
+ return colorMap[value];
9210
+ const lower = value.toLowerCase();
9211
+ if (colorMap?.[lower])
9212
+ return colorMap[lower];
9213
+ return COLOR_NAMES[lower] ?? null;
9214
+ };
9215
+ const getAvailability = (optionName, value, options, variants, selections) => {
9216
+ const optionIndex = options.findIndex((o) => o.name === optionName);
9217
+ if (optionIndex === -1)
9218
+ return true;
9219
+ const optionKey = `option${optionIndex + 1}`;
9220
+ return variants.some((variant) => {
9221
+ if (variant[optionKey] !== value)
9222
+ return false;
9223
+ if (variant.available === false)
9224
+ return false;
9225
+ for (const [selName, selValue] of Object.entries(selections)) {
9226
+ if (selName === optionName)
9227
+ continue;
9228
+ const selIdx = options.findIndex((o) => o.name === selName);
9229
+ if (selIdx === -1)
9230
+ continue;
9231
+ const selKey = `option${selIdx + 1}`;
9232
+ if (variant[selKey] !== selValue)
9233
+ return false;
9234
+ }
9235
+ return true;
9236
+ });
9237
+ };
9238
+ function VariantSelector({ options, variants, selections, onSelectionChange, optionRenderModes, dropdownThreshold = 8, colorMap, showAvailability = true, selectedVariant: _selectedVariant, className, style, }) {
9239
+ if (!options || options.length === 0)
9240
+ return null;
9241
+ const getRenderMode = (option) => {
9242
+ if (optionRenderModes?.[option.name])
9243
+ return optionRenderModes[option.name];
9244
+ if (isColorOption(option.name))
9245
+ return 'swatch';
9246
+ if (option.values.length > dropdownThreshold)
9247
+ return 'dropdown';
9248
+ return 'button';
9249
+ };
9250
+ return (React.createElement("div", { className: clsx('seekora-variant-selector', className), style: { display: 'flex', flexDirection: 'column', gap: 16, ...style } }, options.map((option) => {
9251
+ const mode = getRenderMode(option);
9252
+ const selected = selections[option.name];
9253
+ return (React.createElement("div", { key: option.name, className: "seekora-variant-option-group" },
9254
+ React.createElement("label", { className: "seekora-variant-option-label", style: {
9255
+ display: 'block',
9256
+ fontSize: '0.875rem',
9257
+ fontWeight: 600,
9258
+ marginBottom: 8,
9259
+ color: 'var(--seekora-text-primary, #111827)',
9260
+ } },
9261
+ option.name,
9262
+ selected && (React.createElement("span", { style: { fontWeight: 400, marginLeft: 6, color: 'var(--seekora-text-secondary, #6b7280)' } }, selected))),
9263
+ mode === 'dropdown' ? (React.createElement("select", { className: "seekora-variant-dropdown", value: selected ?? '', onChange: (e) => {
9264
+ e.stopPropagation();
9265
+ onSelectionChange(option.name, e.target.value);
9266
+ }, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), style: {
9267
+ padding: '8px 12px',
9268
+ fontSize: '0.875rem',
9269
+ borderRadius: 6,
9270
+ border: '1px solid var(--seekora-border-color, #e5e7eb)',
9271
+ backgroundColor: 'var(--seekora-bg-surface, #fff)',
9272
+ color: 'var(--seekora-text-primary, #111827)',
9273
+ cursor: 'pointer',
9274
+ minWidth: 120,
9275
+ } },
9276
+ React.createElement("option", { value: "" },
9277
+ "Select ",
9278
+ option.name),
9279
+ option.values.map((value) => {
9280
+ const available = showAvailability
9281
+ ? getAvailability(option.name, value, options, variants, selections)
9282
+ : true;
9283
+ return (React.createElement("option", { key: value, value: value, disabled: !available },
9284
+ value,
9285
+ !available ? ' (Unavailable)' : ''));
9286
+ }))) : (React.createElement("div", { className: "seekora-variant-buttons", style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, option.values.map((value) => {
9287
+ const isActive = selected === value;
9288
+ const available = showAvailability
9289
+ ? getAvailability(option.name, value, options, variants, selections)
9290
+ : true;
9291
+ const color = mode === 'swatch' ? resolveColor(value, colorMap) : null;
9292
+ if (mode === 'swatch' && color) {
9293
+ return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-color-swatch', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), title: value, onMouseDown: (e) => {
9294
+ e.stopPropagation();
9295
+ e.preventDefault();
9296
+ }, onClick: (e) => {
9297
+ e.stopPropagation();
9298
+ onSelectionChange(option.name, value);
9299
+ }, disabled: !available, style: {
9300
+ width: 32,
9301
+ height: 32,
9302
+ borderRadius: '50%',
9303
+ backgroundColor: color,
9304
+ border: isActive
9305
+ ? '2px solid var(--seekora-primary, #111827)'
9306
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
9307
+ outline: isActive ? '2px solid var(--seekora-primary, #111827)' : 'none',
9308
+ outlineOffset: 2,
9309
+ cursor: available ? 'pointer' : 'not-allowed',
9310
+ opacity: available ? 1 : 0.4,
9311
+ position: 'relative',
9312
+ padding: 0,
9313
+ }, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }));
9314
+ }
9315
+ return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-button', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), onMouseDown: (e) => {
9316
+ e.stopPropagation();
9317
+ e.preventDefault();
9318
+ }, onClick: (e) => {
9319
+ e.stopPropagation();
9320
+ onSelectionChange(option.name, value);
9321
+ }, disabled: !available, style: {
9322
+ padding: '6px 16px',
9323
+ fontSize: '0.875rem',
9324
+ borderRadius: 6,
9325
+ border: isActive
9326
+ ? '2px solid var(--seekora-primary, #111827)'
9327
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
9328
+ backgroundColor: isActive
9329
+ ? 'var(--seekora-primary, #111827)'
9330
+ : 'var(--seekora-bg-surface, #fff)',
9331
+ color: isActive
9332
+ ? '#fff'
9333
+ : 'var(--seekora-text-primary, #111827)',
9334
+ cursor: available ? 'pointer' : 'not-allowed',
9335
+ opacity: available ? 1 : 0.5,
9336
+ textDecoration: !available ? 'line-through' : 'none',
9337
+ fontWeight: isActive ? 600 : 400,
9338
+ transition: 'all 120ms ease',
9339
+ }, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }, value));
9340
+ })))));
9341
+ })));
9342
+ }
9343
+
9344
+ /**
9345
+ * withAnalytics – HOC that wraps any React component to inject analytics tracking
9346
+ */
9347
+ function withAnalytics(Component, config = {}) {
9348
+ const { trackClick = true, trackImpression = false, clickEventName = 'product.click', getProductFromProps = () => null, getPositionFromProps = () => 0, } = config;
9349
+ const displayName = Component.displayName || Component.name || 'Component';
9350
+ const Wrapped = React.forwardRef((props, ref) => {
9351
+ const { seekoraClient, seekoraContext, ...rest } = props;
9352
+ const containerRef = React.useRef(null);
9353
+ const impressionTrackedRef = React.useRef(false);
9354
+ const product = getProductFromProps(rest);
9355
+ const position = getPositionFromProps(rest);
9356
+ const sendEvent = React.useCallback(async (eventName, metadata) => {
9357
+ if (!seekoraClient)
9358
+ return;
9359
+ try {
9360
+ await seekoraClient.trackEvent?.({
9361
+ event_name: eventName,
9362
+ metadata: {
9363
+ product_id: product?.id || product?.objectID,
9364
+ product_title: product?.title || product?.name,
9365
+ product_price: product?.price,
9366
+ position,
9367
+ timestamp: Date.now(),
9368
+ source: 'with_analytics_hoc',
9369
+ ...metadata,
9370
+ },
9371
+ }, seekoraContext);
9372
+ }
9373
+ catch (error) {
9374
+ log.warn(`withAnalytics: Failed to track ${eventName}`, { error });
9375
+ }
9376
+ }, [seekoraClient, seekoraContext, product, position]);
9377
+ const handleClick = React.useCallback(() => {
9378
+ if (!trackClick || !product)
9379
+ return;
9380
+ sendEvent(clickEventName, {});
9381
+ if (seekoraClient?.trackClick && product) {
9382
+ Promise.resolve(seekoraClient.trackClick(product.id || product.objectID || '', position + 1, seekoraContext)).catch(() => { });
9383
+ }
9384
+ }, [trackClick, product, sendEvent, seekoraClient, seekoraContext, position]);
9385
+ // Impression tracking
9386
+ React.useEffect(() => {
9387
+ if (!trackImpression || !seekoraClient || !containerRef.current || impressionTrackedRef.current)
9388
+ return;
9389
+ if (typeof IntersectionObserver === 'undefined')
9390
+ return;
9391
+ const observer = new IntersectionObserver((entries) => {
9392
+ for (const entry of entries) {
9393
+ if (entry.isIntersecting && !impressionTrackedRef.current) {
9394
+ impressionTrackedRef.current = true;
9395
+ sendEvent('product.impression', {});
9396
+ observer.disconnect();
9397
+ }
9398
+ }
9399
+ }, { threshold: 0.5 });
9400
+ observer.observe(containerRef.current);
9401
+ return () => observer.disconnect();
9402
+ }, [trackImpression, seekoraClient, sendEvent]);
9403
+ return (React.createElement("div", { ref: (node) => {
9404
+ containerRef.current = node;
9405
+ if (typeof ref === 'function')
9406
+ ref(node);
9407
+ else if (ref)
9408
+ ref.current = node;
9409
+ }, onClick: handleClick, style: { display: 'contents' } },
9410
+ React.createElement(Component, { ...rest })));
9411
+ });
9412
+ Wrapped.displayName = `withAnalytics(${displayName})`;
9413
+ return Wrapped;
9414
+ }
9415
+
9416
+ /**
9417
+ * AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
9418
+ *
9419
+ * Installs a delegated click listener on a container. Any descendant with
9420
+ * data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
9421
+ * automatically fires analytics events without React wiring.
9422
+ */
9423
+ const AnalyticsContext = React.createContext(null);
9424
+ const useAnalyticsProvider = () => React.useContext(AnalyticsContext);
9425
+ function AnalyticsProvider({ client, context, children }) {
9426
+ const containerRef = React.useRef(null);
9427
+ const sendEvent = React.useCallback(async (eventName, metadata) => {
9428
+ try {
9429
+ await client.trackEvent?.({
9430
+ event_name: eventName,
9431
+ metadata: {
9432
+ timestamp: Date.now(),
9433
+ source: 'analytics_provider_delegation',
9434
+ ...metadata,
9435
+ },
9436
+ }, context);
9437
+ log.verbose(`AnalyticsProvider: ${eventName}`, metadata);
9438
+ }
9439
+ catch (error) {
9440
+ log.warn(`AnalyticsProvider: Failed to track ${eventName}`, { error });
9441
+ }
9442
+ }, [client, context]);
9443
+ React.useEffect(() => {
9444
+ const container = containerRef.current;
9445
+ if (!container)
9446
+ return;
9447
+ const handleClick = (e) => {
9448
+ const target = e.target;
9449
+ // Walk up from target to find element with data-seekora-track
9450
+ const tracked = target.closest('[data-seekora-track]');
9451
+ if (!tracked)
9452
+ return;
9453
+ const trackType = tracked.getAttribute('data-seekora-track');
9454
+ const productId = tracked.getAttribute('data-seekora-product-id');
9455
+ const position = tracked.getAttribute('data-seekora-position');
9456
+ const section = tracked.getAttribute('data-seekora-section');
9457
+ const variantSku = tracked.getAttribute('data-seekora-variant-sku');
9458
+ const variantOption = tracked.getAttribute('data-seekora-variant-option');
9459
+ const variantValue = tracked.getAttribute('data-seekora-variant-value');
9460
+ const metadata = {};
9461
+ if (productId)
9462
+ metadata.product_id = productId;
9463
+ if (position)
9464
+ metadata.position = parseInt(position, 10);
9465
+ if (section)
9466
+ metadata.section = section;
9467
+ switch (trackType) {
9468
+ case 'click':
9469
+ sendEvent('product.click', metadata);
9470
+ if (client?.trackClick && productId) {
9471
+ const pos = position ? parseInt(position, 10) + 1 : 1;
9472
+ Promise.resolve(client.trackClick(productId, pos, context)).catch(() => { });
9473
+ }
9474
+ break;
9475
+ case 'add-to-cart':
9476
+ if (variantSku)
9477
+ metadata.variant_sku = variantSku;
9478
+ sendEvent('product.add_to_cart', metadata);
9479
+ break;
9480
+ case 'variant-select':
9481
+ if (variantOption)
9482
+ metadata.option_name = variantOption;
9483
+ if (variantValue)
9484
+ metadata.option_value = variantValue;
9485
+ sendEvent('product.variant_select', metadata);
9486
+ break;
9487
+ default:
9488
+ // Custom track type — use it as event name
9489
+ if (trackType) {
9490
+ sendEvent(trackType, metadata);
9491
+ }
9492
+ break;
9493
+ }
9494
+ };
9495
+ container.addEventListener('click', handleClick);
9496
+ return () => container.removeEventListener('click', handleClick);
9497
+ }, [client, context, sendEvent]);
9498
+ return (React.createElement(AnalyticsContext.Provider, { value: { client, context } },
9499
+ React.createElement("div", { ref: containerRef, style: { display: 'contents' } }, children)));
9500
+ }
9501
+
9502
+ /**
9503
+ * SectionSearchContext – preset query/filter section state
9504
+ *
9505
+ * For menus, sidebar, front-page blocks. Independent of main search state.
9506
+ */
9507
+ const SectionSearchContext = React.createContext(null);
9508
+ function useSectionSearchContext() {
9509
+ const context = React.useContext(SectionSearchContext);
9510
+ if (!context) {
9511
+ const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
9512
+ log.error('SectionSearchContext: not available', { error: error.message });
9513
+ throw error;
9514
+ }
9515
+ return context;
9516
+ }
9517
+
9518
+ /**
9519
+ * SectionSearchProvider – preset query + filter section
9520
+ *
9521
+ * Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
9522
+ * query/filters change. Does not use global SearchStateManager. Use for menus,
9523
+ * sidebar, front-page blocks (e.g. "New arrivals", "On sale").
9524
+ */
9525
+ function extractItems(response) {
9526
+ if (!response)
9527
+ return [];
9528
+ if (Array.isArray(response.results))
9529
+ return response.results;
9530
+ if (Array.isArray(response.hits))
9531
+ return response.hits;
9532
+ const data = response.data;
9533
+ if (data && Array.isArray(data.results))
9534
+ return data.results;
9535
+ if (data && Array.isArray(data.data))
9536
+ return data.data;
9537
+ return [];
9538
+ }
9539
+ function extractTotal(response) {
9540
+ if (!response)
9541
+ return 0;
9542
+ const n = response.totalResults ?? response.total ?? response.total_results;
9543
+ if (typeof n === 'number')
9544
+ return n;
9545
+ const data = response.data;
9546
+ if (data?.total_results != null)
9547
+ return Number(data.total_results);
9548
+ if (data?.data?.total_results != null)
9549
+ return Number(data.data.total_results);
9550
+ return 0;
9551
+ }
9552
+ function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
9553
+ const { client } = useSearchContext();
9554
+ const [items, setItems] = React.useState([]);
9555
+ const [loading, setLoading] = React.useState(true);
9556
+ const [error, setError] = React.useState(null);
9557
+ const [totalCount, setTotalCount] = React.useState(0);
9558
+ React.useEffect(() => {
9559
+ if (!enabled || !client?.search) {
9560
+ setItems([]);
9561
+ setLoading(false);
9562
+ setError(null);
9563
+ setTotalCount(0);
9564
+ return;
9565
+ }
9566
+ let cancelled = false;
9567
+ setLoading(true);
9568
+ setError(null);
9569
+ const options = {
9570
+ per_page: maxItems,
9571
+ page: 1,
9572
+ };
9573
+ if (sortBy)
9574
+ options.sort_by = sortBy;
9575
+ if (refinements.length > 0) {
9576
+ options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
8127
9577
  }
8128
- this.cache.set(key, {
8129
- data,
8130
- timestamp: Date.now(),
8131
- ttl: ttlMs ?? this.defaultTtl,
9578
+ client
9579
+ .search(query, options)
9580
+ .then((response) => {
9581
+ if (cancelled)
9582
+ return;
9583
+ setItems(extractItems(response));
9584
+ setTotalCount(extractTotal(response));
9585
+ setLoading(false);
9586
+ })
9587
+ .catch((err) => {
9588
+ if (cancelled)
9589
+ return;
9590
+ setError(err instanceof Error ? err : new Error(String(err)));
9591
+ setItems([]);
9592
+ setLoading(false);
8132
9593
  });
8133
- }
8134
- /**
8135
- * Check if key exists and is valid
8136
- */
8137
- has(key) {
8138
- return this.get(key) !== null;
8139
- }
8140
- /**
8141
- * Clear all cached entries
8142
- */
8143
- clear() {
8144
- this.cache.clear();
8145
- }
8146
- /**
8147
- * Clear expired entries
8148
- */
8149
- cleanup() {
8150
- const now = Date.now();
8151
- for (const [key, entry] of this.cache.entries()) {
8152
- if (now - entry.timestamp > entry.ttl) {
8153
- this.cache.delete(key);
8154
- }
8155
- }
8156
- }
8157
- /**
8158
- * Get cache statistics
8159
- */
8160
- getStats() {
8161
- return {
8162
- size: this.cache.size,
8163
- maxSize: this.maxSize,
9594
+ return () => {
9595
+ cancelled = true;
8164
9596
  };
8165
- }
9597
+ }, [client, enabled, query, maxItems, sortBy, refinements]);
9598
+ const trackClick = React.useCallback((item, position) => {
9599
+ if (!client?.trackEvent)
9600
+ return;
9601
+ const id = item?.id ?? item?.objectID;
9602
+ client.trackEvent({
9603
+ event_name: 'section_result_click',
9604
+ clicked_item_id: id,
9605
+ position,
9606
+ section: sectionId,
9607
+ metadata: { section_id: sectionId },
9608
+ }, undefined);
9609
+ }, [client, sectionId]);
9610
+ const value = React.useMemo(() => ({
9611
+ items,
9612
+ loading,
9613
+ error,
9614
+ totalCount,
9615
+ sectionId,
9616
+ trackClick,
9617
+ }), [items, loading, error, totalCount, sectionId, trackClick]);
9618
+ return React.createElement(SectionSearchContext.Provider, { value: value }, children);
8166
9619
  }
8167
- // Global cache instance for suggestions (shared across components)
8168
- let globalSuggestionsCache = null;
9620
+
8169
9621
  /**
8170
- * Get the global suggestions cache instance
9622
+ * SectionLoading loading state for section (primitive)
8171
9623
  */
8172
- const getSuggestionsCache = (options) => {
8173
- if (!globalSuggestionsCache) {
8174
- globalSuggestionsCache = new SuggestionsCache(options);
8175
- }
8176
- return globalSuggestionsCache;
8177
- };
9624
+ function SectionLoading({ className, style, text = 'Loading...' }) {
9625
+ const { loading } = useSectionSearchContext();
9626
+ if (!loading)
9627
+ return null;
9628
+ return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
9629
+ }
9630
+
8178
9631
  /**
8179
- * Create a new cache instance (for isolated caching per component)
9632
+ * SectionError error state for section (primitive)
8180
9633
  */
8181
- const createSuggestionsCache = (options) => {
8182
- return new SuggestionsCache(options);
8183
- };
9634
+ function SectionError({ className, style, render }) {
9635
+ const { error } = useSectionSearchContext();
9636
+ if (!error)
9637
+ return null;
9638
+ if (render)
9639
+ return React.createElement(React.Fragment, null, render(error));
9640
+ return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
9641
+ }
9642
+
8184
9643
  /**
8185
- * Clear the global cache
9644
+ * SectionItemGrid generic grid of items from SectionSearchProvider (primitive)
8186
9645
  */
8187
- const clearSuggestionsCache = () => {
8188
- globalSuggestionsCache?.clear();
8189
- };
9646
+ function SectionItemGrid({ columns = 4, maxItems = 12, className, style, showLoadingState = false, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
9647
+ const { items, loading, error, trackClick } = useSectionSearchContext();
9648
+ if (loading && items.length === 0 && showLoadingState)
9649
+ return React.createElement(SectionLoading, { className: className, style: style });
9650
+ if (error)
9651
+ return React.createElement(SectionError, { className: className, style: style });
9652
+ // When loading with previous items, show them (no loading screen)
9653
+ return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
9654
+ }
9655
+
9656
+ /**
9657
+ * ProductGallery – product detail image gallery (primitive)
9658
+ *
9659
+ * Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
9660
+ * For use on individual product page.
9661
+ */
9662
+ function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
9663
+ return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
9664
+ React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
9665
+ }
9666
+
9667
+ /**
9668
+ * ProductInfo – product detail block (primitive)
9669
+ *
9670
+ * Title, description, price, optional variant selector and CTA. Minimal layout;
9671
+ * override with className/style. For use on individual product page.
9672
+ *
9673
+ * When options/variants/selections/onSelectionChange are provided and
9674
+ * renderVariantSelector is NOT provided, renders the built-in VariantSelector.
9675
+ * renderVariantSelector still takes priority as an override.
9676
+ */
9677
+ function ProductInfo({ title, description, price, currency = '$', comparePrice, brand, available, badges, options, variants, selectedVariant: _selectedVariant, selections, onSelectionChange, renderVariantSelector, renderCTA, className, style, }) {
9678
+ const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
9679
+ const comparePriceNum = comparePrice != null ? (typeof comparePrice === 'number' ? comparePrice : parseFloat(String(comparePrice))) : null;
9680
+ const showBuiltInVariantSelector = !renderVariantSelector && options && options.length > 0 && variants && selections && onSelectionChange;
9681
+ return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
9682
+ badges && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "inline" })),
9683
+ brand && (React.createElement("span", { className: "seekora-product-info-brand", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
9684
+ React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
9685
+ (priceNum != null && !Number.isNaN(priceNum)) && (React.createElement("div", { className: "seekora-product-info-price" },
9686
+ React.createElement(PriceDisplay, { price: priceNum, comparePrice: comparePriceNum ?? undefined, currency: currency, style: { fontSize: '1.125rem' } }))),
9687
+ available != null && (React.createElement("span", { className: "seekora-product-info-availability", style: {
9688
+ fontSize: '0.875rem',
9689
+ color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
9690
+ } }, available ? 'In Stock' : 'Out of Stock')),
9691
+ description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
9692
+ renderVariantSelector ? renderVariantSelector() : null,
9693
+ showBuiltInVariantSelector && (React.createElement(VariantSelector, { options: options, variants: variants, selections: selections, onSelectionChange: onSelectionChange, showAvailability: true })),
9694
+ renderCTA?.()));
9695
+ }
9696
+
9697
+ /**
9698
+ * ProductRecommendations – related / frequently bought (primitive)
9699
+ *
9700
+ * Renders a section of recommended items (generic ItemGrid or product list).
9701
+ * Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
9702
+ * For use on individual product page.
9703
+ */
9704
+ function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
9705
+ if (!items?.length)
9706
+ return null;
9707
+ return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
9708
+ React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
9709
+ React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
9710
+ }
8190
9711
 
8191
9712
  /**
8192
9713
  * Responsive Utilities and Breakpoints
@@ -8865,7 +10386,7 @@ const RatingStars = ({ rating, count }) => {
8865
10386
  ")"))));
8866
10387
  };
8867
10388
  const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
8868
- const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
10389
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
8869
10390
  // Inject global responsive styles
8870
10391
  useInjectResponsiveStyles();
8871
10392
  // Responsive state
@@ -8962,7 +10483,7 @@ const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
8962
10483
  }
8963
10484
  `),
8964
10485
  header,
8965
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10486
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
8966
10487
  React.createElement("div", { style: styles.spinner }),
8967
10488
  React.createElement("span", null, "Searching...")))) : (React.createElement(React.Fragment, null,
8968
10489
  filteredTabs.length > 0 && (React.createElement("div", { style: {
@@ -9296,7 +10817,7 @@ const TrendingIcon$2 = () => (React.createElement("svg", { width: "20", height:
9296
10817
  const ArrowIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", width: "20", height: "20" },
9297
10818
  React.createElement("path", { d: "M9 5l-4 4 4 4", stroke: "currentColor", strokeWidth: "2", fill: "none", transform: "rotate(-90 12 12)" })));
9298
10819
  const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
9299
- const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
10820
+ const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
9300
10821
  // Inject global responsive styles
9301
10822
  useInjectResponsiveStyles();
9302
10823
  // Responsive state
@@ -9415,7 +10936,7 @@ const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
9415
10936
  }
9416
10937
  `),
9417
10938
  header,
9418
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10939
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
9419
10940
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
9420
10941
  React.createElement("ul", { style: styles.list },
9421
10942
  showRecent && (React.createElement(React.Fragment, null,
@@ -9750,7 +11271,7 @@ const ImageIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill
9750
11271
  React.createElement("polyline", { points: "21 15 16 10 5 21" })));
9751
11272
  const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref) {
9752
11273
  const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showSaveButton = true, activeCategory: initialActiveCategory, activeTab, // From BaseDropdownProps
9753
- showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
11274
+ showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
9754
11275
  // Inject global responsive styles
9755
11276
  useInjectResponsiveStyles();
9756
11277
  // Responsive state
@@ -9844,7 +11365,7 @@ const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref
9844
11365
  "(",
9845
11366
  cat.count,
9846
11367
  ")"))))))))),
9847
- React.createElement("div", { style: { ...styles.content, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11368
+ React.createElement("div", { style: { ...styles.content, maxHeight } }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
9848
11369
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
9849
11370
  processedSuggestions.length > 0 && (React.createElement("div", { style: styles.suggestionsRow }, processedSuggestions.slice(0, 8).map((s, idx) => (React.createElement("button", { key: s.id || idx, "data-index": idx, style: mergeStyles(styles.suggestionPill, activeIndex === idx ? styles.suggestionPillActive : undefined, hoveredSuggestion === idx && activeIndex !== idx ? styles.suggestionPillHover : undefined), onClick: () => onSuggestionSelect?.(s._raw, idx), onMouseEnter: () => {
9850
11371
  setHoveredSuggestion(idx);
@@ -10188,7 +11709,7 @@ const ShoppingIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", f
10188
11709
  const ClockIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", width: "16", height: "16" },
10189
11710
  React.createElement("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z", clipRule: "evenodd" })));
10190
11711
  const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref) {
10191
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
11712
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
10192
11713
  // Inject global responsive styles
10193
11714
  useInjectResponsiveStyles();
10194
11715
  // Responsive state
@@ -10342,7 +11863,7 @@ const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref
10342
11863
  React.createElement("span", { style: styles.kbd }, "K")))),
10343
11864
  header,
10344
11865
  React.createElement("div", { style: styles.content },
10345
- React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11866
+ React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10346
11867
  React.createElement("div", { style: styles.spinner })))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty },
10347
11868
  "No results for \"",
10348
11869
  query,
@@ -10741,7 +12262,7 @@ const SearchIcon$1 = () => (React.createElement("svg", { width: "18", height: "1
10741
12262
  const ArrowIcon = () => (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "currentColor" },
10742
12263
  React.createElement("path", { fillRule: "evenodd", d: "M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z", clipRule: "evenodd" })));
10743
12264
  const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
10744
- const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
12265
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
10745
12266
  // Inject global responsive styles
10746
12267
  useInjectResponsiveStyles();
10747
12268
  // Responsive state
@@ -10813,7 +12334,7 @@ const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
10813
12334
  }
10814
12335
  `),
10815
12336
  header,
10816
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12337
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10817
12338
  React.createElement("div", { style: styles.spinner })))) : (React.createElement("div", { style: styles.layout },
10818
12339
  React.createElement("div", { style: styles.leftPanel },
10819
12340
  React.createElement("div", { style: styles.section },
@@ -11148,7 +12669,7 @@ const ChevronIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fi
11148
12669
  const ArrowUpIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "20", height: "20" },
11149
12670
  React.createElement("path", { d: "M7 17l5-5-5-5M13 17l5-5-5-5" })));
11150
12671
  const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props, ref) {
11151
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, renderLoading, renderEmpty, } = props;
12672
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
11152
12673
  // Inject global responsive styles
11153
12674
  useInjectResponsiveStyles();
11154
12675
  const styles = React.useMemo(() => createStyles$2(), []);
@@ -11290,7 +12811,7 @@ const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props,
11290
12811
  }
11291
12812
  }, style: styles.searchInput, autoFocus: true })),
11292
12813
  showCancel && (React.createElement("button", { style: styles.cancelBtn, onClick: onClose }, cancelText)))))),
11293
- React.createElement("div", { style: styles.content }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12814
+ React.createElement("div", { style: styles.content }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11294
12815
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
11295
12816
  showRecent && (React.createElement("div", { style: styles.section },
11296
12817
  React.createElement("div", { style: styles.sectionHeader },
@@ -11526,7 +13047,7 @@ const createStyles$1 = () => ({
11526
13047
  },
11527
13048
  });
11528
13049
  const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
11529
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
13050
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
11530
13051
  // Inject global responsive styles
11531
13052
  useInjectResponsiveStyles();
11532
13053
  // Responsive state
@@ -11612,7 +13133,7 @@ const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
11612
13133
  }
11613
13134
  `),
11614
13135
  header,
11615
- React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
13136
+ React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
11616
13137
  showRecent && (React.createElement(React.Fragment, null,
11617
13138
  showSectionDividers && (React.createElement("div", { style: styles.divider }, "Recent")),
11618
13139
  recentQueries.map((q, idx) => {
@@ -11947,6 +13468,7 @@ const SuggestionSearchBar = React.forwardRef(function SuggestionSearchBar(props,
11947
13468
  include_dropdown_product_list: includeDropdownProductList,
11948
13469
  include_filtered_tabs: includeFilteredTabs,
11949
13470
  include_categories: includeCategories,
13471
+ include_facets: true, // Enable facet-based suggestions
11950
13472
  filtered_tabs: filteredTabs,
11951
13473
  analytics_tags: analyticsTags,
11952
13474
  returnFullResponse: true, // Required to get tabs and products
@@ -12628,7 +14150,7 @@ function getHitKey(hit, index) {
12628
14150
  return hit.objectID;
12629
14151
  return `suggestion-${hit.url}-${index}`;
12630
14152
  }
12631
- function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
14153
+ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, showLoadingState = false, error, translations = {}, sources: _sources = [], }) {
12632
14154
  const listRef = React.useRef(null);
12633
14155
  React.useEffect(() => {
12634
14156
  if (!listRef.current || hits.length === 0)
@@ -12662,7 +14184,7 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
12662
14184
  return (React.createElement("div", { className: "seekora-docsearch-empty" },
12663
14185
  React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
12664
14186
  }
12665
- if (isLoading && hits.length === 0) {
14187
+ if (isLoading && hits.length === 0 && showLoadingState) {
12666
14188
  return (React.createElement("div", { className: "seekora-docsearch-loading" },
12667
14189
  React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
12668
14190
  React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
@@ -12670,6 +14192,10 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
12670
14192
  React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
12671
14193
  React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
12672
14194
  }
14195
+ if (isLoading && hits.length === 0) {
14196
+ return React.createElement("div", { className: "seekora-docsearch-empty" });
14197
+ }
14198
+ // When loading with previous hits, fall through and show them (no loading screen)
12673
14199
  if (error) {
12674
14200
  return (React.createElement("div", { className: "seekora-docsearch-error" },
12675
14201
  React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
@@ -13877,6 +15403,164 @@ function formatParsedFilters(filters) {
13877
15403
  .join(', ');
13878
15404
  }
13879
15405
 
15406
+ /**
15407
+ * useVariantSelection – manages variant selection state
15408
+ *
15409
+ * Single source of truth for variant interactions on product pages.
15410
+ * Cards display variants passively; product pages use this hook.
15411
+ */
15412
+ function useVariantSelection({ options = [], variants = [], initialSelections = {}, onVariantChange, } = {}) {
15413
+ const [selections, setSelections] = React.useState(initialSelections);
15414
+ const selectedVariant = React.useMemo(() => findVariantBySelections(options, variants, selections), [options, variants, selections]);
15415
+ const isComplete = React.useMemo(() => options.length > 0 && options.every((opt) => !!selections[opt.name]), [options, selections]);
15416
+ const availableValues = React.useMemo(() => {
15417
+ const result = {};
15418
+ for (const option of options) {
15419
+ result[option.name] = getAvailableValuesForOption(option.name, options, variants, selections);
15420
+ }
15421
+ return result;
15422
+ }, [options, variants, selections]);
15423
+ const effectivePrice = selectedVariant?.price ?? null;
15424
+ const effectiveComparePrice = selectedVariant?.comparePrice ?? null;
15425
+ const setSelection = React.useCallback((optionName, value) => {
15426
+ setSelections((prev) => {
15427
+ const next = { ...prev, [optionName]: value };
15428
+ const variant = findVariantBySelections(options, variants, next);
15429
+ onVariantChange?.(variant, next);
15430
+ return next;
15431
+ });
15432
+ }, [options, variants, onVariantChange]);
15433
+ const resetSelections = React.useCallback(() => {
15434
+ setSelections({});
15435
+ onVariantChange?.(null, {});
15436
+ }, [onVariantChange]);
15437
+ return {
15438
+ selections,
15439
+ setSelection,
15440
+ resetSelections,
15441
+ selectedVariant,
15442
+ availableValues,
15443
+ isComplete,
15444
+ effectivePrice,
15445
+ effectiveComparePrice,
15446
+ };
15447
+ }
15448
+
15449
+ /**
15450
+ * useProductAnalytics – analytics event binding hook for custom components
15451
+ *
15452
+ * Returns event handler props ready to spread onto any element, plus imperative
15453
+ * tracking methods. Builds on existing useAnalytics infrastructure.
15454
+ */
15455
+ function useProductAnalytics({ client, product, position = 0, section, tabId, query, context, enabled = true, }) {
15456
+ const impressionTrackedRef = React.useRef(false);
15457
+ const observerRef = React.useRef(null);
15458
+ const elementRef = React.useRef(null);
15459
+ const productId = product.id || product.objectID || '';
15460
+ const sendEvent = React.useCallback(async (eventName, metadata) => {
15461
+ if (!enabled || !client)
15462
+ return;
15463
+ try {
15464
+ await client.trackEvent?.({
15465
+ event_name: eventName,
15466
+ metadata: {
15467
+ product_id: productId,
15468
+ product_title: product.title || product.name,
15469
+ product_price: product.price,
15470
+ position,
15471
+ section,
15472
+ tab_id: tabId,
15473
+ original_query: query,
15474
+ timestamp: Date.now(),
15475
+ source: 'product_analytics',
15476
+ ...metadata,
15477
+ },
15478
+ }, context);
15479
+ log.verbose(`ProductAnalytics: ${eventName}`, metadata);
15480
+ }
15481
+ catch (error) {
15482
+ log.warn(`Failed to track ${eventName}`, { error });
15483
+ }
15484
+ }, [client, enabled, productId, product, position, section, tabId, query, context]);
15485
+ const trackClick = React.useCallback(() => {
15486
+ sendEvent('product.click', {});
15487
+ // Also fire via client.trackClick for V3 compat
15488
+ if (client?.trackClick) {
15489
+ Promise.resolve(client.trackClick(productId, position + 1, context)).catch(() => { });
15490
+ }
15491
+ }, [sendEvent, client, productId, position, context]);
15492
+ const trackVariantSelect = React.useCallback((optionName, value) => {
15493
+ sendEvent('product.variant_select', {
15494
+ option_name: optionName,
15495
+ option_value: value,
15496
+ });
15497
+ }, [sendEvent]);
15498
+ const trackAddToCart = React.useCallback((variant) => {
15499
+ sendEvent('product.add_to_cart', {
15500
+ variant_id: variant?.id,
15501
+ variant_sku: variant?.sku,
15502
+ variant_title: variant?.title,
15503
+ variant_price: variant?.price,
15504
+ });
15505
+ }, [sendEvent]);
15506
+ const trackCustomEvent = React.useCallback((eventName, metadata) => {
15507
+ sendEvent(eventName, metadata ?? {});
15508
+ }, [sendEvent]);
15509
+ // Impression tracking via IntersectionObserver
15510
+ const impressionRef = React.useCallback((node) => {
15511
+ // Cleanup previous observer
15512
+ if (observerRef.current) {
15513
+ observerRef.current.disconnect();
15514
+ observerRef.current = null;
15515
+ }
15516
+ elementRef.current = node;
15517
+ if (!node || !enabled || impressionTrackedRef.current)
15518
+ return;
15519
+ if (typeof IntersectionObserver === 'undefined')
15520
+ return;
15521
+ observerRef.current = new IntersectionObserver((entries) => {
15522
+ for (const entry of entries) {
15523
+ if (entry.isIntersecting && !impressionTrackedRef.current) {
15524
+ impressionTrackedRef.current = true;
15525
+ sendEvent('product.impression', {});
15526
+ observerRef.current?.disconnect();
15527
+ }
15528
+ }
15529
+ }, { threshold: 0.5 });
15530
+ observerRef.current.observe(node);
15531
+ }, [enabled, sendEvent]);
15532
+ // Cleanup on unmount
15533
+ React.useEffect(() => {
15534
+ return () => {
15535
+ observerRef.current?.disconnect();
15536
+ };
15537
+ }, []);
15538
+ const clickProps = {
15539
+ onClick: () => trackClick(),
15540
+ 'data-seekora-product-id': productId,
15541
+ 'data-seekora-position': position,
15542
+ };
15543
+ const variantSelectProps = (optionName, value) => ({
15544
+ onClick: () => trackVariantSelect(optionName, value),
15545
+ 'data-seekora-variant-option': optionName,
15546
+ 'data-seekora-variant-value': value,
15547
+ });
15548
+ const addToCartProps = (variant) => ({
15549
+ onClick: () => trackAddToCart(variant),
15550
+ 'data-seekora-action': 'add-to-cart',
15551
+ });
15552
+ return {
15553
+ clickProps,
15554
+ variantSelectProps,
15555
+ addToCartProps,
15556
+ impressionRef,
15557
+ trackClick,
15558
+ trackVariantSelect,
15559
+ trackAddToCart,
15560
+ trackCustomEvent,
15561
+ };
15562
+ }
15563
+
13880
15564
  /**
13881
15565
  * Dark Theme
13882
15566
  */
@@ -14416,7 +16100,10 @@ function updateSuggestionsStyles(theme) {
14416
16100
  injectSuggestionsStyles(theme, true);
14417
16101
  }
14418
16102
 
16103
+ exports.ActionButtons = ActionButtons;
14419
16104
  exports.AmazonDropdown = AmazonDropdown;
16105
+ exports.AnalyticsProvider = AnalyticsProvider;
16106
+ exports.BadgeList = BadgeList;
14420
16107
  exports.Breadcrumb = Breadcrumb;
14421
16108
  exports.CategoriesTabs = CategoriesTabs;
14422
16109
  exports.ClearRefinements = ClearRefinements;
@@ -14433,6 +16120,7 @@ exports.HierarchicalMenu = HierarchicalMenu;
14433
16120
  exports.Highlight = Highlight$1;
14434
16121
  exports.HitsPerPage = HitsPerPage;
14435
16122
  exports.ImageDisplay = ImageDisplay;
16123
+ exports.ImageZoom = ImageZoom;
14436
16124
  exports.InfiniteHits = InfiniteHits;
14437
16125
  exports.ItemCard = ItemCard;
14438
16126
  exports.ItemGrid = ItemGrid;
@@ -14442,6 +16130,7 @@ exports.MobileFiltersButton = MobileFiltersButton;
14442
16130
  exports.MobileSheetDropdown = MobileSheetDropdown;
14443
16131
  exports.Pagination = Pagination;
14444
16132
  exports.PinterestDropdown = PinterestDropdown;
16133
+ exports.PriceDisplay = PriceDisplay;
14445
16134
  exports.ProductCard = ProductCard;
14446
16135
  exports.ProductGallery = ProductGallery;
14447
16136
  exports.ProductGrid = ProductGrid;
@@ -14451,6 +16140,7 @@ exports.QuerySuggestions = QuerySuggestions;
14451
16140
  exports.QuerySuggestionsDropdown = QuerySuggestionsDropdown;
14452
16141
  exports.RangeInput = RangeInput;
14453
16142
  exports.RangeSlider = RangeSlider;
16143
+ exports.RatingDisplay = RatingDisplay;
14454
16144
  exports.RecentSearchesList = RecentSearchesList;
14455
16145
  exports.RecentlyViewed = RecentlyViewed;
14456
16146
  exports.RelatedProducts = RelatedProducts;
@@ -14480,6 +16170,8 @@ exports.SuggestionsLoading = SuggestionsLoading;
14480
16170
  exports.SuggestionsProvider = SuggestionsProvider;
14481
16171
  exports.TrendingItems = TrendingItems;
14482
16172
  exports.TrendingList = TrendingList;
16173
+ exports.VariantSelector = VariantSelector;
16174
+ exports.VariantSwatches = VariantSwatches;
14483
16175
  exports.addRecentSearch = addRecentSearch;
14484
16176
  exports.addToRecentlyViewed = addToRecentlyViewed;
14485
16177
  exports.brandPresets = brandPresets;
@@ -14492,14 +16184,19 @@ exports.createTheme = createTheme;
14492
16184
  exports.darkTheme = darkTheme;
14493
16185
  exports.darkThemeVariables = darkThemeVariables;
14494
16186
  exports.defaultTheme = defaultTheme;
16187
+ exports.extractBadges = extractBadges;
14495
16188
  exports.extractBrand = extractBrand;
14496
16189
  exports.extractCategory = extractCategory;
14497
16190
  exports.extractProduct = extractProduct;
14498
16191
  exports.extractSuggestion = extractSuggestion;
16192
+ exports.findVariantBySelections = findVariantBySelections;
14499
16193
  exports.formatParsedFilters = formatParsedFilters;
16194
+ exports.formatPriceRange = formatPriceRange;
14500
16195
  exports.formatSuggestionPrice = formatPrice;
14501
16196
  exports.generateSuggestionsStylesheet = generateSuggestionsStylesheet;
16197
+ exports.getAvailableValuesForOption = getAvailableValuesForOption;
14502
16198
  exports.getFingerprint = getFingerprint;
16199
+ exports.getPriceRange = getPriceRange;
14503
16200
  exports.getRecentSearches = getRecentSearches;
14504
16201
  exports.getShortcutText = getShortcutText;
14505
16202
  exports.getSuggestionsCache = getSuggestionsCache;
@@ -14516,11 +16213,13 @@ exports.removeRecentSearch = removeRecentSearch;
14516
16213
  exports.touchTargets = touchTargets;
14517
16214
  exports.updateSuggestionsStyles = updateSuggestionsStyles;
14518
16215
  exports.useAnalytics = useAnalytics;
16216
+ exports.useAnalyticsProvider = useAnalyticsProvider;
14519
16217
  exports.useDocSearch = useDocSearch;
14520
16218
  exports.useDocSearchSeekoraSearch = useSeekoraSearch$1;
14521
16219
  exports.useInjectResponsiveStyles = useInjectResponsiveStyles;
14522
16220
  exports.useKeyboard = useKeyboard;
14523
16221
  exports.useNaturalLanguageFilters = useNaturalLanguageFilters;
16222
+ exports.useProductAnalytics = useProductAnalytics;
14524
16223
  exports.useQuerySuggestions = useQuerySuggestions;
14525
16224
  exports.useQuerySuggestionsEnhanced = useQuerySuggestionsEnhanced;
14526
16225
  exports.useResponsive = useResponsive;
@@ -14531,4 +16230,6 @@ exports.useSeekoraSearch = useSeekoraSearch;
14531
16230
  exports.useSmartSuggestions = useSmartSuggestions;
14532
16231
  exports.useSuggestionsAnalytics = useSuggestionsAnalytics;
14533
16232
  exports.useSuggestionsContext = useSuggestionsContext;
16233
+ exports.useVariantSelection = useVariantSelection;
16234
+ exports.withAnalytics = withAnalytics;
14534
16235
  //# sourceMappingURL=index.js.map