@seekora-ai/ui-sdk-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/components/Breadcrumb.d.ts +43 -0
  2. package/dist/components/Breadcrumb.d.ts.map +1 -0
  3. package/dist/components/Breadcrumb.js +119 -0
  4. package/dist/components/ClearRefinements.d.ts +42 -0
  5. package/dist/components/ClearRefinements.d.ts.map +1 -0
  6. package/dist/components/ClearRefinements.js +80 -0
  7. package/dist/components/CurrentRefinements.d.ts +41 -0
  8. package/dist/components/CurrentRefinements.d.ts.map +1 -0
  9. package/dist/components/CurrentRefinements.js +83 -0
  10. package/dist/components/Facets.d.ts +53 -0
  11. package/dist/components/Facets.d.ts.map +1 -0
  12. package/dist/components/Facets.js +195 -0
  13. package/dist/components/FederatedDropdown.d.ts +92 -0
  14. package/dist/components/FederatedDropdown.d.ts.map +1 -0
  15. package/dist/components/FederatedDropdown.js +510 -0
  16. package/dist/components/HierarchicalMenu.d.ts +55 -0
  17. package/dist/components/HierarchicalMenu.d.ts.map +1 -0
  18. package/dist/components/HierarchicalMenu.js +168 -0
  19. package/dist/components/Highlight.d.ts +51 -0
  20. package/dist/components/Highlight.d.ts.map +1 -0
  21. package/dist/components/Highlight.js +155 -0
  22. package/dist/components/HitsPerPage.d.ts +41 -0
  23. package/dist/components/HitsPerPage.d.ts.map +1 -0
  24. package/dist/components/HitsPerPage.js +72 -0
  25. package/dist/components/InfiniteHits.d.ts +56 -0
  26. package/dist/components/InfiniteHits.d.ts.map +1 -0
  27. package/dist/components/InfiniteHits.js +181 -0
  28. package/dist/components/MobileFilters.d.ts +71 -0
  29. package/dist/components/MobileFilters.d.ts.map +1 -0
  30. package/dist/components/MobileFilters.js +242 -0
  31. package/dist/components/Pagination.d.ts +44 -0
  32. package/dist/components/Pagination.d.ts.map +1 -0
  33. package/dist/components/Pagination.js +142 -0
  34. package/dist/components/QuerySuggestions.d.ts +38 -0
  35. package/dist/components/QuerySuggestions.d.ts.map +1 -0
  36. package/dist/components/QuerySuggestions.js +86 -0
  37. package/dist/components/QuerySuggestionsDropdown.d.ts +86 -0
  38. package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -0
  39. package/dist/components/QuerySuggestionsDropdown.js +395 -0
  40. package/dist/components/RangeInput.d.ts +58 -0
  41. package/dist/components/RangeInput.d.ts.map +1 -0
  42. package/dist/components/RangeInput.js +203 -0
  43. package/dist/components/RangeSlider.d.ts +51 -0
  44. package/dist/components/RangeSlider.d.ts.map +1 -0
  45. package/dist/components/RangeSlider.js +193 -0
  46. package/dist/components/Recommendations.d.ts +90 -0
  47. package/dist/components/Recommendations.d.ts.map +1 -0
  48. package/dist/components/Recommendations.js +270 -0
  49. package/dist/components/RichQuerySuggestions.d.ts +77 -0
  50. package/dist/components/RichQuerySuggestions.d.ts.map +1 -0
  51. package/dist/components/RichQuerySuggestions.js +492 -0
  52. package/dist/components/SearchBar.d.ts +40 -0
  53. package/dist/components/SearchBar.d.ts.map +1 -0
  54. package/dist/components/SearchBar.js +217 -0
  55. package/dist/components/SearchBarWithSuggestions.d.ts +99 -0
  56. package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -0
  57. package/dist/components/SearchBarWithSuggestions.js +275 -0
  58. package/dist/components/SearchLayout.d.ts +35 -0
  59. package/dist/components/SearchLayout.d.ts.map +1 -0
  60. package/dist/components/SearchLayout.js +56 -0
  61. package/dist/components/SearchProvider.d.ts +28 -0
  62. package/dist/components/SearchProvider.d.ts.map +1 -0
  63. package/dist/components/SearchProvider.js +43 -0
  64. package/dist/components/SearchResults.d.ts +51 -0
  65. package/dist/components/SearchResults.d.ts.map +1 -0
  66. package/dist/components/SearchResults.js +485 -0
  67. package/dist/components/SortBy.d.ts +44 -0
  68. package/dist/components/SortBy.d.ts.map +1 -0
  69. package/dist/components/SortBy.js +61 -0
  70. package/dist/components/Stats.d.ts +37 -0
  71. package/dist/components/Stats.d.ts.map +1 -0
  72. package/dist/components/Stats.js +52 -0
  73. package/dist/components/suggestions/AmazonDropdown.d.ts +30 -0
  74. package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -0
  75. package/dist/components/suggestions/AmazonDropdown.js +529 -0
  76. package/dist/components/suggestions/GoogleDropdown.d.ts +31 -0
  77. package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -0
  78. package/dist/components/suggestions/GoogleDropdown.js +370 -0
  79. package/dist/components/suggestions/MinimalDropdown.d.ts +24 -0
  80. package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -0
  81. package/dist/components/suggestions/MinimalDropdown.js +314 -0
  82. package/dist/components/suggestions/MobileSheetDropdown.d.ts +31 -0
  83. package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -0
  84. package/dist/components/suggestions/MobileSheetDropdown.js +485 -0
  85. package/dist/components/suggestions/PinterestDropdown.d.ts +29 -0
  86. package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -0
  87. package/dist/components/suggestions/PinterestDropdown.js +450 -0
  88. package/dist/components/suggestions/ShopifyDropdown.d.ts +27 -0
  89. package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -0
  90. package/dist/components/suggestions/ShopifyDropdown.js +451 -0
  91. package/dist/components/suggestions/SpotlightDropdown.d.ts +33 -0
  92. package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -0
  93. package/dist/components/suggestions/SpotlightDropdown.js +547 -0
  94. package/dist/components/suggestions/SuggestionSearchBar.d.ts +123 -0
  95. package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -0
  96. package/dist/components/suggestions/SuggestionSearchBar.js +652 -0
  97. package/dist/components/suggestions/index.d.ts +37 -0
  98. package/dist/components/suggestions/index.d.ts.map +1 -0
  99. package/dist/components/suggestions/index.js +59 -0
  100. package/dist/components/suggestions/styles/index.d.ts +11 -0
  101. package/dist/components/suggestions/styles/index.d.ts.map +1 -0
  102. package/dist/components/suggestions/styles/index.js +289 -0
  103. package/dist/components/suggestions/styles/responsive.d.ts +107 -0
  104. package/dist/components/suggestions/styles/responsive.d.ts.map +1 -0
  105. package/dist/components/suggestions/styles/responsive.js +237 -0
  106. package/dist/components/suggestions/types.d.ts +489 -0
  107. package/dist/components/suggestions/types.d.ts.map +1 -0
  108. package/dist/components/suggestions/types.js +6 -0
  109. package/dist/components/suggestions/utils.d.ts +213 -0
  110. package/dist/components/suggestions/utils.d.ts.map +1 -0
  111. package/dist/components/suggestions/utils.js +514 -0
  112. package/dist/hooks/useAnalytics.d.ts +20 -0
  113. package/dist/hooks/useAnalytics.d.ts.map +1 -0
  114. package/dist/hooks/useAnalytics.js +62 -0
  115. package/dist/hooks/useNaturalLanguageFilters.d.ts +48 -0
  116. package/dist/hooks/useNaturalLanguageFilters.d.ts.map +1 -0
  117. package/dist/hooks/useNaturalLanguageFilters.js +221 -0
  118. package/dist/hooks/useQuerySuggestions.d.ts +21 -0
  119. package/dist/hooks/useQuerySuggestions.d.ts.map +1 -0
  120. package/dist/hooks/useQuerySuggestions.js +68 -0
  121. package/dist/hooks/useQuerySuggestionsEnhanced.d.ts +114 -0
  122. package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -0
  123. package/dist/hooks/useQuerySuggestionsEnhanced.js +376 -0
  124. package/dist/hooks/useSearchState.d.ts +35 -0
  125. package/dist/hooks/useSearchState.d.ts.map +1 -0
  126. package/dist/hooks/useSearchState.js +68 -0
  127. package/dist/hooks/useSeekoraSearch.d.ts +20 -0
  128. package/dist/hooks/useSeekoraSearch.d.ts.map +1 -0
  129. package/dist/hooks/useSeekoraSearch.js +63 -0
  130. package/dist/hooks/useSmartSuggestions.d.ts +55 -0
  131. package/dist/hooks/useSmartSuggestions.d.ts.map +1 -0
  132. package/dist/hooks/useSmartSuggestions.js +236 -0
  133. package/dist/hooks/useSuggestionsAnalytics.d.ts +91 -0
  134. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -0
  135. package/dist/hooks/useSuggestionsAnalytics.js +226 -0
  136. package/dist/index.d.ts +80 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +86 -0
  139. package/dist/index.umd.js +1 -0
  140. package/dist/src/index.d.ts +2849 -0
  141. package/dist/src/index.esm.js +11679 -0
  142. package/dist/src/index.esm.js.map +1 -0
  143. package/dist/src/index.js +11761 -0
  144. package/dist/src/index.js.map +1 -0
  145. package/dist/themes/createTheme.d.ts +8 -0
  146. package/dist/themes/createTheme.d.ts.map +1 -0
  147. package/dist/themes/createTheme.js +10 -0
  148. package/dist/themes/dark.d.ts +6 -0
  149. package/dist/themes/dark.d.ts.map +1 -0
  150. package/dist/themes/dark.js +34 -0
  151. package/dist/themes/default.d.ts +6 -0
  152. package/dist/themes/default.d.ts.map +1 -0
  153. package/dist/themes/default.js +71 -0
  154. package/dist/themes/mergeThemes.d.ts +7 -0
  155. package/dist/themes/mergeThemes.d.ts.map +1 -0
  156. package/dist/themes/mergeThemes.js +6 -0
  157. package/dist/themes/minimal.d.ts +6 -0
  158. package/dist/themes/minimal.d.ts.map +1 -0
  159. package/dist/themes/minimal.js +34 -0
  160. package/dist/themes/suggestions.d.ts +216 -0
  161. package/dist/themes/suggestions.d.ts.map +1 -0
  162. package/dist/themes/suggestions.js +546 -0
  163. package/dist/themes/types.d.ts +7 -0
  164. package/dist/themes/types.d.ts.map +1 -0
  165. package/dist/themes/types.js +6 -0
  166. package/dist/types/index.d.ts +33 -0
  167. package/dist/types/index.d.ts.map +1 -0
  168. package/dist/types/index.js +4 -0
  169. package/package.json +65 -0
@@ -0,0 +1,652 @@
1
+ /**
2
+ * SuggestionSearchBar - Unified Search Component with Premium Dropdowns
3
+ *
4
+ * A self-contained search component that:
5
+ * - Fetches suggestions from the API automatically
6
+ * - Parses responses internally (no user-side parsing needed)
7
+ * - Renders any of the 7 premium dropdown variants
8
+ * - Handles analytics tracking
9
+ * - Supports recent searches via localStorage
10
+ * - Fully responsive and mobile-ready
11
+ *
12
+ * Usage:
13
+ * ```tsx
14
+ * <SuggestionSearchBar
15
+ * client={seekoraClient}
16
+ * variant="amazon"
17
+ * onSearch={(query) => router.push(`/search?q=${query}`)}
18
+ * />
19
+ * ```
20
+ */
21
+ import React, { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle, useMemo, } from 'react';
22
+ import { getRecentSearches, addRecentSearch, removeRecentSearch, cx, mergeStyles, getSuggestionsCache, } from './utils';
23
+ import { useResponsive } from './styles/responsive';
24
+ import { useInjectResponsiveStyles } from './styles';
25
+ import { useSearchContext } from '../SearchProvider';
26
+ import { useSuggestionsAnalytics } from '../../hooks/useSuggestionsAnalytics';
27
+ // Import all variants
28
+ import { AmazonDropdown } from './AmazonDropdown';
29
+ import { GoogleDropdown } from './GoogleDropdown';
30
+ import { PinterestDropdown } from './PinterestDropdown';
31
+ import { SpotlightDropdown } from './SpotlightDropdown';
32
+ import { ShopifyDropdown } from './ShopifyDropdown';
33
+ import { MobileSheetDropdown } from './MobileSheetDropdown';
34
+ import { MinimalDropdown } from './MinimalDropdown';
35
+ // ============================================================================
36
+ // Theme helpers (support both Theme { colors } and DropdownThemeConfig)
37
+ // ============================================================================
38
+ function getThemeVars(theme) {
39
+ if (!theme) {
40
+ return {
41
+ '--seekora-bg-surface': '#ffffff',
42
+ '--seekora-bg-secondary': '#f7f7f7',
43
+ '--seekora-bg-tertiary': '#f0f0f0',
44
+ '--seekora-bg-hover': '#f5f5f5',
45
+ '--seekora-text-primary': '#0f1111',
46
+ '--seekora-text-secondary': '#565959',
47
+ '--seekora-text-tertiary': '#767676',
48
+ '--seekora-border-color': '#e7e7e7',
49
+ '--seekora-primary': '#f90',
50
+ '--seekora-text-on-primary': '#0f1111',
51
+ '--seekora-text-inverse': '#ffffff',
52
+ '--seekora-link': '#007185',
53
+ '--seekora-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
54
+ };
55
+ }
56
+ const t = theme;
57
+ const colors = t.colors ?? {};
58
+ return {
59
+ '--seekora-bg-surface': t.backgroundColor ?? colors.background ?? '#ffffff',
60
+ '--seekora-bg-secondary': t.surfaceColor ?? colors.hover ?? '#f7f7f7',
61
+ '--seekora-bg-tertiary': t.hoverColor ?? colors.hover ?? '#f0f0f0',
62
+ '--seekora-bg-hover': t.hoverColor ?? colors.hover ?? '#f5f5f5',
63
+ '--seekora-text-primary': t.textColor ?? colors.text ?? '#0f1111',
64
+ '--seekora-text-secondary': t.textSecondaryColor ?? colors.text ?? '#565959',
65
+ '--seekora-text-tertiary': t.textSecondaryColor ?? '#767676',
66
+ '--seekora-border-color': t.borderColor ?? colors.border ?? '#e7e7e7',
67
+ '--seekora-primary': t.primaryColor ?? colors.primary ?? '#f90',
68
+ '--seekora-text-on-primary': t.textOnPrimaryColor ?? '#0f1111',
69
+ '--seekora-text-inverse': t.textInverseColor ?? '#ffffff',
70
+ '--seekora-link': t.primaryColor ?? colors.primary ?? '#007185',
71
+ '--seekora-font-family': t.fontFamily ?? '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
72
+ };
73
+ }
74
+ function themeToDropdownConfig(theme) {
75
+ if (!theme)
76
+ return undefined;
77
+ const t = theme;
78
+ const colors = t.colors ?? {};
79
+ return {
80
+ backgroundColor: t.backgroundColor ?? colors.background,
81
+ textColor: t.textColor ?? colors.text,
82
+ surfaceColor: t.surfaceColor ?? colors.hover,
83
+ borderColor: t.borderColor ?? colors.border,
84
+ hoverColor: t.hoverColor ?? colors.hover,
85
+ primaryColor: t.primaryColor ?? colors.primary,
86
+ textSecondaryColor: t.textSecondaryColor ?? colors.text,
87
+ fontFamily: t.fontFamily,
88
+ };
89
+ }
90
+ // ============================================================================
91
+ // Variant Map
92
+ // ============================================================================
93
+ const VariantComponents = {
94
+ amazon: AmazonDropdown,
95
+ google: GoogleDropdown,
96
+ pinterest: PinterestDropdown,
97
+ spotlight: SpotlightDropdown,
98
+ shopify: ShopifyDropdown,
99
+ 'mobile-sheet': MobileSheetDropdown,
100
+ minimal: MinimalDropdown,
101
+ };
102
+ // ============================================================================
103
+ // Styles
104
+ // ============================================================================
105
+ const createStyles = (isMobile) => ({
106
+ container: {
107
+ position: 'relative',
108
+ width: '100%',
109
+ fontFamily: 'var(--seekora-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif)',
110
+ },
111
+ inputWrapper: {
112
+ display: 'flex',
113
+ alignItems: 'center',
114
+ backgroundColor: 'var(--seekora-bg-surface, #ffffff)',
115
+ color: 'var(--seekora-text-primary, #111827)',
116
+ borderWidth: '2px',
117
+ borderStyle: 'solid',
118
+ borderColor: 'var(--seekora-border-color, #d1d5db)',
119
+ borderRadius: isMobile ? '8px' : '24px',
120
+ overflow: 'hidden',
121
+ transition: 'border-color 150ms, box-shadow 150ms',
122
+ boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
123
+ },
124
+ inputWrapperFocused: {
125
+ borderColor: 'var(--seekora-primary, #f90)',
126
+ boxShadow: '0 0 0 3px rgba(255, 153, 0, 0.15), 0 2px 8px rgba(0,0,0,0.1)',
127
+ },
128
+ searchIcon: {
129
+ padding: isMobile ? '0 10px 0 14px' : '0 12px 0 18px',
130
+ color: 'var(--seekora-text-secondary, #6b7280)',
131
+ display: 'flex',
132
+ alignItems: 'center',
133
+ flexShrink: 0,
134
+ },
135
+ input: {
136
+ flex: 1,
137
+ padding: isMobile ? '12px 8px 12px 0' : '14px 12px 14px 0',
138
+ fontSize: isMobile ? '16px' : '15px',
139
+ borderWidth: 0,
140
+ borderStyle: 'none',
141
+ outline: 'none',
142
+ backgroundColor: 'transparent',
143
+ color: 'var(--seekora-text-primary, #0f1111)',
144
+ fontFamily: 'inherit',
145
+ minWidth: 0,
146
+ WebkitAppearance: 'none',
147
+ },
148
+ clearButton: {
149
+ padding: '8px 14px',
150
+ background: 'none',
151
+ borderWidth: 0,
152
+ borderStyle: 'none',
153
+ cursor: 'pointer',
154
+ color: 'var(--seekora-text-tertiary, #9ca3af)',
155
+ fontSize: '18px',
156
+ lineHeight: 1,
157
+ display: 'flex',
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ flexShrink: 0,
161
+ transition: 'color 150ms',
162
+ },
163
+ loadingSpinner: {
164
+ width: '18px',
165
+ height: '18px',
166
+ borderWidth: '2px',
167
+ borderStyle: 'solid',
168
+ borderColor: '#e5e7eb',
169
+ borderTopColor: '#f90',
170
+ borderRadius: '50%',
171
+ animation: 'seekora-spin 0.8s linear infinite',
172
+ marginRight: '12px',
173
+ flexShrink: 0,
174
+ },
175
+ });
176
+ // ============================================================================
177
+ // Component
178
+ // ============================================================================
179
+ export const SuggestionSearchBar = forwardRef(function SuggestionSearchBar(props, ref) {
180
+ const { client, variant = 'amazon', autoMobileVariant = true, placeholder = 'Search...', defaultQuery = '', value, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, includeDropdownRecommendations = true, includeCategories = true, filteredTabs, analyticsTags, enableRecentSearches = true, maxRecentSearches = 10, showProducts = true, showTrendingOnEmpty = true, enableAnalytics = true, analyticsConfig, suggestionFields, productFields, theme, onSearch, onQueryChange, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, className, style, inputClassName, dropdownWidth, dropdownMaxHeight = '500px', zIndex = 1000, enableCache = true, cacheTtlMs = 30000, cacheMaxSize = 100, } = props;
181
+ // Theme: prop overrides context (SearchProvider theme)
182
+ const searchContext = useSearchContext();
183
+ const effectiveTheme = theme ?? searchContext.theme;
184
+ // Initialize analytics hook for proper event tracking
185
+ const analytics = useSuggestionsAnalytics({
186
+ client,
187
+ enabled: enableAnalytics,
188
+ analyticsTags: analyticsConfig?.tags ?? analyticsTags,
189
+ trackImpressions: analyticsConfig?.trackImpressions ?? true,
190
+ trackClicks: analyticsConfig?.trackSuggestionClicks ?? true,
191
+ });
192
+ // Inject responsive styles
193
+ useInjectResponsiveStyles();
194
+ // Inject theme CSS variables so dropdown and page use one consistent theme
195
+ useEffect(() => {
196
+ const root = document.documentElement;
197
+ const themeVars = getThemeVars(effectiveTheme);
198
+ Object.entries(themeVars).forEach(([key, value]) => {
199
+ root.style.setProperty(key, value);
200
+ });
201
+ }, [effectiveTheme]);
202
+ // Responsive state
203
+ const responsive = useResponsive();
204
+ const { isMobile } = responsive;
205
+ // Determine actual variant (auto-switch to mobile on small screens)
206
+ const actualVariant = useMemo(() => {
207
+ if (autoMobileVariant && isMobile && variant !== 'mobile-sheet' && variant !== 'spotlight') {
208
+ return 'mobile-sheet';
209
+ }
210
+ return variant;
211
+ }, [variant, isMobile, autoMobileVariant]);
212
+ // State
213
+ const [query, setQuery] = useState(defaultQuery);
214
+ const [isOpen, setIsOpen] = useState(false);
215
+ const [isFocused, setIsFocused] = useState(false);
216
+ const [loading, setLoading] = useState(false);
217
+ const [suggestions, setSuggestions] = useState([]);
218
+ const [products, setProducts] = useState([]);
219
+ const [categories, setCategories] = useState([]);
220
+ const [tabs, setTabs] = useState([]);
221
+ const [trendingSearches, setTrendingSearches] = useState([]);
222
+ const [recentSearches, setRecentSearches] = useState([]);
223
+ const [activeTab, setActiveTab] = useState('all');
224
+ // Refs
225
+ const inputRef = useRef(null);
226
+ const dropdownRef = useRef(null);
227
+ const debounceRef = useRef(null);
228
+ const containerRef = useRef(null);
229
+ // Initialize cache
230
+ const cache = useMemo(() => {
231
+ if (!enableCache)
232
+ return null;
233
+ return getSuggestionsCache({ maxSize: cacheMaxSize, defaultTtlMs: cacheTtlMs });
234
+ }, [enableCache, cacheMaxSize, cacheTtlMs]);
235
+ // Controlled vs uncontrolled query
236
+ const currentQuery = value !== undefined ? value : query;
237
+ // Styles
238
+ const styles = useMemo(() => createStyles(isMobile), [isMobile]);
239
+ // Load recent searches on mount
240
+ useEffect(() => {
241
+ if (enableRecentSearches) {
242
+ const allRecent = getRecentSearches();
243
+ setRecentSearches(allRecent.slice(0, maxRecentSearches));
244
+ }
245
+ }, [enableRecentSearches, maxRecentSearches]);
246
+ // ========================================================================
247
+ // API Data Fetching
248
+ // ========================================================================
249
+ const fetchSuggestions = useCallback(async (searchQuery) => {
250
+ if (!client)
251
+ return;
252
+ // Allow empty string "" for focus-triggered search (API returns trending/default); otherwise enforce min length
253
+ if (searchQuery.length > 0 && searchQuery.length < minQueryLength) {
254
+ setSuggestions([]);
255
+ setProducts([]);
256
+ setTabs([]);
257
+ return;
258
+ }
259
+ // Build cache key from query and options
260
+ const cacheOptions = {
261
+ maxSuggestions,
262
+ includeDropdownRecommendations,
263
+ includeCategories,
264
+ filteredTabs: filteredTabs?.map(t => t.filter).join(','),
265
+ };
266
+ const cacheKey = cache?.generateKey(searchQuery, cacheOptions);
267
+ // Check cache first
268
+ if (cache && cacheKey) {
269
+ const cachedResponse = cache.get(cacheKey);
270
+ if (cachedResponse) {
271
+ console.log('Seekora: Cache hit for query:', searchQuery);
272
+ parseAndSetData(cachedResponse);
273
+ return;
274
+ }
275
+ }
276
+ setLoading(true);
277
+ try {
278
+ // Use the SDK client to fetch suggestions ("" is valid - API returns something for empty query)
279
+ // IMPORTANT: returnFullResponse: true is required to get filtered_tabs and extensions
280
+ const response = await client.getSuggestions?.(searchQuery, {
281
+ hitsPerPage: maxSuggestions,
282
+ include_dropdown_recommendations: includeDropdownRecommendations,
283
+ include_categories: includeCategories,
284
+ filtered_tabs: filteredTabs,
285
+ analytics_tags: analyticsTags,
286
+ returnFullResponse: true, // Required to get tabs and products
287
+ });
288
+ // Cache the response
289
+ if (cache && cacheKey && response) {
290
+ cache.set(cacheKey, response);
291
+ console.log('Seekora: Cached response for query:', searchQuery);
292
+ }
293
+ // Parse the response - SDK returns normalized structure
294
+ parseAndSetData(response);
295
+ // Track impressions via analytics hook (sends to API)
296
+ if (enableAnalytics && response && !Array.isArray(response)) {
297
+ const resp = response;
298
+ analytics.trackImpression({
299
+ suggestions: (resp.suggestions || []).map((s) => ({
300
+ query: s.query || s.text || '',
301
+ objectID: s.objectID || s.id,
302
+ popularity: s.popularity,
303
+ })),
304
+ products: resp.extensions?.trending_products || resp.extensions?.item_recommendations || [],
305
+ categories: resp.extensions?.popular_categories || [],
306
+ brands: resp.extensions?.popular_brands || [],
307
+ query: searchQuery,
308
+ timestamp: Date.now(),
309
+ });
310
+ }
311
+ }
312
+ catch (error) {
313
+ console.error('Seekora: Failed to fetch suggestions', error);
314
+ setSuggestions([]);
315
+ setProducts([]);
316
+ setTabs([]);
317
+ }
318
+ finally {
319
+ setLoading(false);
320
+ }
321
+ }, [client, minQueryLength, maxSuggestions, includeDropdownRecommendations, includeCategories, filteredTabs, analyticsTags, enableAnalytics, analytics, cache]);
322
+ // Parse API response - handles multiple response formats
323
+ const parseAndSetData = useCallback((response) => {
324
+ // Handle different response structures from the API/SDK
325
+ let hits = [];
326
+ let extensions = {};
327
+ console.log('Seekora: Raw response structure', {
328
+ hasSuggestions: !!response?.suggestions,
329
+ hasResults: !!response?.results,
330
+ hasExtensions: !!response?.extensions,
331
+ hasData: !!response?.data,
332
+ });
333
+ // SDK returnFullResponse format: { suggestions, results, extensions }
334
+ if (response?.suggestions && response?.extensions) {
335
+ hits = response.suggestions || [];
336
+ extensions = response.extensions || {};
337
+ }
338
+ // Multi-index response: { results: [{ hits, extensions }, ...] }
339
+ else if (response?.results && Array.isArray(response.results)) {
340
+ const result0 = response.results[0] || {};
341
+ const result1 = response.results[1] || {};
342
+ hits = result0.hits || result0.suggestions || [];
343
+ // Extensions can be in result0 or result1
344
+ extensions = result0.extensions || result1.extensions || {};
345
+ }
346
+ // Wrapped response: { data: { hits, extensions, ... } }
347
+ else if (response?.data) {
348
+ hits = response.data.hits || [];
349
+ extensions = response.data.extensions || response.data.dropdown_recommendations || {};
350
+ }
351
+ // Direct response: { hits, extensions, ... }
352
+ else if (response?.hits) {
353
+ hits = response.hits || [];
354
+ extensions = response.extensions || response.dropdown_recommendations || {};
355
+ }
356
+ // Array response
357
+ else if (Array.isArray(response)) {
358
+ hits = response;
359
+ }
360
+ // Set suggestions
361
+ setSuggestions(hits);
362
+ // Set filtered tabs from extensions
363
+ const filteredTabsData = extensions.filtered_tabs || [];
364
+ setTabs(filteredTabsData);
365
+ // Set products from first tab or trending
366
+ if (filteredTabsData.length > 0 && filteredTabsData[0].products) {
367
+ setProducts(filteredTabsData[0].products);
368
+ }
369
+ else {
370
+ setProducts(extensions.trending_products || extensions.item_recommendations || []);
371
+ }
372
+ // Set categories
373
+ setCategories(extensions.popular_categories || []);
374
+ // Set trending searches
375
+ setTrendingSearches(extensions.trending_searches || extensions.top_searches || []);
376
+ console.log('Seekora: Parsed response', {
377
+ suggestions: hits.length,
378
+ tabs: filteredTabsData.length,
379
+ products: filteredTabsData[0]?.products?.length || 0,
380
+ extensionKeys: Object.keys(extensions),
381
+ });
382
+ }, []);
383
+ // ========================================================================
384
+ // Event Handlers
385
+ // ========================================================================
386
+ const handleInputChange = useCallback((e) => {
387
+ const newQuery = e.target.value;
388
+ if (value === undefined) {
389
+ setQuery(newQuery);
390
+ }
391
+ onQueryChange?.(newQuery);
392
+ setActiveTab('all');
393
+ // Debounced fetch
394
+ if (debounceRef.current) {
395
+ clearTimeout(debounceRef.current);
396
+ }
397
+ if (newQuery.length >= minQueryLength) {
398
+ setIsOpen(true);
399
+ debounceRef.current = setTimeout(() => {
400
+ fetchSuggestions(newQuery);
401
+ }, debounceMs);
402
+ }
403
+ else if (newQuery.length === 0 && (enableRecentSearches || showTrendingOnEmpty)) {
404
+ setIsOpen(true);
405
+ setSuggestions([]);
406
+ }
407
+ else {
408
+ setIsOpen(false);
409
+ }
410
+ }, [value, onQueryChange, minQueryLength, debounceMs, fetchSuggestions, enableRecentSearches, showTrendingOnEmpty]);
411
+ const handleInputFocus = useCallback(() => {
412
+ setIsFocused(true);
413
+ setIsOpen(true);
414
+ // Track dropdown open
415
+ if (enableAnalytics) {
416
+ analytics.trackDropdownOpen(currentQuery);
417
+ }
418
+ // Trigger empty search "" when user clicks/focuses the box so API returns trending/default content
419
+ if (currentQuery.length >= minQueryLength) {
420
+ if (suggestions.length === 0)
421
+ fetchSuggestions(currentQuery);
422
+ }
423
+ else {
424
+ fetchSuggestions('');
425
+ }
426
+ }, [currentQuery, minQueryLength, suggestions.length, fetchSuggestions, enableAnalytics, analytics]);
427
+ const handleInputBlur = useCallback(() => {
428
+ setIsFocused(false);
429
+ // Delay close to allow click events on dropdown
430
+ setTimeout(() => {
431
+ if (!containerRef.current?.contains(document.activeElement)) {
432
+ setIsOpen(false);
433
+ }
434
+ }, 150);
435
+ }, []);
436
+ const handleKeyDown = useCallback((e) => {
437
+ if (e.key === 'Escape') {
438
+ setIsOpen(false);
439
+ inputRef.current?.blur();
440
+ }
441
+ else if (e.key === 'Enter') {
442
+ e.preventDefault();
443
+ // Check if there's an active selection in the dropdown
444
+ const activeIndex = dropdownRef.current?.getActiveIndex?.() ?? -1;
445
+ if (activeIndex >= 0 && isOpen) {
446
+ // Let the dropdown handle the selection
447
+ dropdownRef.current?.selectActive?.();
448
+ }
449
+ else if (currentQuery.trim()) {
450
+ // No active selection, submit the current query
451
+ onSearch?.(currentQuery.trim());
452
+ if (enableRecentSearches) {
453
+ addRecentSearch(currentQuery.trim());
454
+ setRecentSearches(getRecentSearches().slice(0, maxRecentSearches));
455
+ }
456
+ // Track search submit analytics
457
+ if (enableAnalytics) {
458
+ analytics.trackSearchSubmit(currentQuery.trim(), false);
459
+ }
460
+ setIsOpen(false);
461
+ }
462
+ }
463
+ else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
464
+ e.preventDefault();
465
+ // Open dropdown if not already open
466
+ if (!isOpen && (suggestions.length > 0 || recentSearches.length > 0)) {
467
+ setIsOpen(true);
468
+ }
469
+ if (e.key === 'ArrowDown') {
470
+ dropdownRef.current?.navigateNext?.();
471
+ }
472
+ else {
473
+ dropdownRef.current?.navigatePrevious?.();
474
+ }
475
+ }
476
+ }, [currentQuery, onSearch, enableRecentSearches, maxRecentSearches, isOpen, suggestions.length, recentSearches.length, enableAnalytics, analytics]);
477
+ const handleSuggestionSelect = useCallback((suggestion, index) => {
478
+ const text = suggestion.query || suggestion.text || suggestion.objectID || '';
479
+ if (value === undefined) {
480
+ setQuery(text);
481
+ }
482
+ onQueryChange?.(text);
483
+ onSuggestionSelect?.(suggestion, index);
484
+ if (enableRecentSearches && text) {
485
+ addRecentSearch(text);
486
+ setRecentSearches(getRecentSearches().slice(0, maxRecentSearches));
487
+ }
488
+ // Track analytics via the proper hook (sends to API)
489
+ if (enableAnalytics) {
490
+ analytics.trackSuggestionClick({
491
+ suggestion: {
492
+ query: text,
493
+ objectID: suggestion.objectID || suggestion.id,
494
+ popularity: suggestion.popularity,
495
+ },
496
+ position: index,
497
+ section: 'suggestions',
498
+ query: currentQuery,
499
+ totalSuggestions: suggestions.length,
500
+ });
501
+ analytics.trackSearchSubmit(text, true, suggestion);
502
+ }
503
+ onSearch?.(text);
504
+ setIsOpen(false);
505
+ }, [value, onQueryChange, onSuggestionSelect, enableRecentSearches, maxRecentSearches, enableAnalytics, onSearch, analytics, currentQuery, suggestions.length]);
506
+ const handleProductClick = useCallback((product, index) => {
507
+ onProductClick?.(product, index);
508
+ // Track analytics via the proper hook (sends to API)
509
+ if (enableAnalytics) {
510
+ analytics.trackProductClick({
511
+ product: {
512
+ id: product.id || product.objectID,
513
+ objectID: product.objectID || product.id,
514
+ title: product.title || product.name,
515
+ image: product.image,
516
+ price: product.price,
517
+ },
518
+ position: index,
519
+ section: activeTab !== 'all' ? 'filtered_tab' : 'products',
520
+ tabId: activeTab !== 'all' ? activeTab : undefined,
521
+ query: currentQuery,
522
+ });
523
+ }
524
+ }, [onProductClick, enableAnalytics, analytics, activeTab, currentQuery]);
525
+ const handleCategoryClick = useCallback((category, index) => {
526
+ onCategoryClick?.(category, index);
527
+ // Track analytics via the proper hook (sends to API)
528
+ if (enableAnalytics) {
529
+ analytics.trackCategoryClick({
530
+ value: category.label || category.value || category.id,
531
+ count: category.count,
532
+ path: category.path,
533
+ }, currentQuery);
534
+ }
535
+ }, [onCategoryClick, enableAnalytics, analytics, currentQuery]);
536
+ const handleTabChange = useCallback((tabId) => {
537
+ console.log('Seekora: Tab changed to', tabId);
538
+ setActiveTab(tabId);
539
+ onTabChange?.(tabId);
540
+ // Track analytics via the proper hook (sends to API)
541
+ if (enableAnalytics) {
542
+ const tab = tabs.find(t => t.id === tabId);
543
+ if (tab) {
544
+ analytics.trackTabSelect({
545
+ id: tab.id,
546
+ label: tab.label,
547
+ filter: tab.filter,
548
+ products: tab.products,
549
+ nb_hits: tab.nb_hits,
550
+ }, currentQuery);
551
+ }
552
+ }
553
+ }, [onTabChange, enableAnalytics, analytics, tabs, currentQuery]);
554
+ const handleRecentClick = useCallback((text) => {
555
+ if (value === undefined) {
556
+ setQuery(text);
557
+ }
558
+ onQueryChange?.(text);
559
+ fetchSuggestions(text);
560
+ // Track analytics via the proper hook (sends to API)
561
+ if (enableAnalytics) {
562
+ analytics.trackRecentSearchClick({
563
+ query: text,
564
+ timestamp: Date.now(),
565
+ });
566
+ }
567
+ }, [value, onQueryChange, fetchSuggestions, enableAnalytics, analytics]);
568
+ const handleRecentRemove = useCallback((text) => {
569
+ removeRecentSearch(text);
570
+ setRecentSearches(getRecentSearches().slice(0, maxRecentSearches));
571
+ }, [maxRecentSearches]);
572
+ const handleClear = useCallback(() => {
573
+ if (value === undefined) {
574
+ setQuery('');
575
+ }
576
+ onQueryChange?.('');
577
+ setSuggestions([]);
578
+ setProducts([]);
579
+ setTabs([]);
580
+ inputRef.current?.focus();
581
+ }, [value, onQueryChange]);
582
+ const handleClose = useCallback(() => {
583
+ setIsOpen(false);
584
+ // Track dropdown close
585
+ if (enableAnalytics) {
586
+ analytics.trackDropdownClose(currentQuery);
587
+ }
588
+ }, [enableAnalytics, analytics, currentQuery]);
589
+ // ========================================================================
590
+ // Imperative Handle
591
+ // ========================================================================
592
+ useImperativeHandle(ref, () => ({
593
+ focus: () => inputRef.current?.focus(),
594
+ blur: () => inputRef.current?.blur(),
595
+ clear: handleClear,
596
+ getQuery: () => currentQuery,
597
+ setQuery: (q) => {
598
+ if (value === undefined) {
599
+ setQuery(q);
600
+ }
601
+ onQueryChange?.(q);
602
+ },
603
+ openDropdown: () => setIsOpen(true),
604
+ closeDropdown: () => setIsOpen(false),
605
+ refresh: () => fetchSuggestions(currentQuery),
606
+ clearCache: () => cache?.clear(),
607
+ getCacheStats: () => cache?.getStats() ?? null,
608
+ }), [currentQuery, value, onQueryChange, handleClear, fetchSuggestions, cache]);
609
+ // ========================================================================
610
+ // Render
611
+ // ========================================================================
612
+ const DropdownComponent = VariantComponents[actualVariant] || AmazonDropdown;
613
+ // Get products for current tab - always derive from tabs to ensure consistency
614
+ const currentTabProducts = useMemo(() => {
615
+ console.log('Seekora: Computing products for tab', activeTab, 'from', tabs.length, 'tabs');
616
+ if (tabs.length === 0) {
617
+ return products; // Fallback to products state if no tabs
618
+ }
619
+ if (activeTab === 'all') {
620
+ // For 'all' tab, use first tab's products or combined
621
+ const firstTabProducts = tabs[0]?.products || [];
622
+ return firstTabProducts.length > 0 ? firstTabProducts : products;
623
+ }
624
+ // Find the specific tab
625
+ const tab = tabs.find(t => t.id === activeTab);
626
+ if (tab?.products && tab.products.length > 0) {
627
+ console.log('Seekora: Found', tab.products.length, 'products for tab', activeTab);
628
+ return tab.products;
629
+ }
630
+ // Fallback
631
+ return products;
632
+ }, [activeTab, tabs, products]);
633
+ return (React.createElement("div", { ref: containerRef, className: cx('seekora-suggestion-search', className), style: mergeStyles(styles.container, style) },
634
+ React.createElement("div", { style: mergeStyles(styles.inputWrapper, isFocused ? styles.inputWrapperFocused : undefined) },
635
+ React.createElement("div", { style: styles.searchIcon },
636
+ React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", width: "20", height: "20" },
637
+ React.createElement("path", { fillRule: "evenodd", d: "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", clipRule: "evenodd" }))),
638
+ React.createElement("input", { ref: inputRef, type: "text", value: currentQuery, onChange: handleInputChange, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleKeyDown, placeholder: placeholder, className: inputClassName, style: styles.input, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-label": "Search", "aria-expanded": isOpen, "aria-haspopup": "listbox" }),
639
+ loading && React.createElement("div", { style: styles.loadingSpinner }),
640
+ currentQuery && !loading && (React.createElement("button", { type: "button", onClick: handleClear, style: styles.clearButton, "aria-label": "Clear search" }, "\u00D7"))),
641
+ isOpen && (React.createElement(DropdownComponent, { query: currentQuery, isOpen: isOpen, loading: loading, suggestions: suggestions, products: currentTabProducts, categories: tabs.map(t => ({ id: t.id, label: t.label, count: t.nb_hits })), recentSearches: recentSearches.map(r => typeof r === 'string' ? r : r.query), trendingSearches: trendingSearches, filteredTabs: tabs, activeTab: activeTab, suggestionFields: suggestionFields, productFields: productFields, theme: themeToDropdownConfig(effectiveTheme) ?? effectiveTheme, width: dropdownWidth || '100%', maxHeight: dropdownMaxHeight, zIndex: zIndex, analytics: {
642
+ enabled: enableAnalytics,
643
+ trackSuggestionClicks: analyticsConfig?.trackSuggestionClicks ?? true,
644
+ trackProductClicks: analyticsConfig?.trackProductClicks ?? true,
645
+ trackImpressions: analyticsConfig?.trackImpressions ?? true,
646
+ trackCategoryClicks: analyticsConfig?.trackCategoryClicks ?? true,
647
+ trackBrandClicks: analyticsConfig?.trackBrandClicks ?? true,
648
+ trackSearchSubmit: analyticsConfig?.trackSearchSubmit ?? true,
649
+ tags: analyticsConfig?.tags ?? analyticsTags,
650
+ }, onSuggestionSelect: handleSuggestionSelect, onProductClick: handleProductClick, onCategoryClick: handleCategoryClick, onTabChange: handleTabChange, onRecentClick: handleRecentClick, onRecentRemove: handleRecentRemove, onClose: handleClose }))));
651
+ });
652
+ export default SuggestionSearchBar;