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