@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.
- package/dist/components/clear-refinements.d.ts +39 -0
- package/dist/components/clear-refinements.d.ts.map +1 -0
- package/dist/components/clear-refinements.js +133 -0
- package/dist/components/current-refinements.d.ts +36 -0
- package/dist/components/current-refinements.d.ts.map +1 -0
- package/dist/components/current-refinements.js +186 -0
- package/dist/components/facets.d.ts +45 -0
- package/dist/components/facets.d.ts.map +1 -0
- package/dist/components/facets.js +259 -0
- package/dist/components/hits-per-page.d.ts +37 -0
- package/dist/components/hits-per-page.d.ts.map +1 -0
- package/dist/components/hits-per-page.js +132 -0
- package/dist/components/infinite-hits.d.ts +61 -0
- package/dist/components/infinite-hits.d.ts.map +1 -0
- package/dist/components/infinite-hits.js +316 -0
- package/dist/components/pagination.d.ts +33 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/pagination.js +364 -0
- package/dist/components/query-suggestions.d.ts +39 -0
- package/dist/components/query-suggestions.d.ts.map +1 -0
- package/dist/components/query-suggestions.js +217 -0
- package/dist/components/range-input.d.ts +42 -0
- package/dist/components/range-input.d.ts.map +1 -0
- package/dist/components/range-input.js +274 -0
- package/dist/components/search-bar.d.ts +140 -0
- package/dist/components/search-bar.d.ts.map +1 -0
- package/dist/components/search-bar.js +899 -0
- package/dist/components/search-layout.d.ts +35 -0
- package/dist/components/search-layout.d.ts.map +1 -0
- package/dist/components/search-layout.js +144 -0
- package/dist/components/search-provider.d.ts +28 -0
- package/dist/components/search-provider.d.ts.map +1 -0
- package/dist/components/search-provider.js +44 -0
- package/dist/components/search-results.d.ts +55 -0
- package/dist/components/search-results.d.ts.map +1 -0
- package/dist/components/search-results.js +537 -0
- package/dist/components/sort-by.d.ts +33 -0
- package/dist/components/sort-by.d.ts.map +1 -0
- package/dist/components/sort-by.js +122 -0
- package/dist/components/stats.d.ts +36 -0
- package/dist/components/stats.d.ts.map +1 -0
- package/dist/components/stats.js +138 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +4008 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +4055 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +1 -0
- package/dist/themes/createTheme.d.ts +8 -0
- package/dist/themes/createTheme.d.ts.map +1 -0
- package/dist/themes/createTheme.js +10 -0
- package/dist/themes/dark.d.ts +6 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +34 -0
- package/dist/themes/default.d.ts +6 -0
- package/dist/themes/default.d.ts.map +1 -0
- package/dist/themes/default.js +71 -0
- package/dist/themes/mergeThemes.d.ts +7 -0
- package/dist/themes/mergeThemes.d.ts.map +1 -0
- package/dist/themes/mergeThemes.js +6 -0
- package/dist/themes/minimal.d.ts +6 -0
- package/dist/themes/minimal.d.ts.map +1 -0
- package/dist/themes/minimal.js +34 -0
- package/dist/themes/types.d.ts +7 -0
- package/dist/themes/types.d.ts.map +1 -0
- package/dist/themes/types.js +6 -0
- package/dist/utils/search-manager.d.ts +33 -0
- package/dist/utils/search-manager.d.ts.map +1 -0
- package/dist/utils/search-manager.js +89 -0
- 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
|
+
}
|