@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,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchResults Component
|
|
3
|
+
*
|
|
4
|
+
* Displays search results with customizable rendering
|
|
5
|
+
*/
|
|
6
|
+
import { extractField, formatPrice, log } from '@seekora-ai/ui-sdk-core';
|
|
7
|
+
export class SearchResults {
|
|
8
|
+
constructor(provider, options) {
|
|
9
|
+
this.unsubscribeStateManager = null;
|
|
10
|
+
this.provider = provider;
|
|
11
|
+
const container = typeof options.container === 'string'
|
|
12
|
+
? document.querySelector(options.container)
|
|
13
|
+
: options.container;
|
|
14
|
+
if (!container) {
|
|
15
|
+
throw new Error('SearchResults: container element not found');
|
|
16
|
+
}
|
|
17
|
+
this.container = container;
|
|
18
|
+
this.options = {
|
|
19
|
+
viewMode: options.viewMode || 'list',
|
|
20
|
+
fieldMapping: options.fieldMapping || {},
|
|
21
|
+
itemsPerPage: options.itemsPerPage || 10,
|
|
22
|
+
results: options.results,
|
|
23
|
+
loading: options.loading,
|
|
24
|
+
error: options.error,
|
|
25
|
+
onResultClick: options.onResultClick,
|
|
26
|
+
renderResult: options.renderResult,
|
|
27
|
+
renderEmpty: options.renderEmpty,
|
|
28
|
+
renderLoading: options.renderLoading,
|
|
29
|
+
renderError: options.renderError,
|
|
30
|
+
};
|
|
31
|
+
// Attach event delegation listener to the persistent container for result clicks
|
|
32
|
+
// This ensures clicks work even when results are recreated during re-renders
|
|
33
|
+
// Note: We attach the listener even if onResultClick is not provided, to support analytics
|
|
34
|
+
this.container.addEventListener('click', async (e) => {
|
|
35
|
+
const target = e.target;
|
|
36
|
+
// Find the result element (could be the target itself or a parent)
|
|
37
|
+
const resultElement = target.hasAttribute('data-result-id')
|
|
38
|
+
? target
|
|
39
|
+
: target.closest('[data-result-id]');
|
|
40
|
+
if (resultElement) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
const resultId = resultElement.getAttribute('data-result-id');
|
|
44
|
+
const resultIndexStr = resultElement.getAttribute('data-result-index');
|
|
45
|
+
const resultIndex = resultIndexStr ? parseInt(resultIndexStr, 10) : -1;
|
|
46
|
+
console.log('🔵 SearchResults: Result clicked via delegation', {
|
|
47
|
+
resultId,
|
|
48
|
+
resultIndex,
|
|
49
|
+
target: target.tagName,
|
|
50
|
+
resultElement
|
|
51
|
+
});
|
|
52
|
+
if (resultId && resultIndex >= 0) {
|
|
53
|
+
// Find the result in the current results
|
|
54
|
+
const results = this.extractResults();
|
|
55
|
+
const result = results[resultIndex];
|
|
56
|
+
if (result) {
|
|
57
|
+
const extractedResult = this.extractFields(result);
|
|
58
|
+
// Track analytics event if enabled
|
|
59
|
+
if (this.provider.enableAnalytics) {
|
|
60
|
+
try {
|
|
61
|
+
const searchResponse = this.provider.stateManager.getResults();
|
|
62
|
+
const state = this.provider.stateManager.getState();
|
|
63
|
+
// Calculate absolute position (1-based) accounting for pagination
|
|
64
|
+
// Position = (currentPage - 1) * itemsPerPage + resultIndex + 1
|
|
65
|
+
const absolutePosition = (state.currentPage - 1) * state.itemsPerPage + resultIndex + 1;
|
|
66
|
+
// Build search context from current state and response
|
|
67
|
+
const searchContext = searchResponse?.context || (searchResponse ? {
|
|
68
|
+
query: state.query,
|
|
69
|
+
filters: state.refinements.length > 0
|
|
70
|
+
? state.refinements.map(r => `${r.field}:${r.value}`).join(' && ')
|
|
71
|
+
: undefined,
|
|
72
|
+
page: state.currentPage,
|
|
73
|
+
sortBy: state.sortBy,
|
|
74
|
+
} : undefined);
|
|
75
|
+
console.log('🔵 SearchResults: Tracking analytics event', {
|
|
76
|
+
resultId,
|
|
77
|
+
resultIndex,
|
|
78
|
+
absolutePosition,
|
|
79
|
+
currentPage: state.currentPage,
|
|
80
|
+
itemsPerPage: state.itemsPerPage,
|
|
81
|
+
hasContext: !!searchContext,
|
|
82
|
+
searchContext
|
|
83
|
+
});
|
|
84
|
+
await this.provider.client.trackEvent({
|
|
85
|
+
event_name: 'product_click',
|
|
86
|
+
clicked_item_id: resultId,
|
|
87
|
+
metadata: {
|
|
88
|
+
result: extractedResult,
|
|
89
|
+
position: absolutePosition,
|
|
90
|
+
},
|
|
91
|
+
}, searchContext);
|
|
92
|
+
console.log('🟢 SearchResults: Analytics event tracked successfully', {
|
|
93
|
+
resultId,
|
|
94
|
+
position: absolutePosition,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
99
|
+
log.error('SearchResults: Error tracking analytics event', {
|
|
100
|
+
resultId,
|
|
101
|
+
error: error.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Call user-provided callback
|
|
106
|
+
if (this.options.onResultClick) {
|
|
107
|
+
try {
|
|
108
|
+
console.log('🔵 SearchResults: Calling onResultClick callback', {
|
|
109
|
+
resultId,
|
|
110
|
+
resultIndex,
|
|
111
|
+
extractedResult
|
|
112
|
+
});
|
|
113
|
+
this.options.onResultClick(extractedResult, resultIndex);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
117
|
+
log.error('SearchResults: Error in onResultClick callback', {
|
|
118
|
+
resultId,
|
|
119
|
+
resultIndex,
|
|
120
|
+
error: error.message,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, { passive: false });
|
|
128
|
+
console.log('🔵 SearchResults: Event delegation listener attached to container', { container: this.container });
|
|
129
|
+
// Subscribe to state manager for automatic updates
|
|
130
|
+
this.unsubscribeStateManager = this.provider.stateManager.subscribe((state) => {
|
|
131
|
+
this.options.results = state.results;
|
|
132
|
+
this.options.loading = state.loading;
|
|
133
|
+
this.options.error = state.error;
|
|
134
|
+
this.render();
|
|
135
|
+
});
|
|
136
|
+
this.render();
|
|
137
|
+
}
|
|
138
|
+
destroy() {
|
|
139
|
+
if (this.unsubscribeStateManager) {
|
|
140
|
+
this.unsubscribeStateManager();
|
|
141
|
+
this.unsubscribeStateManager = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
update(options) {
|
|
145
|
+
// Update options
|
|
146
|
+
if (options.results !== undefined) {
|
|
147
|
+
this.options.results = options.results;
|
|
148
|
+
}
|
|
149
|
+
if (options.loading !== undefined) {
|
|
150
|
+
this.options.loading = options.loading;
|
|
151
|
+
}
|
|
152
|
+
if (options.error !== undefined) {
|
|
153
|
+
this.options.error = options.error;
|
|
154
|
+
}
|
|
155
|
+
// Always re-render when update is called (even if values appear the same, object references may differ)
|
|
156
|
+
const resultsCount = this.extractResults().length;
|
|
157
|
+
log.verbose('SearchResults: Updating and re-rendering', {
|
|
158
|
+
hasResults: !!this.options.results,
|
|
159
|
+
loading: this.options.loading,
|
|
160
|
+
hasError: !!this.options.error,
|
|
161
|
+
resultsCount,
|
|
162
|
+
providedResults: options.results !== undefined,
|
|
163
|
+
providedLoading: options.loading !== undefined,
|
|
164
|
+
providedError: options.error !== undefined,
|
|
165
|
+
});
|
|
166
|
+
this.render();
|
|
167
|
+
}
|
|
168
|
+
render() {
|
|
169
|
+
// Clear container first
|
|
170
|
+
this.container.innerHTML = '';
|
|
171
|
+
log.verbose('SearchResults: Rendering', {
|
|
172
|
+
hasResults: !!this.options.results,
|
|
173
|
+
loading: this.options.loading,
|
|
174
|
+
hasError: !!this.options.error,
|
|
175
|
+
});
|
|
176
|
+
if (this.options.loading) {
|
|
177
|
+
const loadingEl = this.options.renderLoading
|
|
178
|
+
? this.options.renderLoading()
|
|
179
|
+
: this.renderDefaultLoading();
|
|
180
|
+
this.container.appendChild(loadingEl);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (this.options.error) {
|
|
184
|
+
const errorEl = this.options.renderError
|
|
185
|
+
? this.options.renderError(this.options.error)
|
|
186
|
+
: this.renderDefaultError(this.options.error);
|
|
187
|
+
this.container.appendChild(errorEl);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const displayedResults = this.extractResults();
|
|
191
|
+
if (displayedResults.length === 0) {
|
|
192
|
+
const emptyEl = this.options.renderEmpty
|
|
193
|
+
? this.options.renderEmpty()
|
|
194
|
+
: this.renderDefaultEmpty();
|
|
195
|
+
this.container.appendChild(emptyEl);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Create a wrapper for grid/card layout
|
|
199
|
+
if (this.options.viewMode === 'card' || this.options.viewMode === 'grid') {
|
|
200
|
+
const gridWrapper = document.createElement('div');
|
|
201
|
+
gridWrapper.style.cssText = this.getGridContainerStyle();
|
|
202
|
+
displayedResults.forEach((result, index) => {
|
|
203
|
+
const resultEl = this.options.renderResult
|
|
204
|
+
? this.options.renderResult(this.extractFields(result), index)
|
|
205
|
+
: this.renderDefaultResult(this.extractFields(result), index);
|
|
206
|
+
gridWrapper.appendChild(resultEl);
|
|
207
|
+
});
|
|
208
|
+
this.container.appendChild(gridWrapper);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// List view - no wrapper needed
|
|
212
|
+
displayedResults.forEach((result, index) => {
|
|
213
|
+
const resultEl = this.options.renderResult
|
|
214
|
+
? this.options.renderResult(this.extractFields(result), index)
|
|
215
|
+
: this.renderDefaultResult(this.extractFields(result), index);
|
|
216
|
+
this.container.appendChild(resultEl);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
extractResults() {
|
|
221
|
+
if (!this.options.results) {
|
|
222
|
+
log.verbose('SearchResults: No results to extract');
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
let extracted = [];
|
|
226
|
+
if (Array.isArray(this.options.results)) {
|
|
227
|
+
extracted = this.options.results;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const res = this.options.results;
|
|
231
|
+
if (res.results && Array.isArray(res.results)) {
|
|
232
|
+
extracted = res.results;
|
|
233
|
+
}
|
|
234
|
+
else if (res.data) {
|
|
235
|
+
const data = res.data;
|
|
236
|
+
if (Array.isArray(data)) {
|
|
237
|
+
extracted = data;
|
|
238
|
+
}
|
|
239
|
+
else if (data.results && Array.isArray(data.results)) {
|
|
240
|
+
extracted = data.results;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
log.verbose('SearchResults: Extracted results', {
|
|
245
|
+
count: extracted.length,
|
|
246
|
+
viewMode: this.options.viewMode,
|
|
247
|
+
});
|
|
248
|
+
return extracted;
|
|
249
|
+
}
|
|
250
|
+
extractFields(item) {
|
|
251
|
+
try {
|
|
252
|
+
const mapping = this.options.fieldMapping;
|
|
253
|
+
return {
|
|
254
|
+
id: extractField(item, mapping.id) || String(item.id || ''),
|
|
255
|
+
title: extractField(item, mapping.title) || extractField(item, mapping.primaryText) || 'Untitled',
|
|
256
|
+
description: extractField(item, mapping.description) || extractField(item, mapping.secondaryText),
|
|
257
|
+
image: extractField(item, mapping.image) || extractField(item, mapping.imageUrl),
|
|
258
|
+
price: mapping.price ? formatPrice(extractField(item, mapping.price)) : undefined,
|
|
259
|
+
url: extractField(item, mapping.url),
|
|
260
|
+
metadata: item,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
265
|
+
log.warn('SearchResults: Error extracting fields from result', {
|
|
266
|
+
error: error.message,
|
|
267
|
+
itemId: item?.id || 'unknown',
|
|
268
|
+
});
|
|
269
|
+
// Return fallback result
|
|
270
|
+
return {
|
|
271
|
+
id: String(item?.id || ''),
|
|
272
|
+
title: 'Error loading result',
|
|
273
|
+
metadata: item,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
renderDefaultLoading() {
|
|
278
|
+
const div = document.createElement('div');
|
|
279
|
+
div.style.cssText = this.getLoadingStyle();
|
|
280
|
+
div.textContent = 'Loading results...';
|
|
281
|
+
return div;
|
|
282
|
+
}
|
|
283
|
+
renderDefaultError(error) {
|
|
284
|
+
const div = document.createElement('div');
|
|
285
|
+
div.style.cssText = this.getErrorStyle();
|
|
286
|
+
div.textContent = `Error: ${error.message}`;
|
|
287
|
+
return div;
|
|
288
|
+
}
|
|
289
|
+
renderDefaultEmpty() {
|
|
290
|
+
const div = document.createElement('div');
|
|
291
|
+
div.style.cssText = this.getEmptyStyle();
|
|
292
|
+
div.textContent = 'No results found';
|
|
293
|
+
return div;
|
|
294
|
+
}
|
|
295
|
+
renderDefaultResult(result, index) {
|
|
296
|
+
const div = document.createElement('div');
|
|
297
|
+
div.style.cssText = this.getResultStyle(index);
|
|
298
|
+
// Always set data attributes for event delegation (even if no onResultClick)
|
|
299
|
+
div.setAttribute('data-result-id', result.id);
|
|
300
|
+
div.setAttribute('data-result-index', String(index));
|
|
301
|
+
if (this.options.onResultClick) {
|
|
302
|
+
div.style.cursor = 'pointer';
|
|
303
|
+
div.setAttribute('role', 'button');
|
|
304
|
+
div.setAttribute('tabindex', '0');
|
|
305
|
+
console.log('SearchResults: Result element created', {
|
|
306
|
+
resultId: result.id,
|
|
307
|
+
index,
|
|
308
|
+
element: div,
|
|
309
|
+
hasOnResultClick: !!this.options.onResultClick
|
|
310
|
+
});
|
|
311
|
+
// No need to attach individual listeners - we use event delegation on the container
|
|
312
|
+
// This ensures clicks work even when results are recreated during re-renders
|
|
313
|
+
// Still handle keyboard accessibility
|
|
314
|
+
div.addEventListener('keydown', (e) => {
|
|
315
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
// Trigger the click event which will be handled by delegation
|
|
319
|
+
const clickEvent = new MouseEvent('click', {
|
|
320
|
+
bubbles: true,
|
|
321
|
+
cancelable: true,
|
|
322
|
+
view: window
|
|
323
|
+
});
|
|
324
|
+
div.dispatchEvent(clickEvent);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (this.options.viewMode === 'card' || this.options.viewMode === 'grid') {
|
|
329
|
+
this.renderCardResult(div, result);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
this.renderListResult(div, result);
|
|
333
|
+
}
|
|
334
|
+
return div;
|
|
335
|
+
}
|
|
336
|
+
renderCardResult(container, result) {
|
|
337
|
+
if (result.image) {
|
|
338
|
+
const imgContainer = document.createElement('div');
|
|
339
|
+
imgContainer.style.cssText = this.getImageContainerStyle();
|
|
340
|
+
const img = document.createElement('img');
|
|
341
|
+
img.src = result.image;
|
|
342
|
+
img.alt = result.title;
|
|
343
|
+
img.style.cssText = this.getImageStyle();
|
|
344
|
+
imgContainer.appendChild(img);
|
|
345
|
+
container.appendChild(imgContainer);
|
|
346
|
+
}
|
|
347
|
+
const content = document.createElement('div');
|
|
348
|
+
content.style.cssText = this.getCardContentStyle();
|
|
349
|
+
if (result.title) {
|
|
350
|
+
const h3 = document.createElement('h3');
|
|
351
|
+
h3.style.cssText = this.getTitleStyle();
|
|
352
|
+
h3.textContent = result.title;
|
|
353
|
+
content.appendChild(h3);
|
|
354
|
+
}
|
|
355
|
+
if (result.description) {
|
|
356
|
+
const p = document.createElement('p');
|
|
357
|
+
p.style.cssText = this.getDescriptionStyle();
|
|
358
|
+
p.textContent = result.description;
|
|
359
|
+
content.appendChild(p);
|
|
360
|
+
}
|
|
361
|
+
if (result.price) {
|
|
362
|
+
const priceDiv = document.createElement('div');
|
|
363
|
+
priceDiv.style.cssText = this.getPriceStyle();
|
|
364
|
+
priceDiv.textContent = result.price;
|
|
365
|
+
content.appendChild(priceDiv);
|
|
366
|
+
}
|
|
367
|
+
container.appendChild(content);
|
|
368
|
+
}
|
|
369
|
+
renderListResult(container, result) {
|
|
370
|
+
if (result.image) {
|
|
371
|
+
const img = document.createElement('img');
|
|
372
|
+
img.src = result.image;
|
|
373
|
+
img.alt = result.title;
|
|
374
|
+
img.style.cssText = this.getListImageStyle();
|
|
375
|
+
container.appendChild(img);
|
|
376
|
+
}
|
|
377
|
+
const content = document.createElement('div');
|
|
378
|
+
content.style.cssText = 'flex: 1; min-width: 0;';
|
|
379
|
+
if (result.title) {
|
|
380
|
+
const h3 = document.createElement('h3');
|
|
381
|
+
h3.style.cssText = this.getTitleStyle();
|
|
382
|
+
h3.textContent = result.title;
|
|
383
|
+
content.appendChild(h3);
|
|
384
|
+
}
|
|
385
|
+
if (result.description) {
|
|
386
|
+
const p = document.createElement('p');
|
|
387
|
+
p.style.cssText = this.getDescriptionStyle();
|
|
388
|
+
p.textContent = result.description;
|
|
389
|
+
content.appendChild(p);
|
|
390
|
+
}
|
|
391
|
+
if (result.price) {
|
|
392
|
+
const priceDiv = document.createElement('div');
|
|
393
|
+
priceDiv.style.cssText = this.getPriceStyle();
|
|
394
|
+
priceDiv.textContent = result.price;
|
|
395
|
+
content.appendChild(priceDiv);
|
|
396
|
+
}
|
|
397
|
+
container.appendChild(content);
|
|
398
|
+
}
|
|
399
|
+
get theme() {
|
|
400
|
+
return this.provider.theme;
|
|
401
|
+
}
|
|
402
|
+
getLoadingStyle() {
|
|
403
|
+
return `
|
|
404
|
+
padding: ${this.theme.spacing.large};
|
|
405
|
+
text-align: center;
|
|
406
|
+
color: ${this.theme.colors.text};
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
getErrorStyle() {
|
|
410
|
+
return `
|
|
411
|
+
padding: ${this.theme.spacing.large};
|
|
412
|
+
text-align: center;
|
|
413
|
+
color: ${this.theme.colors.error};
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
getEmptyStyle() {
|
|
417
|
+
return `
|
|
418
|
+
padding: ${this.theme.spacing.large};
|
|
419
|
+
text-align: center;
|
|
420
|
+
color: ${this.theme.colors.textSecondary || this.theme.colors.text};
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
getGridContainerStyle() {
|
|
424
|
+
return `
|
|
425
|
+
display: grid;
|
|
426
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
427
|
+
gap: ${this.theme.spacing.medium};
|
|
428
|
+
width: 100%;
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
getResultStyle(index) {
|
|
432
|
+
const borderRadius = typeof this.theme.borderRadius === 'string'
|
|
433
|
+
? this.theme.borderRadius
|
|
434
|
+
: this.theme.borderRadius.medium;
|
|
435
|
+
const transition = this.theme.transitions?.normal || '250ms ease-in-out';
|
|
436
|
+
if (this.options.viewMode === 'list') {
|
|
437
|
+
return `
|
|
438
|
+
padding: ${this.theme.spacing.medium};
|
|
439
|
+
border-bottom: 1px solid ${this.theme.colors.border};
|
|
440
|
+
border-radius: 0;
|
|
441
|
+
margin-bottom: 0;
|
|
442
|
+
transition: ${transition};
|
|
443
|
+
background-color: ${this.theme.colors.background};
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: flex-start;
|
|
446
|
+
gap: ${this.theme.spacing.medium};
|
|
447
|
+
`;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Card/Grid view - cards should be contained within grid
|
|
451
|
+
return `
|
|
452
|
+
padding: 0;
|
|
453
|
+
border: 1px solid ${this.theme.colors.border};
|
|
454
|
+
border-radius: ${borderRadius};
|
|
455
|
+
box-shadow: ${this.theme.shadows.small};
|
|
456
|
+
transition: ${transition};
|
|
457
|
+
background-color: ${this.theme.colors.background};
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-direction: column;
|
|
460
|
+
overflow: hidden;
|
|
461
|
+
height: 100%;
|
|
462
|
+
`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
getImageContainerStyle() {
|
|
466
|
+
return `
|
|
467
|
+
width: 100%;
|
|
468
|
+
aspect-ratio: ${this.options.viewMode === 'grid' ? '1/1' : '16/9'};
|
|
469
|
+
overflow: hidden;
|
|
470
|
+
background-color: ${this.theme.colors.hover};
|
|
471
|
+
flex-shrink: 0;
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
getImageStyle() {
|
|
475
|
+
return `
|
|
476
|
+
width: 100%;
|
|
477
|
+
height: 100%;
|
|
478
|
+
object-fit: cover;
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
getCardContentStyle() {
|
|
482
|
+
return `
|
|
483
|
+
padding: ${this.theme.spacing.medium};
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
flex: 1;
|
|
487
|
+
min-height: 0;
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
getListContentStyle() {
|
|
491
|
+
return `
|
|
492
|
+
display: flex;
|
|
493
|
+
align-items: flex-start;
|
|
494
|
+
gap: ${this.theme.spacing.medium};
|
|
495
|
+
flex: 1;
|
|
496
|
+
min-width: 0;
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
getListImageStyle() {
|
|
500
|
+
const borderRadius = typeof this.theme.borderRadius === 'string'
|
|
501
|
+
? this.theme.borderRadius
|
|
502
|
+
: this.theme.borderRadius.medium;
|
|
503
|
+
return `
|
|
504
|
+
width: 100px;
|
|
505
|
+
height: 100px;
|
|
506
|
+
object-fit: cover;
|
|
507
|
+
border-radius: ${borderRadius};
|
|
508
|
+
flex-shrink: 0;
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
getTitleStyle() {
|
|
512
|
+
return `
|
|
513
|
+
font-size: ${this.theme.typography.fontSize.large};
|
|
514
|
+
font-weight: bold;
|
|
515
|
+
margin: 0;
|
|
516
|
+
margin-bottom: ${this.theme.spacing.small};
|
|
517
|
+
color: ${this.theme.colors.text};
|
|
518
|
+
`;
|
|
519
|
+
}
|
|
520
|
+
getDescriptionStyle() {
|
|
521
|
+
return `
|
|
522
|
+
font-size: ${this.theme.typography.fontSize.medium};
|
|
523
|
+
color: ${this.theme.colors.text};
|
|
524
|
+
margin: 0;
|
|
525
|
+
margin-bottom: ${this.theme.spacing.small};
|
|
526
|
+
opacity: 0.8;
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
getPriceStyle() {
|
|
530
|
+
return `
|
|
531
|
+
font-size: ${this.theme.typography.fontSize.medium};
|
|
532
|
+
font-weight: bold;
|
|
533
|
+
color: ${this.theme.colors.primary};
|
|
534
|
+
margin-top: auto;
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortBy Component
|
|
3
|
+
*
|
|
4
|
+
* Displays sort options for search results
|
|
5
|
+
*/
|
|
6
|
+
import { SearchProvider } from './search-provider';
|
|
7
|
+
export interface SortOption {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SortByOptions {
|
|
12
|
+
container: HTMLElement | string;
|
|
13
|
+
options: SortOption[];
|
|
14
|
+
value?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
onSortChange?: (value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare class SortBy {
|
|
19
|
+
private container;
|
|
20
|
+
private provider;
|
|
21
|
+
private options;
|
|
22
|
+
private currentValue;
|
|
23
|
+
private unsubscribeStateManager;
|
|
24
|
+
constructor(provider: SearchProvider, options: SortByOptions);
|
|
25
|
+
destroy(): void;
|
|
26
|
+
update(options: Partial<Pick<SortByOptions, 'value'>>): void;
|
|
27
|
+
private render;
|
|
28
|
+
private get theme();
|
|
29
|
+
private getContainerStyle;
|
|
30
|
+
private getLabelStyle;
|
|
31
|
+
private getSelectStyle;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=sort-by.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sort-by.d.ts","sourceRoot":"","sources":["../../src/components/sort-by.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,OAAO,CAIb;IACF,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,uBAAuB,CAA6B;gBAEhD,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa;IAgC5D,OAAO,IAAI,IAAI;IAOf,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI;IAO5D,OAAO,CAAC,MAAM;IAkDd,OAAO,KAAK,KAAK,GAEhB;IAED,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,cAAc;CAevB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortBy Component
|
|
3
|
+
*
|
|
4
|
+
* Displays sort options for search results
|
|
5
|
+
*/
|
|
6
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
7
|
+
export class SortBy {
|
|
8
|
+
constructor(provider, options) {
|
|
9
|
+
this.unsubscribeStateManager = null;
|
|
10
|
+
this.provider = provider;
|
|
11
|
+
const container = typeof options.container === 'string'
|
|
12
|
+
? document.querySelector(options.container)
|
|
13
|
+
: options.container;
|
|
14
|
+
if (!container) {
|
|
15
|
+
const error = new Error('SortBy: container element not found');
|
|
16
|
+
log.error('SortBy: Initialization failed', { error: error.message });
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
this.container = container;
|
|
20
|
+
this.currentValue = options.value || options.options[0]?.value || '';
|
|
21
|
+
this.options = {
|
|
22
|
+
options: options.options,
|
|
23
|
+
label: options.label,
|
|
24
|
+
onSortChange: options.onSortChange,
|
|
25
|
+
};
|
|
26
|
+
// Subscribe to state manager to sync sort value
|
|
27
|
+
this.unsubscribeStateManager = this.provider.stateManager.subscribe((state) => {
|
|
28
|
+
if (state.sortBy && state.sortBy !== this.currentValue) {
|
|
29
|
+
this.currentValue = state.sortBy;
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.render();
|
|
34
|
+
}
|
|
35
|
+
destroy() {
|
|
36
|
+
if (this.unsubscribeStateManager) {
|
|
37
|
+
this.unsubscribeStateManager();
|
|
38
|
+
this.unsubscribeStateManager = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
update(options) {
|
|
42
|
+
if (options.value !== undefined) {
|
|
43
|
+
this.currentValue = options.value;
|
|
44
|
+
this.render();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
this.container.innerHTML = '';
|
|
49
|
+
const wrapper = document.createElement('div');
|
|
50
|
+
wrapper.style.cssText = this.getContainerStyle();
|
|
51
|
+
if (this.options.label) {
|
|
52
|
+
const label = document.createElement('label');
|
|
53
|
+
label.textContent = this.options.label;
|
|
54
|
+
label.style.cssText = this.getLabelStyle();
|
|
55
|
+
wrapper.appendChild(label);
|
|
56
|
+
}
|
|
57
|
+
const select = document.createElement('select');
|
|
58
|
+
select.value = this.currentValue;
|
|
59
|
+
select.style.cssText = this.getSelectStyle();
|
|
60
|
+
select.setAttribute('aria-label', 'Sort results');
|
|
61
|
+
select.addEventListener('change', (e) => {
|
|
62
|
+
const target = e.target;
|
|
63
|
+
this.currentValue = target.value;
|
|
64
|
+
log.verbose('SortBy: Sort option changed', { value: target.value });
|
|
65
|
+
// Update state manager (automatically triggers search)
|
|
66
|
+
this.provider.stateManager.setSortBy(target.value);
|
|
67
|
+
// Call callback for backwards compatibility
|
|
68
|
+
if (this.options.onSortChange) {
|
|
69
|
+
try {
|
|
70
|
+
this.options.onSortChange(target.value);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
log.error('SortBy: Error in onSortChange callback', {
|
|
75
|
+
value: target.value,
|
|
76
|
+
error: error.message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.options.options.forEach((option) => {
|
|
82
|
+
const optionEl = document.createElement('option');
|
|
83
|
+
optionEl.value = option.value;
|
|
84
|
+
optionEl.textContent = option.label;
|
|
85
|
+
select.appendChild(optionEl);
|
|
86
|
+
});
|
|
87
|
+
wrapper.appendChild(select);
|
|
88
|
+
this.container.appendChild(wrapper);
|
|
89
|
+
}
|
|
90
|
+
get theme() {
|
|
91
|
+
return this.provider.theme;
|
|
92
|
+
}
|
|
93
|
+
getContainerStyle() {
|
|
94
|
+
return `
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: ${this.theme.spacing.small};
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
getLabelStyle() {
|
|
101
|
+
return `
|
|
102
|
+
font-size: ${this.theme.typography.fontSize.medium};
|
|
103
|
+
color: ${this.theme.colors.text};
|
|
104
|
+
font-weight: ${this.theme.typography.fontWeight?.medium || 500};
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
getSelectStyle() {
|
|
108
|
+
const borderRadius = typeof this.theme.borderRadius === 'string'
|
|
109
|
+
? this.theme.borderRadius
|
|
110
|
+
: this.theme.borderRadius.medium;
|
|
111
|
+
return `
|
|
112
|
+
padding: ${this.theme.spacing.small} ${this.theme.spacing.medium};
|
|
113
|
+
font-size: ${this.theme.typography.fontSize.medium};
|
|
114
|
+
border: 1px solid ${this.theme.colors.border};
|
|
115
|
+
border-radius: ${borderRadius};
|
|
116
|
+
background-color: ${this.theme.colors.background};
|
|
117
|
+
color: ${this.theme.colors.text};
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
outline: none;
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
}
|