@seekora-ai/ui-sdk-vanilla 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 (71) hide show
  1. package/dist/components/clear-refinements.d.ts +39 -0
  2. package/dist/components/clear-refinements.d.ts.map +1 -0
  3. package/dist/components/clear-refinements.js +133 -0
  4. package/dist/components/current-refinements.d.ts +36 -0
  5. package/dist/components/current-refinements.d.ts.map +1 -0
  6. package/dist/components/current-refinements.js +186 -0
  7. package/dist/components/facets.d.ts +45 -0
  8. package/dist/components/facets.d.ts.map +1 -0
  9. package/dist/components/facets.js +259 -0
  10. package/dist/components/hits-per-page.d.ts +37 -0
  11. package/dist/components/hits-per-page.d.ts.map +1 -0
  12. package/dist/components/hits-per-page.js +132 -0
  13. package/dist/components/infinite-hits.d.ts +61 -0
  14. package/dist/components/infinite-hits.d.ts.map +1 -0
  15. package/dist/components/infinite-hits.js +316 -0
  16. package/dist/components/pagination.d.ts +33 -0
  17. package/dist/components/pagination.d.ts.map +1 -0
  18. package/dist/components/pagination.js +364 -0
  19. package/dist/components/query-suggestions.d.ts +39 -0
  20. package/dist/components/query-suggestions.d.ts.map +1 -0
  21. package/dist/components/query-suggestions.js +217 -0
  22. package/dist/components/range-input.d.ts +42 -0
  23. package/dist/components/range-input.d.ts.map +1 -0
  24. package/dist/components/range-input.js +274 -0
  25. package/dist/components/search-bar.d.ts +140 -0
  26. package/dist/components/search-bar.d.ts.map +1 -0
  27. package/dist/components/search-bar.js +899 -0
  28. package/dist/components/search-layout.d.ts +35 -0
  29. package/dist/components/search-layout.d.ts.map +1 -0
  30. package/dist/components/search-layout.js +144 -0
  31. package/dist/components/search-provider.d.ts +28 -0
  32. package/dist/components/search-provider.d.ts.map +1 -0
  33. package/dist/components/search-provider.js +44 -0
  34. package/dist/components/search-results.d.ts +55 -0
  35. package/dist/components/search-results.d.ts.map +1 -0
  36. package/dist/components/search-results.js +537 -0
  37. package/dist/components/sort-by.d.ts +33 -0
  38. package/dist/components/sort-by.d.ts.map +1 -0
  39. package/dist/components/sort-by.js +122 -0
  40. package/dist/components/stats.d.ts +36 -0
  41. package/dist/components/stats.d.ts.map +1 -0
  42. package/dist/components/stats.js +138 -0
  43. package/dist/index.d.ts +670 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.esm.js +4008 -0
  46. package/dist/index.esm.js.map +1 -0
  47. package/dist/index.js +4055 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/index.umd.js +1 -0
  50. package/dist/themes/createTheme.d.ts +8 -0
  51. package/dist/themes/createTheme.d.ts.map +1 -0
  52. package/dist/themes/createTheme.js +10 -0
  53. package/dist/themes/dark.d.ts +6 -0
  54. package/dist/themes/dark.d.ts.map +1 -0
  55. package/dist/themes/dark.js +34 -0
  56. package/dist/themes/default.d.ts +6 -0
  57. package/dist/themes/default.d.ts.map +1 -0
  58. package/dist/themes/default.js +71 -0
  59. package/dist/themes/mergeThemes.d.ts +7 -0
  60. package/dist/themes/mergeThemes.d.ts.map +1 -0
  61. package/dist/themes/mergeThemes.js +6 -0
  62. package/dist/themes/minimal.d.ts +6 -0
  63. package/dist/themes/minimal.d.ts.map +1 -0
  64. package/dist/themes/minimal.js +34 -0
  65. package/dist/themes/types.d.ts +7 -0
  66. package/dist/themes/types.d.ts.map +1 -0
  67. package/dist/themes/types.js +6 -0
  68. package/dist/utils/search-manager.d.ts +33 -0
  69. package/dist/utils/search-manager.d.ts.map +1 -0
  70. package/dist/utils/search-manager.js +89 -0
  71. package/package.json +60 -0
@@ -0,0 +1,899 @@
1
+ /**
2
+ * SearchBar Component
3
+ *
4
+ * Creates a search bar with autocomplete suggestions
5
+ */
6
+ import { log } from '@seekora-ai/ui-sdk-core';
7
+ import { SearchProvider } from './search-provider';
8
+ export class SearchBar {
9
+ constructor(providerOrClient, options) {
10
+ this.suggestions = [];
11
+ this.richSuggestionsData = null;
12
+ this.selectedIndex = -1;
13
+ this.debounceTimer = null;
14
+ this.isFocused = false;
15
+ this.unsubscribeStateManager = null;
16
+ // Support both SearchProvider (new) and SeekoraClient (backwards compatibility)
17
+ if (providerOrClient instanceof SearchProvider) {
18
+ this.provider = providerOrClient;
19
+ this.client = this.provider.client;
20
+ }
21
+ else {
22
+ // Backwards compatibility: create a provider if client is passed
23
+ this.provider = new SearchProvider({ client: providerOrClient });
24
+ this.client = providerOrClient;
25
+ }
26
+ const container = typeof options.container === 'string'
27
+ ? document.querySelector(options.container)
28
+ : options.container;
29
+ if (!container) {
30
+ throw new Error('SearchBar: container element not found');
31
+ }
32
+ this.container = container;
33
+ this.options = {
34
+ placeholder: options.placeholder || 'Search...',
35
+ showSuggestions: options.showSuggestions !== false,
36
+ minQueryLength: options.minQueryLength || 1,
37
+ maxSuggestions: options.maxSuggestions || 10,
38
+ debounceMs: options.debounceMs || 300,
39
+ enableRichSuggestions: options.enableRichSuggestions !== false, // Default to true
40
+ onSearch: options.onSearch,
41
+ onSuggestionSelect: options.onSuggestionSelect,
42
+ onProductClick: options.onProductClick,
43
+ onBrandClick: options.onBrandClick,
44
+ searchOptions: options.searchOptions,
45
+ };
46
+ // Cache credentials and environment from client for rich suggestions
47
+ this.cacheClientCredentials();
48
+ this.render();
49
+ this.attachEventListeners();
50
+ // Subscribe to state manager to sync input with query state
51
+ this.unsubscribeStateManager = this.provider.stateManager.subscribe((state) => {
52
+ // Only update input if it's different (avoid cursor jumping)
53
+ if (this.input.value !== state.query) {
54
+ this.input.value = state.query;
55
+ }
56
+ // Update response time display
57
+ if (state.results) {
58
+ const res = state.results;
59
+ const processingTime = res.processingTimeMS
60
+ || res.data?.processingTimeMS
61
+ || res.data?.data?.processingTimeMS;
62
+ if (processingTime !== undefined) {
63
+ this.responseTimeElement.textContent = `${processingTime}ms`;
64
+ this.responseTimeElement.style.display = 'inline';
65
+ }
66
+ else {
67
+ this.responseTimeElement.style.display = 'none';
68
+ }
69
+ }
70
+ else {
71
+ this.responseTimeElement.style.display = 'none';
72
+ }
73
+ // Call onSearch callback if results are available (for backwards compatibility)
74
+ if (state.results && this.options.onSearch) {
75
+ this.options.onSearch(state.query, state.results);
76
+ }
77
+ });
78
+ }
79
+ render() {
80
+ this.container.style.position = 'relative';
81
+ this.container.style.display = 'flex';
82
+ this.container.style.alignItems = 'center';
83
+ // Create wrapper for input and response time
84
+ this.inputWrapper = document.createElement('div');
85
+ this.inputWrapper.style.cssText = 'position: relative; flex: 1; display: flex; align-items: center;';
86
+ this.input = document.createElement('input');
87
+ this.input.type = 'text';
88
+ this.input.placeholder = this.options.placeholder;
89
+ this.input.style.cssText = this.getInputStyle();
90
+ this.injectPlaceholderStyles();
91
+ this.inputWrapper.appendChild(this.input);
92
+ // Response time element
93
+ this.responseTimeElement = document.createElement('span');
94
+ this.responseTimeElement.style.cssText = this.getResponseTimeStyle();
95
+ this.responseTimeElement.style.display = 'none';
96
+ this.inputWrapper.appendChild(this.responseTimeElement);
97
+ this.container.appendChild(this.inputWrapper);
98
+ this.suggestionsContainer = document.createElement('div');
99
+ this.suggestionsContainer.style.cssText = this.getSuggestionsContainerStyle();
100
+ this.suggestionsContainer.style.display = 'none';
101
+ this.container.appendChild(this.suggestionsContainer);
102
+ }
103
+ injectPlaceholderStyles() {
104
+ const theme = this.provider.theme;
105
+ // If input doesn't have an ID, add one
106
+ if (!this.input.id) {
107
+ this.input.id = 'seekora-search-input-' + Math.random().toString(36).substr(2, 9);
108
+ }
109
+ const styleId = `seekora-searchbar-placeholder-${this.input.id}`;
110
+ // Remove existing style if it exists
111
+ const existingStyle = document.getElementById(styleId);
112
+ if (existingStyle) {
113
+ existingStyle.remove();
114
+ }
115
+ // Create new style element
116
+ const style = document.createElement('style');
117
+ style.id = styleId;
118
+ const placeholderColor = theme.colors.textSecondary || theme.colors.text;
119
+ style.textContent = `
120
+ #${this.input.id}::placeholder {
121
+ color: ${placeholderColor};
122
+ opacity: 0.6;
123
+ }
124
+ `;
125
+ document.head.appendChild(style);
126
+ }
127
+ attachEventListeners() {
128
+ this.input.addEventListener('input', (e) => {
129
+ const query = e.target.value;
130
+ this.handleInputChange(query);
131
+ });
132
+ this.input.addEventListener('focus', () => {
133
+ this.isFocused = true;
134
+ // Fetch suggestions with empty query when input is focused
135
+ // This will show trending products/brands even when no query is typed
136
+ if (this.options.showSuggestions && this.options.enableRichSuggestions) {
137
+ console.log('🔍 SearchBar: Input focused, fetching empty query suggestions');
138
+ this.handleInputChange(''); // Trigger fetch with empty query
139
+ }
140
+ else {
141
+ this.updateSuggestionsVisibility();
142
+ }
143
+ });
144
+ this.input.addEventListener('blur', (e) => {
145
+ // Check if the blur is caused by clicking on a suggestion
146
+ const relatedTarget = e.relatedTarget;
147
+ if (relatedTarget && this.suggestionsContainer.contains(relatedTarget)) {
148
+ return; // Don't hide suggestions if clicking on them
149
+ }
150
+ setTimeout(() => {
151
+ // Double-check that we're not clicking on suggestions
152
+ if (!this.suggestionsContainer.contains(document.activeElement)) {
153
+ this.isFocused = false;
154
+ this.updateSuggestionsVisibility();
155
+ this.selectedIndex = -1;
156
+ }
157
+ }, 100);
158
+ });
159
+ this.input.addEventListener('keydown', (e) => {
160
+ this.handleKeyDown(e);
161
+ });
162
+ }
163
+ handleInputChange(query) {
164
+ this.selectedIndex = -1;
165
+ if (this.debounceTimer) {
166
+ clearTimeout(this.debounceTimer);
167
+ }
168
+ // For rich suggestions, allow empty query to show trending products/brands
169
+ const isRichSuggestions = this.options.enableRichSuggestions;
170
+ const isEmptyQuery = query.trim().length === 0;
171
+ const hasMinLength = query.length >= this.options.minQueryLength;
172
+ if (!this.options.showSuggestions) {
173
+ this.suggestions = [];
174
+ this.renderSuggestions();
175
+ return;
176
+ }
177
+ // For basic suggestions, still require minQueryLength
178
+ // For rich suggestions, allow empty query
179
+ if (!isRichSuggestions && !hasMinLength) {
180
+ this.suggestions = [];
181
+ this.renderSuggestions();
182
+ return;
183
+ }
184
+ this.debounceTimer = setTimeout(async () => {
185
+ try {
186
+ if (this.options.enableRichSuggestions) {
187
+ // Fetch rich suggestions with dropdown recommendations
188
+ // Empty query will return trending products/brands
189
+ console.log('🚀 SearchBar: enableRichSuggestions is TRUE, calling fetchRichSuggestions for query:', query || '(empty)');
190
+ await this.fetchRichSuggestions(query);
191
+ }
192
+ else {
193
+ // Fallback to basic suggestions
194
+ console.log('⚠️ SearchBar: enableRichSuggestions is FALSE, using basic suggestions');
195
+ const response = await this.client.getSuggestions(query, this.options.maxSuggestions);
196
+ const rawSuggestions = Array.isArray(response) ? response : [];
197
+ this.suggestions = rawSuggestions.map((suggestion) => {
198
+ const suggestionQuery = (suggestion.query ?? suggestion.text ?? suggestion);
199
+ const count = (suggestion.popularity !== undefined
200
+ ? suggestion.popularity
201
+ : (suggestion.count !== undefined ? suggestion.count : undefined));
202
+ return {
203
+ query: typeof suggestionQuery === 'string' ? suggestionQuery : String(suggestionQuery),
204
+ count: typeof count === 'number' ? count : undefined,
205
+ metadata: suggestion,
206
+ };
207
+ });
208
+ this.richSuggestionsData = null;
209
+ }
210
+ this.renderSuggestions();
211
+ }
212
+ catch (err) {
213
+ log.error('Error fetching suggestions:', err);
214
+ this.suggestions = [];
215
+ this.richSuggestionsData = null;
216
+ this.renderSuggestions();
217
+ }
218
+ }, this.options.debounceMs);
219
+ }
220
+ handleKeyDown(e) {
221
+ switch (e.key) {
222
+ case 'ArrowDown':
223
+ e.preventDefault();
224
+ if (this.suggestions.length > 0) {
225
+ this.selectedIndex = this.selectedIndex < this.suggestions.length - 1
226
+ ? this.selectedIndex + 1
227
+ : this.selectedIndex;
228
+ this.renderSuggestions();
229
+ }
230
+ break;
231
+ case 'ArrowUp':
232
+ e.preventDefault();
233
+ if (this.suggestions.length > 0) {
234
+ this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : -1;
235
+ this.renderSuggestions();
236
+ }
237
+ break;
238
+ case 'Enter':
239
+ e.preventDefault();
240
+ if (this.suggestions.length > 0 && this.selectedIndex >= 0 && this.selectedIndex < this.suggestions.length) {
241
+ this.selectSuggestion(this.suggestions[this.selectedIndex].query);
242
+ }
243
+ else {
244
+ // Search with "*" if query is empty
245
+ this.performSearch();
246
+ }
247
+ break;
248
+ case 'Escape':
249
+ this.isFocused = false;
250
+ this.selectedIndex = -1;
251
+ this.updateSuggestionsVisibility();
252
+ this.input.blur();
253
+ break;
254
+ }
255
+ }
256
+ selectSuggestion(suggestion) {
257
+ this.input.value = suggestion;
258
+ this.selectedIndex = -1;
259
+ this.isFocused = false;
260
+ this.updateSuggestionsVisibility();
261
+ this.input.blur();
262
+ if (this.options.onSuggestionSelect) {
263
+ this.options.onSuggestionSelect(suggestion);
264
+ }
265
+ this.performSearch();
266
+ }
267
+ async fetchRichSuggestions(query) {
268
+ try {
269
+ // Use cached credentials first, then try to get from client
270
+ let baseUrl = 'https://api.seekora.com';
271
+ let storeId = this.cachedStoreId;
272
+ let readSecret = this.cachedReadSecret;
273
+ const env = this.cachedEnvironment;
274
+ // If not cached, try to get from client
275
+ const clientAny = this.client;
276
+ if (!storeId || !readSecret) {
277
+ if (clientAny.config) {
278
+ storeId = storeId || clientAny.config.storeId;
279
+ readSecret = readSecret || clientAny.config.readSecret;
280
+ }
281
+ else if (clientAny.storeId && clientAny.readSecret) {
282
+ storeId = storeId || clientAny.storeId;
283
+ readSecret = readSecret || clientAny.readSecret;
284
+ }
285
+ else if (clientAny._config) {
286
+ storeId = storeId || clientAny._config.storeId;
287
+ readSecret = readSecret || clientAny._config.readSecret;
288
+ }
289
+ else if (clientAny._storeId && clientAny._readSecret) {
290
+ storeId = storeId || clientAny._storeId;
291
+ readSecret = readSecret || clientAny._readSecret;
292
+ }
293
+ }
294
+ // Determine environment and set baseUrl
295
+ const environment = env || clientAny.environment || clientAny.config?.environment || clientAny._config?.environment;
296
+ const isDevelopment = environment === 'development';
297
+ // ALWAYS use localhost:3000 for development - FORCE IT
298
+ baseUrl = 'http://localhost:3000'; // Force localhost for all requests in dev
299
+ console.log('🌐 SearchBar: Setting baseUrl', {
300
+ environment,
301
+ isDevelopment,
302
+ forcedBaseUrl: baseUrl,
303
+ });
304
+ if (!storeId || !readSecret) {
305
+ log.warn('SearchBar: Missing store credentials, falling back to basic suggestions', {
306
+ hasConfig: !!clientAny.config,
307
+ hasDirectProps: !!(clientAny.storeId && clientAny.readSecret),
308
+ hasPrivateConfig: !!clientAny._config,
309
+ clientKeys: Object.keys(clientAny),
310
+ });
311
+ await this.fallbackToBasicSuggestions(query);
312
+ return;
313
+ }
314
+ log.verbose('SearchBar: Fetching rich suggestions', {
315
+ baseUrl,
316
+ storeId: storeId ? storeId.substring(0, 8) + '...' : 'missing',
317
+ query,
318
+ environment,
319
+ isDevelopment,
320
+ credentialsFromCache: !!(this.cachedStoreId && this.cachedReadSecret),
321
+ });
322
+ // Build API URL with rich recommendations
323
+ // Use 'query' parameter to match API expectations (also accepts 'q')
324
+ // Empty query is allowed - will return trending products/brands
325
+ const params = new URLSearchParams({
326
+ include_dropdown_recommendations: 'true',
327
+ hitsPerPage: String(this.options.maxSuggestions),
328
+ });
329
+ // Only add query parameter if it's not empty
330
+ if (query && query.trim().length > 0) {
331
+ params.append('query', query);
332
+ }
333
+ const url = `${baseUrl}/api/v1/suggestions/queries?${params.toString()}`;
334
+ log.verbose('SearchBar: Making rich suggestions request', { url: url.replace(/secret=[^&]+/, 'secret=***') });
335
+ const response = await fetch(url, {
336
+ method: 'GET',
337
+ headers: {
338
+ 'x-storeid': storeId,
339
+ 'x-storesecret': readSecret,
340
+ 'Content-Type': 'application/json',
341
+ },
342
+ });
343
+ if (!response.ok) {
344
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
345
+ }
346
+ const data = await response.json();
347
+ if (data.status === 'error') {
348
+ throw new Error('API returned error status');
349
+ }
350
+ this.richSuggestionsData = data;
351
+ // Transform hits to SuggestionItem format
352
+ this.suggestions = (data.data.hits || []).map((hit) => ({
353
+ query: hit.query,
354
+ count: hit.popularity,
355
+ metadata: hit,
356
+ }));
357
+ console.log('✨ SearchBar: Rich suggestions loaded', {
358
+ query,
359
+ suggestionsCount: this.suggestions.length,
360
+ hasTrendingSearches: !!data.data.dropdown_recommendations?.trending_searches?.length,
361
+ hasTrendingProducts: !!data.data.dropdown_recommendations?.trending_products?.length,
362
+ hasPopularBrands: !!data.data.dropdown_recommendations?.popular_brands?.length,
363
+ rawData: data.data.dropdown_recommendations,
364
+ });
365
+ }
366
+ catch (err) {
367
+ const error = err instanceof Error ? err : new Error(String(err));
368
+ log.error('SearchBar: Failed to fetch rich suggestions, falling back to basic', {
369
+ error: error.message,
370
+ stack: error.stack,
371
+ query,
372
+ });
373
+ await this.fallbackToBasicSuggestions(query);
374
+ }
375
+ }
376
+ async fallbackToBasicSuggestions(query) {
377
+ try {
378
+ const response = await this.client.getSuggestions(query, this.options.maxSuggestions);
379
+ const rawSuggestions = Array.isArray(response) ? response : [];
380
+ this.suggestions = rawSuggestions.map((suggestion) => {
381
+ const suggestionQuery = (suggestion.query ?? suggestion.text ?? suggestion);
382
+ const count = (suggestion.popularity !== undefined
383
+ ? suggestion.popularity
384
+ : (suggestion.count !== undefined ? suggestion.count : undefined));
385
+ return {
386
+ query: typeof suggestionQuery === 'string' ? suggestionQuery : String(suggestionQuery),
387
+ count: typeof count === 'number' ? count : undefined,
388
+ metadata: suggestion,
389
+ };
390
+ });
391
+ this.richSuggestionsData = null;
392
+ }
393
+ catch (fallbackErr) {
394
+ log.error('SearchBar: Fallback suggestions also failed', fallbackErr);
395
+ this.suggestions = [];
396
+ this.richSuggestionsData = null;
397
+ }
398
+ }
399
+ cacheClientCredentials() {
400
+ const clientAny = this.client;
401
+ console.log('🔍 SearchBar: Attempting to cache credentials', {
402
+ hasConfig: !!clientAny.config,
403
+ hasDirectProps: !!(clientAny.storeId && clientAny.readSecret),
404
+ hasPrivateConfig: !!clientAny._config,
405
+ hasPrivateProps: !!(clientAny._storeId && clientAny._readSecret),
406
+ clientKeys: Object.keys(clientAny).slice(0, 10),
407
+ // Try to see actual values
408
+ configStoreId: clientAny.config?.storeId,
409
+ directStoreId: clientAny.storeId,
410
+ });
411
+ // Try multiple ways to extract credentials - check ALL properties
412
+ if (clientAny.config && clientAny.config.storeId) {
413
+ this.cachedStoreId = clientAny.config.storeId;
414
+ this.cachedReadSecret = clientAny.config.readSecret;
415
+ this.cachedEnvironment = clientAny.config.environment;
416
+ console.log('📍 Extracted from config');
417
+ }
418
+ if (!this.cachedStoreId && clientAny.storeId) {
419
+ this.cachedStoreId = clientAny.storeId;
420
+ this.cachedReadSecret = clientAny.readSecret;
421
+ this.cachedEnvironment = clientAny.environment;
422
+ console.log('📍 Extracted from direct props');
423
+ }
424
+ if (!this.cachedStoreId && clientAny._config) {
425
+ this.cachedStoreId = clientAny._config.storeId;
426
+ this.cachedReadSecret = clientAny._config.readSecret;
427
+ this.cachedEnvironment = clientAny._config.environment;
428
+ console.log('📍 Extracted from _config');
429
+ }
430
+ if (!this.cachedStoreId && clientAny._storeId) {
431
+ this.cachedStoreId = clientAny._storeId;
432
+ this.cachedReadSecret = clientAny._readSecret;
433
+ this.cachedEnvironment = clientAny._environment || clientAny.environment;
434
+ console.log('📍 Extracted from _private props');
435
+ }
436
+ console.log('✅ SearchBar: Cached client credentials', {
437
+ hasStoreId: !!this.cachedStoreId,
438
+ storeIdPreview: this.cachedStoreId ? this.cachedStoreId.substring(0, 8) + '...' : 'MISSING',
439
+ hasReadSecret: !!this.cachedReadSecret,
440
+ environment: this.cachedEnvironment,
441
+ });
442
+ }
443
+ async performSearch() {
444
+ // Use empty string for empty queries
445
+ const query = this.input.value.trim();
446
+ log.verbose('SearchBar: Setting query in state manager', { query });
447
+ // Update state manager - this will automatically trigger search
448
+ // State manager handles debouncing and search triggering
449
+ this.provider.stateManager.setQuery(query);
450
+ // Note: onSearch callback will be called via state manager subscription
451
+ }
452
+ renderSuggestions() {
453
+ this.suggestionsContainer.innerHTML = '';
454
+ const hasRichData = this.richSuggestionsData?.data?.dropdown_recommendations;
455
+ const recommendations = hasRichData ? this.richSuggestionsData.data.dropdown_recommendations : null;
456
+ console.log('🎨 SearchBar: renderSuggestions called', {
457
+ hasRichData,
458
+ recommendationsKeys: recommendations ? Object.keys(recommendations) : [],
459
+ trendingSearchesCount: recommendations?.trending_searches?.length || 0,
460
+ trendingProductsCount: recommendations?.trending_products?.length || 0,
461
+ popularBrandsCount: recommendations?.popular_brands?.length || 0,
462
+ suggestionsCount: this.suggestions.length,
463
+ });
464
+ // Render Query Suggestions Section
465
+ const displayedSuggestions = this.suggestions.slice(0, this.options.maxSuggestions);
466
+ if (displayedSuggestions.length > 0) {
467
+ const suggestionsSection = document.createElement('div');
468
+ suggestionsSection.style.cssText = this.getSectionStyle();
469
+ const sectionTitle = document.createElement('div');
470
+ sectionTitle.style.cssText = this.getSectionTitleStyle();
471
+ sectionTitle.textContent = 'Suggestions';
472
+ suggestionsSection.appendChild(sectionTitle);
473
+ const suggestionsList = document.createElement('div');
474
+ suggestionsList.style.cssText = this.getSuggestionsListStyle();
475
+ displayedSuggestions.forEach((suggestion, index) => {
476
+ const item = document.createElement('div');
477
+ item.style.cssText = this.getSuggestionItemStyle(index === this.selectedIndex);
478
+ const querySpan = document.createElement('span');
479
+ querySpan.textContent = suggestion.query;
480
+ item.appendChild(querySpan);
481
+ if (suggestion.count !== undefined) {
482
+ const countSpan = document.createElement('span');
483
+ countSpan.textContent = ` (${suggestion.count})`;
484
+ countSpan.style.cssText = this.getCountStyle();
485
+ item.appendChild(countSpan);
486
+ }
487
+ // Use mousedown instead of click to fire before blur event
488
+ item.addEventListener('mousedown', (e) => {
489
+ e.preventDefault(); // Prevent input blur
490
+ this.selectSuggestion(suggestion.query);
491
+ });
492
+ // Also handle click for accessibility
493
+ item.addEventListener('click', (e) => {
494
+ e.preventDefault();
495
+ this.selectSuggestion(suggestion.query);
496
+ });
497
+ suggestionsList.appendChild(item);
498
+ });
499
+ suggestionsSection.appendChild(suggestionsList);
500
+ this.suggestionsContainer.appendChild(suggestionsSection);
501
+ }
502
+ // Render Trending Searches Section
503
+ if (recommendations?.trending_searches && recommendations.trending_searches.length > 0) {
504
+ const trendingSection = document.createElement('div');
505
+ trendingSection.style.cssText = this.getSectionStyle();
506
+ const sectionTitle = document.createElement('div');
507
+ sectionTitle.style.cssText = this.getSectionTitleStyle();
508
+ sectionTitle.textContent = 'Trending Searches';
509
+ trendingSection.appendChild(sectionTitle);
510
+ const trendingList = document.createElement('div');
511
+ trendingList.style.cssText = this.getSuggestionsListStyle();
512
+ recommendations.trending_searches.forEach((trend) => {
513
+ const item = document.createElement('div');
514
+ item.style.cssText = this.getSuggestionItemStyle(false);
515
+ const querySpan = document.createElement('span');
516
+ querySpan.textContent = trend.query;
517
+ item.appendChild(querySpan);
518
+ if (trend.searches !== undefined) {
519
+ const countSpan = document.createElement('span');
520
+ countSpan.textContent = ` (${trend.searches})`;
521
+ countSpan.style.cssText = this.getCountStyle();
522
+ item.appendChild(countSpan);
523
+ }
524
+ item.addEventListener('mousedown', (e) => {
525
+ e.preventDefault();
526
+ this.selectSuggestion(trend.query);
527
+ });
528
+ item.addEventListener('click', (e) => {
529
+ e.preventDefault();
530
+ this.selectSuggestion(trend.query);
531
+ });
532
+ trendingList.appendChild(item);
533
+ });
534
+ trendingSection.appendChild(trendingList);
535
+ this.suggestionsContainer.appendChild(trendingSection);
536
+ }
537
+ // Render Trending Products Section
538
+ if (recommendations?.trending_products && recommendations.trending_products.length > 0) {
539
+ const productsSection = document.createElement('div');
540
+ productsSection.style.cssText = this.getSectionStyle();
541
+ const sectionTitle = document.createElement('div');
542
+ sectionTitle.style.cssText = this.getSectionTitleStyle();
543
+ sectionTitle.textContent = 'Trending Products';
544
+ productsSection.appendChild(sectionTitle);
545
+ const productsGrid = document.createElement('div');
546
+ productsGrid.style.cssText = this.getProductsGridStyle();
547
+ recommendations.trending_products.forEach((product) => {
548
+ const productCard = document.createElement('div');
549
+ productCard.style.cssText = this.getProductCardStyle();
550
+ if (product.image) {
551
+ const img = document.createElement('img');
552
+ img.src = product.image;
553
+ img.alt = product.title;
554
+ img.style.cssText = this.getProductImageStyle();
555
+ img.onerror = () => {
556
+ img.style.display = 'none';
557
+ };
558
+ productCard.appendChild(img);
559
+ }
560
+ const productInfo = document.createElement('div');
561
+ productInfo.style.cssText = this.getProductInfoStyle();
562
+ const title = document.createElement('div');
563
+ title.style.cssText = this.getProductTitleStyle();
564
+ title.textContent = product.title;
565
+ productInfo.appendChild(title);
566
+ if (product.price !== undefined) {
567
+ const price = document.createElement('div');
568
+ price.style.cssText = this.getProductPriceStyle();
569
+ const currency = product.currency || '$';
570
+ price.textContent = `${currency}${product.price}`;
571
+ productInfo.appendChild(price);
572
+ }
573
+ productCard.appendChild(productInfo);
574
+ productCard.addEventListener('mousedown', (e) => {
575
+ e.preventDefault();
576
+ if (this.options.onProductClick) {
577
+ this.options.onProductClick(product.id, product);
578
+ }
579
+ else if (product.url) {
580
+ window.location.href = product.url;
581
+ }
582
+ });
583
+ productCard.addEventListener('click', (e) => {
584
+ e.preventDefault();
585
+ if (this.options.onProductClick) {
586
+ this.options.onProductClick(product.id, product);
587
+ }
588
+ else if (product.url) {
589
+ window.location.href = product.url;
590
+ }
591
+ });
592
+ productsGrid.appendChild(productCard);
593
+ });
594
+ productsSection.appendChild(productsGrid);
595
+ this.suggestionsContainer.appendChild(productsSection);
596
+ }
597
+ // Render Popular Brands Section
598
+ if (recommendations?.popular_brands && recommendations.popular_brands.length > 0) {
599
+ const brandsSection = document.createElement('div');
600
+ brandsSection.style.cssText = this.getSectionStyle();
601
+ const sectionTitle = document.createElement('div');
602
+ sectionTitle.style.cssText = this.getSectionTitleStyle();
603
+ sectionTitle.textContent = 'Popular Brands';
604
+ brandsSection.appendChild(sectionTitle);
605
+ const brandsList = document.createElement('div');
606
+ brandsList.style.cssText = this.getBrandsListStyle();
607
+ recommendations.popular_brands.forEach((brand) => {
608
+ const brandItem = document.createElement('div');
609
+ brandItem.style.cssText = this.getBrandItemStyle();
610
+ if (brand.logo) {
611
+ const img = document.createElement('img');
612
+ img.src = brand.logo;
613
+ img.alt = brand.name;
614
+ img.style.cssText = this.getBrandLogoStyle();
615
+ img.onerror = () => {
616
+ // Fallback to text if image fails
617
+ img.style.display = 'none';
618
+ const textSpan = document.createElement('span');
619
+ textSpan.textContent = brand.name;
620
+ textSpan.style.cssText = this.getBrandNameStyle();
621
+ brandItem.appendChild(textSpan);
622
+ };
623
+ brandItem.appendChild(img);
624
+ }
625
+ else {
626
+ const textSpan = document.createElement('span');
627
+ textSpan.textContent = brand.name;
628
+ textSpan.style.cssText = this.getBrandNameStyle();
629
+ brandItem.appendChild(textSpan);
630
+ }
631
+ brandItem.addEventListener('mousedown', (e) => {
632
+ e.preventDefault();
633
+ if (this.options.onBrandClick) {
634
+ this.options.onBrandClick(brand.name, brand);
635
+ }
636
+ else {
637
+ this.selectSuggestion(brand.name);
638
+ }
639
+ });
640
+ brandItem.addEventListener('click', (e) => {
641
+ e.preventDefault();
642
+ if (this.options.onBrandClick) {
643
+ this.options.onBrandClick(brand.name, brand);
644
+ }
645
+ else {
646
+ this.selectSuggestion(brand.name);
647
+ }
648
+ });
649
+ brandsList.appendChild(brandItem);
650
+ });
651
+ brandsSection.appendChild(brandsList);
652
+ this.suggestionsContainer.appendChild(brandsSection);
653
+ }
654
+ this.updateSuggestionsVisibility();
655
+ }
656
+ updateSuggestionsVisibility() {
657
+ // Show dropdown if there are suggestions OR rich data (trending products/brands)
658
+ const hasRichData = this.richSuggestionsData?.data?.dropdown_recommendations;
659
+ const hasTrendingProducts = (hasRichData?.trending_products?.length ?? 0) > 0;
660
+ const hasTrendingSearches = (hasRichData?.trending_searches?.length ?? 0) > 0;
661
+ const hasPopularBrands = (hasRichData?.popular_brands?.length ?? 0) > 0;
662
+ const hasAnyContent = this.suggestions.length > 0 || hasTrendingProducts || hasTrendingSearches || hasPopularBrands;
663
+ // For rich suggestions, allow showing even with empty query if there's rich data
664
+ const queryLength = this.input.value.length;
665
+ const hasMinLength = queryLength >= this.options.minQueryLength;
666
+ const isEmptyWithRichData = queryLength === 0 && (hasTrendingProducts || hasTrendingSearches || hasPopularBrands);
667
+ const canShow = hasMinLength || (this.options.enableRichSuggestions && isEmptyWithRichData);
668
+ const shouldShow = this.isFocused &&
669
+ this.options.showSuggestions &&
670
+ canShow &&
671
+ hasAnyContent;
672
+ console.log('👁️ updateSuggestionsVisibility', {
673
+ isFocused: this.isFocused,
674
+ hasAnyContent,
675
+ suggestionsCount: this.suggestions.length,
676
+ hasTrendingProducts,
677
+ hasPopularBrands,
678
+ queryLength,
679
+ isEmptyWithRichData,
680
+ canShow,
681
+ shouldShow,
682
+ });
683
+ this.suggestionsContainer.style.display = shouldShow ? 'block' : 'none';
684
+ }
685
+ getInputStyle() {
686
+ const theme = this.provider.theme;
687
+ return `
688
+ flex: 1;
689
+ padding: ${theme.spacing.medium};
690
+ font-size: ${theme.typography.fontSize.medium};
691
+ font-family: ${theme.typography.fontFamily};
692
+ background-color: ${theme.colors.background};
693
+ color: ${theme.colors.text};
694
+ border: 1px solid ${theme.colors.border};
695
+ border-radius: ${typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium};
696
+ outline: none;
697
+ `;
698
+ }
699
+ getResponseTimeStyle() {
700
+ const theme = this.provider.theme;
701
+ return `
702
+ margin-left: ${theme.spacing.small};
703
+ font-size: ${theme.typography.fontSize.small};
704
+ color: ${theme.colors.text};
705
+ opacity: 0.7;
706
+ white-space: nowrap;
707
+ `;
708
+ }
709
+ getSuggestionsContainerStyle() {
710
+ const theme = this.provider.theme;
711
+ return `
712
+ position: absolute;
713
+ top: 100%;
714
+ left: 0;
715
+ right: 0;
716
+ margin-top: ${theme.spacing.small};
717
+ background-color: ${theme.colors.background};
718
+ border: 1px solid ${theme.colors.border};
719
+ border-radius: ${typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium};
720
+ box-shadow: ${theme.shadows.medium};
721
+ max-height: 600px;
722
+ overflow-y: auto;
723
+ z-index: 1000;
724
+ width: 100%;
725
+ `;
726
+ }
727
+ getSuggestionItemStyle(isSelected) {
728
+ const theme = this.provider.theme;
729
+ return `
730
+ padding: ${theme.spacing.medium};
731
+ cursor: pointer;
732
+ background-color: ${isSelected ? theme.colors.hover : 'transparent'};
733
+ color: ${theme.colors.text};
734
+ font-family: ${theme.typography.fontFamily};
735
+ transition: ${theme.transitions?.fast || '150ms ease-in-out'};
736
+ `;
737
+ }
738
+ getSectionStyle() {
739
+ const theme = this.provider.theme;
740
+ return `
741
+ padding: ${theme.spacing.large || '1.5rem'} ${theme.spacing.medium};
742
+ border-bottom: 1px solid ${theme.colors.border};
743
+ `;
744
+ }
745
+ getSectionTitleStyle() {
746
+ const theme = this.provider.theme;
747
+ return `
748
+ font-size: ${theme.typography.fontSize.small};
749
+ font-weight: 600;
750
+ text-transform: uppercase;
751
+ letter-spacing: 0.5px;
752
+ color: ${theme.colors.textSecondary || theme.colors.text};
753
+ opacity: 0.7;
754
+ margin-bottom: ${theme.spacing.medium};
755
+ `;
756
+ }
757
+ getSuggestionsListStyle() {
758
+ return `
759
+ display: flex;
760
+ flex-direction: column;
761
+ `;
762
+ }
763
+ getCountStyle() {
764
+ const theme = this.provider.theme;
765
+ return `
766
+ opacity: 0.6;
767
+ margin-left: ${theme.spacing.small};
768
+ font-size: ${theme.typography.fontSize.small};
769
+ `;
770
+ }
771
+ getProductsGridStyle() {
772
+ return `
773
+ display: grid;
774
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
775
+ gap: ${this.provider.theme.spacing.medium};
776
+ max-height: 300px;
777
+ overflow-y: auto;
778
+ `;
779
+ }
780
+ getProductCardStyle() {
781
+ const theme = this.provider.theme;
782
+ const borderRadius = typeof theme.borderRadius === 'string'
783
+ ? theme.borderRadius
784
+ : theme.borderRadius.medium;
785
+ return `
786
+ display: flex;
787
+ flex-direction: column;
788
+ cursor: pointer;
789
+ border: 1px solid ${theme.colors.border};
790
+ border-radius: ${borderRadius};
791
+ overflow: hidden;
792
+ transition: ${theme.transitions?.fast || '150ms ease-in-out'};
793
+ background-color: ${theme.colors.background};
794
+ `;
795
+ }
796
+ getProductImageStyle() {
797
+ return `
798
+ width: 100%;
799
+ height: 120px;
800
+ object-fit: cover;
801
+ background-color: #f0f0f0;
802
+ `;
803
+ }
804
+ getProductInfoStyle() {
805
+ const theme = this.provider.theme;
806
+ return `
807
+ padding: ${theme.spacing.small} ${theme.spacing.medium};
808
+ display: flex;
809
+ flex-direction: column;
810
+ gap: ${theme.spacing.xsmall ?? theme.spacing.small ?? '0.25rem'};
811
+ `;
812
+ }
813
+ getProductTitleStyle() {
814
+ const theme = this.provider.theme;
815
+ return `
816
+ font-size: ${theme.typography.fontSize.small};
817
+ font-weight: 500;
818
+ color: ${theme.colors.text};
819
+ line-height: 1.4;
820
+ overflow: hidden;
821
+ text-overflow: ellipsis;
822
+ display: -webkit-box;
823
+ -webkit-line-clamp: 2;
824
+ -webkit-box-orient: vertical;
825
+ `;
826
+ }
827
+ getProductPriceStyle() {
828
+ const theme = this.provider.theme;
829
+ return `
830
+ font-size: ${theme.typography.fontSize.medium};
831
+ font-weight: 600;
832
+ color: ${theme.colors.primary || theme.colors.text};
833
+ `;
834
+ }
835
+ getBrandsListStyle() {
836
+ return `
837
+ display: flex;
838
+ flex-wrap: wrap;
839
+ gap: ${this.provider.theme.spacing.medium};
840
+ max-height: 200px;
841
+ overflow-y: auto;
842
+ `;
843
+ }
844
+ getBrandItemStyle() {
845
+ const theme = this.provider.theme;
846
+ const borderRadius = typeof theme.borderRadius === 'string'
847
+ ? theme.borderRadius
848
+ : theme.borderRadius.medium;
849
+ return `
850
+ display: flex;
851
+ align-items: center;
852
+ justify-content: center;
853
+ cursor: pointer;
854
+ padding: ${theme.spacing.small};
855
+ border: 1px solid ${theme.colors.border};
856
+ border-radius: ${borderRadius};
857
+ transition: ${theme.transitions?.fast || '150ms ease-in-out'};
858
+ background-color: ${theme.colors.background};
859
+ min-width: 80px;
860
+ min-height: 60px;
861
+ `;
862
+ }
863
+ getBrandLogoStyle() {
864
+ return `
865
+ max-width: 60px;
866
+ max-height: 40px;
867
+ object-fit: contain;
868
+ `;
869
+ }
870
+ getBrandNameStyle() {
871
+ const theme = this.provider.theme;
872
+ return `
873
+ font-size: ${theme.typography.fontSize.small};
874
+ font-weight: 500;
875
+ color: ${theme.colors.text};
876
+ text-align: center;
877
+ `;
878
+ }
879
+ /**
880
+ * Trigger a search programmatically
881
+ * @param query - Optional query string. If not provided, uses current input value or "*" for all
882
+ */
883
+ async search(query) {
884
+ if (query !== undefined) {
885
+ this.input.value = query;
886
+ }
887
+ await this.performSearch();
888
+ }
889
+ destroy() {
890
+ if (this.debounceTimer) {
891
+ clearTimeout(this.debounceTimer);
892
+ }
893
+ if (this.unsubscribeStateManager) {
894
+ this.unsubscribeStateManager();
895
+ this.unsubscribeStateManager = null;
896
+ }
897
+ this.container.innerHTML = '';
898
+ }
899
+ }