@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,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Component
|
|
3
|
+
*
|
|
4
|
+
* Displays pagination controls for search results
|
|
5
|
+
*/
|
|
6
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
7
|
+
export class Pagination {
|
|
8
|
+
constructor(provider, options) {
|
|
9
|
+
this.unsubscribeStateManager = null;
|
|
10
|
+
console.log('Pagination: Constructor called', { options });
|
|
11
|
+
this.provider = provider;
|
|
12
|
+
const container = typeof options.container === 'string'
|
|
13
|
+
? document.querySelector(options.container)
|
|
14
|
+
: options.container;
|
|
15
|
+
if (!container) {
|
|
16
|
+
const error = new Error('Pagination: container element not found');
|
|
17
|
+
log.error('Pagination: Initialization failed', { error: error.message });
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
console.log('Pagination: Container found', { container, containerId: container.id, containerTag: container.tagName });
|
|
21
|
+
this.container = container;
|
|
22
|
+
this.currentPage = options.currentPage || 1;
|
|
23
|
+
this.options = {
|
|
24
|
+
itemsPerPage: options.itemsPerPage || 10,
|
|
25
|
+
showFirstLast: options.showFirstLast !== false,
|
|
26
|
+
showPrevNext: options.showPrevNext !== false,
|
|
27
|
+
results: options.results,
|
|
28
|
+
currentPage: options.currentPage || 1,
|
|
29
|
+
onPageChange: options.onPageChange,
|
|
30
|
+
};
|
|
31
|
+
// Attach event delegation listener to the persistent container (not recreated on render)
|
|
32
|
+
// This ensures clicks work even when buttons are recreated
|
|
33
|
+
const handleContainerClick = (e) => {
|
|
34
|
+
const target = e.target;
|
|
35
|
+
// Debug: log all clicks on container
|
|
36
|
+
console.log('Pagination: Container clicked', {
|
|
37
|
+
target: target.tagName,
|
|
38
|
+
targetClass: target.className,
|
|
39
|
+
hasDataPage: target.hasAttribute('data-page'),
|
|
40
|
+
closestButton: target.closest('button[data-page]')
|
|
41
|
+
});
|
|
42
|
+
// Ignore clicks on non-interactive elements (like ellipsis spans)
|
|
43
|
+
if (target.tagName === 'SPAN' || target.tagName === 'DIV') {
|
|
44
|
+
// Check if it's inside a button
|
|
45
|
+
const button = target.closest('button[data-page]');
|
|
46
|
+
if (!button) {
|
|
47
|
+
// Click is on ellipsis or other non-button element, ignore it
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// If span is inside a button, use the button
|
|
51
|
+
}
|
|
52
|
+
// Find the button (could be the target itself or a parent)
|
|
53
|
+
const button = (target.tagName === 'BUTTON' && target.hasAttribute('data-page'))
|
|
54
|
+
? target
|
|
55
|
+
: target.closest('button[data-page]');
|
|
56
|
+
if (button) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
const pageStr = button.getAttribute('data-page');
|
|
60
|
+
const page = pageStr ? parseInt(pageStr, 10) : 0;
|
|
61
|
+
console.log('Pagination: Button found', {
|
|
62
|
+
page,
|
|
63
|
+
currentPage: this.currentPage,
|
|
64
|
+
pageStr,
|
|
65
|
+
button,
|
|
66
|
+
isValid: page > 0 && page !== this.currentPage
|
|
67
|
+
});
|
|
68
|
+
if (page > 0) {
|
|
69
|
+
// Always update, even if it's the same page (for debugging)
|
|
70
|
+
console.log('Pagination: Processing page change', {
|
|
71
|
+
page,
|
|
72
|
+
currentPage: this.currentPage,
|
|
73
|
+
willUpdate: page !== this.currentPage
|
|
74
|
+
});
|
|
75
|
+
if (page !== this.currentPage) {
|
|
76
|
+
// Update state manager (automatically triggers search)
|
|
77
|
+
this.provider.stateManager.setPage(page);
|
|
78
|
+
// Call callback for backwards compatibility
|
|
79
|
+
if (this.options.onPageChange) {
|
|
80
|
+
try {
|
|
81
|
+
this.options.onPageChange(page);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
85
|
+
log.error('Pagination: Error in onPageChange callback', {
|
|
86
|
+
page,
|
|
87
|
+
error: error.message,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log('Pagination: Page is already current, skipping update', { page });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.warn('Pagination: Invalid page number', { page, pageStr, button });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log('Pagination: Click not on a button', { target: target.tagName });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
this.container.addEventListener('click', handleContainerClick, { passive: false });
|
|
105
|
+
console.log('Pagination: Event delegation listener attached to container', { container: this.container });
|
|
106
|
+
// Subscribe to state manager to get results and current page
|
|
107
|
+
this.unsubscribeStateManager = this.provider.stateManager.subscribe((state) => {
|
|
108
|
+
// Update results from state manager
|
|
109
|
+
if (state.results) {
|
|
110
|
+
this.options.results = state.results;
|
|
111
|
+
}
|
|
112
|
+
// Update current page from state manager
|
|
113
|
+
if (state.currentPage !== this.currentPage) {
|
|
114
|
+
this.currentPage = state.currentPage;
|
|
115
|
+
}
|
|
116
|
+
// Re-render to sync with state
|
|
117
|
+
this.render();
|
|
118
|
+
});
|
|
119
|
+
this.render();
|
|
120
|
+
}
|
|
121
|
+
update(options) {
|
|
122
|
+
if (options.results !== undefined)
|
|
123
|
+
this.options.results = options.results;
|
|
124
|
+
if (options.currentPage !== undefined)
|
|
125
|
+
this.currentPage = options.currentPage;
|
|
126
|
+
this.render();
|
|
127
|
+
}
|
|
128
|
+
destroy() {
|
|
129
|
+
if (this.unsubscribeStateManager) {
|
|
130
|
+
this.unsubscribeStateManager();
|
|
131
|
+
this.unsubscribeStateManager = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
render() {
|
|
135
|
+
console.log('🔵 Pagination.render() START', {
|
|
136
|
+
container: this.container,
|
|
137
|
+
containerId: this.container.id,
|
|
138
|
+
currentPage: this.currentPage,
|
|
139
|
+
hasResults: !!this.options.results
|
|
140
|
+
});
|
|
141
|
+
this.container.innerHTML = '';
|
|
142
|
+
const totalPages = this.getTotalPages();
|
|
143
|
+
console.log('🔵 Pagination: Total pages calculated', { totalPages });
|
|
144
|
+
if (totalPages <= 1) {
|
|
145
|
+
console.log('🔵 Pagination: NOT RENDERING - totalPages <= 1', { totalPages });
|
|
146
|
+
log.verbose('Pagination: Not rendering, totalPages <= 1', { totalPages });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
console.log('🔵 Pagination: WILL RENDER', {
|
|
150
|
+
currentPage: this.currentPage,
|
|
151
|
+
totalPages,
|
|
152
|
+
hasResults: !!this.options.results
|
|
153
|
+
});
|
|
154
|
+
log.verbose('Pagination: Rendering', {
|
|
155
|
+
currentPage: this.currentPage,
|
|
156
|
+
totalPages,
|
|
157
|
+
hasResults: !!this.options.results
|
|
158
|
+
});
|
|
159
|
+
const paginationDiv = document.createElement('div');
|
|
160
|
+
paginationDiv.style.cssText = this.getContainerStyle();
|
|
161
|
+
paginationDiv.setAttribute('data-pagination-wrapper', 'true');
|
|
162
|
+
console.log('Pagination: Created paginationDiv', {
|
|
163
|
+
paginationDiv,
|
|
164
|
+
style: paginationDiv.style.cssText,
|
|
165
|
+
container: this.container
|
|
166
|
+
});
|
|
167
|
+
// Event delegation listener is attached to this.container in constructor
|
|
168
|
+
// No need to attach it here since paginationDiv is recreated on each render
|
|
169
|
+
// Always show First button if not on first page
|
|
170
|
+
if (this.options.showFirstLast && this.currentPage > 1) {
|
|
171
|
+
const firstBtn = this.createButton('First', 1);
|
|
172
|
+
paginationDiv.appendChild(firstBtn);
|
|
173
|
+
}
|
|
174
|
+
// Always show Previous button if not on first page
|
|
175
|
+
if (this.options.showPrevNext && this.currentPage > 1) {
|
|
176
|
+
const prevBtn = this.createButton('Previous', this.currentPage - 1);
|
|
177
|
+
paginationDiv.appendChild(prevBtn);
|
|
178
|
+
}
|
|
179
|
+
// Calculate page range to display (always show consistent number of pages)
|
|
180
|
+
const maxPagesToShow = 5; // Always show 5 page numbers
|
|
181
|
+
let startPage;
|
|
182
|
+
let endPage;
|
|
183
|
+
if (totalPages <= maxPagesToShow) {
|
|
184
|
+
// If total pages is less than max, show all pages
|
|
185
|
+
startPage = 1;
|
|
186
|
+
endPage = totalPages;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Calculate start and end to always show maxPagesToShow pages
|
|
190
|
+
// Try to center current page, but adjust if near boundaries
|
|
191
|
+
const halfRange = Math.floor(maxPagesToShow / 2);
|
|
192
|
+
if (this.currentPage <= halfRange + 1) {
|
|
193
|
+
// Near the beginning: show pages 1 to maxPagesToShow
|
|
194
|
+
startPage = 1;
|
|
195
|
+
endPage = maxPagesToShow;
|
|
196
|
+
}
|
|
197
|
+
else if (this.currentPage >= totalPages - halfRange) {
|
|
198
|
+
// Near the end: show last maxPagesToShow pages
|
|
199
|
+
startPage = totalPages - maxPagesToShow + 1;
|
|
200
|
+
endPage = totalPages;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// In the middle: center around current page
|
|
204
|
+
startPage = this.currentPage - halfRange;
|
|
205
|
+
endPage = this.currentPage + halfRange;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Show ellipsis if needed
|
|
209
|
+
if (startPage > 1) {
|
|
210
|
+
const ellipsis = document.createElement('span');
|
|
211
|
+
ellipsis.textContent = '...';
|
|
212
|
+
ellipsis.style.cssText = `
|
|
213
|
+
padding: ${this.theme.spacing.small} ${this.theme.spacing.medium};
|
|
214
|
+
color: ${this.theme.colors.textSecondary || this.theme.colors.text};
|
|
215
|
+
`;
|
|
216
|
+
paginationDiv.appendChild(ellipsis);
|
|
217
|
+
}
|
|
218
|
+
// Render page number buttons
|
|
219
|
+
console.log('🔵 Pagination: Rendering page buttons', { startPage, endPage, currentPage: this.currentPage });
|
|
220
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
221
|
+
console.log('🔵 Pagination: Creating page button', { i, isActive: i === this.currentPage });
|
|
222
|
+
const pageBtn = this.createButton(String(i), i, i === this.currentPage);
|
|
223
|
+
paginationDiv.appendChild(pageBtn);
|
|
224
|
+
console.log('🔵 Pagination: Page button appended', { page: i, button: pageBtn });
|
|
225
|
+
}
|
|
226
|
+
// Show ellipsis if needed
|
|
227
|
+
if (endPage < totalPages) {
|
|
228
|
+
const ellipsis = document.createElement('span');
|
|
229
|
+
ellipsis.textContent = '...';
|
|
230
|
+
ellipsis.style.cssText = `
|
|
231
|
+
padding: ${this.theme.spacing.small} ${this.theme.spacing.medium};
|
|
232
|
+
color: ${this.theme.colors.textSecondary || this.theme.colors.text};
|
|
233
|
+
`;
|
|
234
|
+
paginationDiv.appendChild(ellipsis);
|
|
235
|
+
}
|
|
236
|
+
// Always show Next button if not on last page
|
|
237
|
+
if (this.options.showPrevNext && this.currentPage < totalPages) {
|
|
238
|
+
const nextBtn = this.createButton('Next', this.currentPage + 1);
|
|
239
|
+
paginationDiv.appendChild(nextBtn);
|
|
240
|
+
}
|
|
241
|
+
// Always show Last button if not on last page
|
|
242
|
+
if (this.options.showFirstLast && this.currentPage < totalPages) {
|
|
243
|
+
const lastBtn = this.createButton('Last', totalPages);
|
|
244
|
+
paginationDiv.appendChild(lastBtn);
|
|
245
|
+
}
|
|
246
|
+
console.log('🔵 Pagination: About to append paginationDiv to container', {
|
|
247
|
+
paginationDiv,
|
|
248
|
+
container: this.container,
|
|
249
|
+
containerChildrenBefore: this.container.children.length,
|
|
250
|
+
paginationDivChildren: paginationDiv.children.length
|
|
251
|
+
});
|
|
252
|
+
this.container.appendChild(paginationDiv);
|
|
253
|
+
console.log('🔵 Pagination: paginationDiv appended to container', {
|
|
254
|
+
containerChildrenAfter: this.container.children.length,
|
|
255
|
+
containerHTML: this.container.innerHTML.substring(0, 300)
|
|
256
|
+
});
|
|
257
|
+
// Verify buttons are in DOM
|
|
258
|
+
const buttons = this.container.querySelectorAll('button[data-page]');
|
|
259
|
+
console.log('🔵 Pagination: Render complete - buttons in DOM', {
|
|
260
|
+
buttonCount: buttons.length,
|
|
261
|
+
buttons: Array.from(buttons).map(b => {
|
|
262
|
+
const btn = b;
|
|
263
|
+
return {
|
|
264
|
+
text: btn.textContent,
|
|
265
|
+
page: btn.getAttribute('data-page'),
|
|
266
|
+
element: btn,
|
|
267
|
+
parent: btn.parentElement?.tagName
|
|
268
|
+
};
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
// Test click handler directly on first button
|
|
272
|
+
console.log('🔵 Pagination: Testing direct click handler');
|
|
273
|
+
const testButton = this.container.querySelector('button[data-page]');
|
|
274
|
+
if (testButton) {
|
|
275
|
+
console.log('🔵 Pagination: Found test button, adding DIRECT listener', {
|
|
276
|
+
testButton,
|
|
277
|
+
page: testButton.getAttribute('data-page'),
|
|
278
|
+
text: testButton.textContent
|
|
279
|
+
});
|
|
280
|
+
testButton.addEventListener('click', (e) => {
|
|
281
|
+
console.log('🟢🟢🟢 DIRECT BUTTON CLICK HANDLER FIRED! 🟢🟢🟢', {
|
|
282
|
+
e,
|
|
283
|
+
button: testButton,
|
|
284
|
+
page: testButton.getAttribute('data-page'),
|
|
285
|
+
target: e.target
|
|
286
|
+
});
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
e.stopPropagation();
|
|
289
|
+
}, { passive: false, capture: true });
|
|
290
|
+
console.log('🔵 Pagination: Direct listener attached to test button');
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.warn('🔴 Pagination: NO TEST BUTTON FOUND for direct listener!');
|
|
294
|
+
}
|
|
295
|
+
console.log('🔵 Pagination.render() END');
|
|
296
|
+
}
|
|
297
|
+
getTotalPages() {
|
|
298
|
+
if (!this.options.results)
|
|
299
|
+
return 0;
|
|
300
|
+
// Check for totalResults (SDK's SearchResponse format)
|
|
301
|
+
let total;
|
|
302
|
+
if (typeof this.options.results.totalResults === 'number') {
|
|
303
|
+
total = this.options.results.totalResults;
|
|
304
|
+
}
|
|
305
|
+
else if (typeof this.options.results.total === 'number') {
|
|
306
|
+
// Fallback to total (alternative format)
|
|
307
|
+
total = this.options.results.total;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// Last resort: use results array length (but log warning)
|
|
311
|
+
const resultsLength = this.options.results.results?.length || 0;
|
|
312
|
+
log.warn('Pagination: Using results array length as total (totalResults not found)', {
|
|
313
|
+
resultsLength,
|
|
314
|
+
responseKeys: Object.keys(this.options.results),
|
|
315
|
+
});
|
|
316
|
+
total = resultsLength;
|
|
317
|
+
}
|
|
318
|
+
const totalPages = Math.ceil(total / this.options.itemsPerPage);
|
|
319
|
+
log.verbose('Pagination: Calculating total pages', {
|
|
320
|
+
total,
|
|
321
|
+
itemsPerPage: this.options.itemsPerPage,
|
|
322
|
+
totalPages,
|
|
323
|
+
currentPage: this.currentPage,
|
|
324
|
+
});
|
|
325
|
+
return totalPages;
|
|
326
|
+
}
|
|
327
|
+
createButton(text, page, isActive = false) {
|
|
328
|
+
const button = document.createElement('button');
|
|
329
|
+
button.textContent = text;
|
|
330
|
+
button.style.cssText = this.getButtonStyle(isActive);
|
|
331
|
+
button.type = 'button'; // Prevent form submission
|
|
332
|
+
button.setAttribute('data-page', String(page)); // For event delegation
|
|
333
|
+
// No need to attach individual listeners - we use event delegation on the container
|
|
334
|
+
// This ensures clicks always work even when buttons are recreated
|
|
335
|
+
return button;
|
|
336
|
+
}
|
|
337
|
+
get theme() {
|
|
338
|
+
return this.provider.theme;
|
|
339
|
+
}
|
|
340
|
+
getContainerStyle() {
|
|
341
|
+
return `
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: ${this.theme.spacing.small};
|
|
345
|
+
flex-wrap: wrap;
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
getButtonStyle(isActive) {
|
|
349
|
+
const borderRadius = typeof this.theme.borderRadius === 'string'
|
|
350
|
+
? this.theme.borderRadius
|
|
351
|
+
: this.theme.borderRadius.medium;
|
|
352
|
+
const transition = this.theme.transitions?.fast || '150ms ease-in-out';
|
|
353
|
+
return `
|
|
354
|
+
padding: ${this.theme.spacing.small} ${this.theme.spacing.medium};
|
|
355
|
+
font-size: ${this.theme.typography.fontSize.medium};
|
|
356
|
+
border: 1px solid ${this.theme.colors.border};
|
|
357
|
+
border-radius: ${borderRadius};
|
|
358
|
+
background-color: ${isActive ? this.theme.colors.primary : this.theme.colors.background};
|
|
359
|
+
color: ${isActive ? '#ffffff' : this.theme.colors.text};
|
|
360
|
+
cursor: pointer;
|
|
361
|
+
transition: ${transition};
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuerySuggestions Component
|
|
3
|
+
*
|
|
4
|
+
* Standalone component for displaying query suggestions
|
|
5
|
+
*/
|
|
6
|
+
import { SearchProvider } from './search-provider';
|
|
7
|
+
export interface QuerySuggestionsOptions {
|
|
8
|
+
container: HTMLElement | string;
|
|
9
|
+
query?: string;
|
|
10
|
+
maxSuggestions?: number;
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
minQueryLength?: number;
|
|
13
|
+
showTitle?: boolean;
|
|
14
|
+
title?: string;
|
|
15
|
+
onSuggestionClick?: (suggestion: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class QuerySuggestions {
|
|
18
|
+
private container;
|
|
19
|
+
private provider;
|
|
20
|
+
private options;
|
|
21
|
+
private suggestions;
|
|
22
|
+
private loading;
|
|
23
|
+
private error;
|
|
24
|
+
private selectedIndex;
|
|
25
|
+
private debounceTimer;
|
|
26
|
+
constructor(provider: SearchProvider, options: QuerySuggestionsOptions);
|
|
27
|
+
update(options: Partial<Pick<QuerySuggestionsOptions, 'query'>>): void;
|
|
28
|
+
private loadSuggestions;
|
|
29
|
+
private render;
|
|
30
|
+
private get theme();
|
|
31
|
+
private getTitleStyle;
|
|
32
|
+
private getLoadingStyle;
|
|
33
|
+
private getEmptyStyle;
|
|
34
|
+
private getListStyle;
|
|
35
|
+
private getSuggestionStyle;
|
|
36
|
+
private getCountStyle;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=query-suggestions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-suggestions.d.ts","sourceRoot":"","sources":["../../src/components/query-suggestions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,OAAO,CAAsJ;IACrK,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,aAAa,CAA8C;gBAEvD,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,uBAAuB;IA4BtE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI;YAiBxD,eAAe;IAyC7B,OAAO,CAAC,MAAM;IAkFd,OAAO,KAAK,KAAK,GAEhB;IAED,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,aAAa;IAOrB,OAAO,IAAI,IAAI;CAMhB"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuerySuggestions Component
|
|
3
|
+
*
|
|
4
|
+
* Standalone component for displaying query suggestions
|
|
5
|
+
*/
|
|
6
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
7
|
+
export class QuerySuggestions {
|
|
8
|
+
constructor(provider, options) {
|
|
9
|
+
this.suggestions = [];
|
|
10
|
+
this.loading = false;
|
|
11
|
+
this.error = null;
|
|
12
|
+
this.selectedIndex = -1;
|
|
13
|
+
this.debounceTimer = null;
|
|
14
|
+
this.provider = provider;
|
|
15
|
+
const container = typeof options.container === 'string'
|
|
16
|
+
? document.querySelector(options.container)
|
|
17
|
+
: options.container;
|
|
18
|
+
if (!container) {
|
|
19
|
+
throw new Error('QuerySuggestions: container element not found');
|
|
20
|
+
}
|
|
21
|
+
this.container = container;
|
|
22
|
+
this.options = {
|
|
23
|
+
query: options.query || '',
|
|
24
|
+
maxSuggestions: options.maxSuggestions || 10,
|
|
25
|
+
debounceMs: options.debounceMs || 300,
|
|
26
|
+
minQueryLength: options.minQueryLength || 2,
|
|
27
|
+
showTitle: options.showTitle || false,
|
|
28
|
+
title: options.title || 'Suggestions',
|
|
29
|
+
onSuggestionClick: options.onSuggestionClick,
|
|
30
|
+
};
|
|
31
|
+
const q = this.options.query ?? '';
|
|
32
|
+
if (q.length >= this.options.minQueryLength) {
|
|
33
|
+
this.loadSuggestions(q);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
update(options) {
|
|
37
|
+
if (options.query !== undefined) {
|
|
38
|
+
this.options.query = options.query;
|
|
39
|
+
if (this.debounceTimer) {
|
|
40
|
+
clearTimeout(this.debounceTimer);
|
|
41
|
+
}
|
|
42
|
+
this.debounceTimer = setTimeout(() => {
|
|
43
|
+
if (this.options.query && this.options.query.length >= this.options.minQueryLength) {
|
|
44
|
+
this.loadSuggestions(this.options.query);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.suggestions = [];
|
|
48
|
+
this.render();
|
|
49
|
+
}
|
|
50
|
+
}, this.options.debounceMs);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async loadSuggestions(query) {
|
|
54
|
+
this.loading = true;
|
|
55
|
+
this.error = null;
|
|
56
|
+
this.render();
|
|
57
|
+
try {
|
|
58
|
+
const response = await this.provider.client.getSuggestions(query, this.options.maxSuggestions);
|
|
59
|
+
const rawSuggestions = Array.isArray(response) ? response : [];
|
|
60
|
+
this.suggestions = rawSuggestions.map((suggestion) => {
|
|
61
|
+
const suggestionQuery = suggestion.query ?? suggestion.text ?? suggestion;
|
|
62
|
+
const count = suggestion.popularity !== undefined
|
|
63
|
+
? suggestion.popularity
|
|
64
|
+
: (suggestion.count !== undefined ? suggestion.count : undefined);
|
|
65
|
+
return {
|
|
66
|
+
query: typeof suggestionQuery === 'string' ? suggestionQuery : String(suggestionQuery),
|
|
67
|
+
count: typeof count === 'number' ? count : undefined,
|
|
68
|
+
metadata: suggestion,
|
|
69
|
+
};
|
|
70
|
+
}).slice(0, this.options.maxSuggestions);
|
|
71
|
+
this.loading = false;
|
|
72
|
+
this.render();
|
|
73
|
+
log.verbose('QuerySuggestions: Suggestions loaded', {
|
|
74
|
+
query: this.options.query,
|
|
75
|
+
count: this.suggestions.length,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
80
|
+
log.error('QuerySuggestions: Failed to load suggestions', {
|
|
81
|
+
query: this.options.query,
|
|
82
|
+
error: error.message,
|
|
83
|
+
stack: error.stack,
|
|
84
|
+
});
|
|
85
|
+
this.error = error;
|
|
86
|
+
this.loading = false;
|
|
87
|
+
this.render();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
render() {
|
|
91
|
+
this.container.innerHTML = '';
|
|
92
|
+
if (!this.options.query || this.options.query.length < this.options.minQueryLength) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (this.loading) {
|
|
96
|
+
const loadingDiv = document.createElement('div');
|
|
97
|
+
loadingDiv.style.cssText = this.getLoadingStyle();
|
|
98
|
+
loadingDiv.textContent = 'Loading suggestions...';
|
|
99
|
+
this.container.appendChild(loadingDiv);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.error || this.suggestions.length === 0) {
|
|
103
|
+
const emptyDiv = document.createElement('div');
|
|
104
|
+
emptyDiv.style.cssText = this.getEmptyStyle();
|
|
105
|
+
emptyDiv.textContent = 'No suggestions available';
|
|
106
|
+
this.container.appendChild(emptyDiv);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (this.options.showTitle) {
|
|
110
|
+
const titleDiv = document.createElement('div');
|
|
111
|
+
titleDiv.style.cssText = this.getTitleStyle();
|
|
112
|
+
titleDiv.textContent = this.options.title;
|
|
113
|
+
this.container.appendChild(titleDiv);
|
|
114
|
+
}
|
|
115
|
+
const listDiv = document.createElement('div');
|
|
116
|
+
listDiv.style.cssText = this.getListStyle();
|
|
117
|
+
this.suggestions.forEach((suggestion, index) => {
|
|
118
|
+
const item = document.createElement('div');
|
|
119
|
+
item.style.cssText = this.getSuggestionStyle(index === this.selectedIndex);
|
|
120
|
+
// Use a named function to ensure the listener is properly attached
|
|
121
|
+
const handleClick = (e) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
if (this.options.onSuggestionClick) {
|
|
125
|
+
log.verbose('QuerySuggestions: Suggestion clicked', { suggestion: suggestion.query });
|
|
126
|
+
try {
|
|
127
|
+
this.options.onSuggestionClick(suggestion.query);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
131
|
+
log.error('QuerySuggestions: Error in onSuggestionClick callback', {
|
|
132
|
+
suggestion: suggestion.query,
|
|
133
|
+
error: error.message,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
item.addEventListener('click', handleClick, { passive: false });
|
|
139
|
+
item.addEventListener('mouseenter', () => {
|
|
140
|
+
this.selectedIndex = index;
|
|
141
|
+
this.render();
|
|
142
|
+
});
|
|
143
|
+
item.addEventListener('mouseleave', () => {
|
|
144
|
+
this.selectedIndex = -1;
|
|
145
|
+
this.render();
|
|
146
|
+
});
|
|
147
|
+
const querySpan = document.createElement('span');
|
|
148
|
+
querySpan.textContent = suggestion.query;
|
|
149
|
+
item.appendChild(querySpan);
|
|
150
|
+
if (suggestion.count !== undefined) {
|
|
151
|
+
const countSpan = document.createElement('span');
|
|
152
|
+
countSpan.textContent = ` (${suggestion.count})`;
|
|
153
|
+
countSpan.style.cssText = this.getCountStyle();
|
|
154
|
+
item.appendChild(countSpan);
|
|
155
|
+
}
|
|
156
|
+
listDiv.appendChild(item);
|
|
157
|
+
});
|
|
158
|
+
this.container.appendChild(listDiv);
|
|
159
|
+
}
|
|
160
|
+
get theme() {
|
|
161
|
+
return this.provider.theme;
|
|
162
|
+
}
|
|
163
|
+
getTitleStyle() {
|
|
164
|
+
return `
|
|
165
|
+
font-size: ${this.theme.typography.fontSize.large};
|
|
166
|
+
font-weight: bold;
|
|
167
|
+
margin-bottom: ${this.theme.spacing.medium};
|
|
168
|
+
color: ${this.theme.colors.text};
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
getLoadingStyle() {
|
|
172
|
+
return `
|
|
173
|
+
padding: ${this.theme.spacing.medium};
|
|
174
|
+
text-align: center;
|
|
175
|
+
color: ${this.theme.colors.text};
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
getEmptyStyle() {
|
|
179
|
+
return `
|
|
180
|
+
padding: ${this.theme.spacing.medium};
|
|
181
|
+
text-align: center;
|
|
182
|
+
color: ${this.theme.colors.text};
|
|
183
|
+
opacity: 0.6;
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
getListStyle() {
|
|
187
|
+
return `
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
getSuggestionStyle(isSelected) {
|
|
193
|
+
const borderRadius = typeof this.theme.borderRadius === 'string'
|
|
194
|
+
? this.theme.borderRadius
|
|
195
|
+
: this.theme.borderRadius.medium;
|
|
196
|
+
return `
|
|
197
|
+
padding: ${this.theme.spacing.medium};
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
border-radius: ${borderRadius};
|
|
200
|
+
transition: ${this.theme.transitions?.fast || '150ms ease-in-out'};
|
|
201
|
+
background-color: ${isSelected ? this.theme.colors.hover : 'transparent'};
|
|
202
|
+
margin-bottom: ${this.theme.spacing.small};
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
getCountStyle() {
|
|
206
|
+
return `
|
|
207
|
+
opacity: 0.6;
|
|
208
|
+
margin-left: ${this.theme.spacing.small};
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
destroy() {
|
|
212
|
+
if (this.debounceTimer) {
|
|
213
|
+
clearTimeout(this.debounceTimer);
|
|
214
|
+
}
|
|
215
|
+
this.container.innerHTML = '';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RangeInput Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a range input for filtering numeric values (e.g., price range)
|
|
5
|
+
*/
|
|
6
|
+
import { SearchProvider } from './search-provider';
|
|
7
|
+
export interface RangeInputOptions {
|
|
8
|
+
container: HTMLElement | string;
|
|
9
|
+
field: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
step?: number;
|
|
14
|
+
currentMin?: number;
|
|
15
|
+
currentMax?: number;
|
|
16
|
+
showApplyButton?: boolean;
|
|
17
|
+
placeholder?: {
|
|
18
|
+
min?: string;
|
|
19
|
+
max?: string;
|
|
20
|
+
};
|
|
21
|
+
onRangeChange?: (min: number | undefined, max: number | undefined) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class RangeInput {
|
|
24
|
+
private container;
|
|
25
|
+
private provider;
|
|
26
|
+
private options;
|
|
27
|
+
private internalMin?;
|
|
28
|
+
private internalMax?;
|
|
29
|
+
constructor(provider: SearchProvider, options: RangeInputOptions);
|
|
30
|
+
update(options: Partial<Pick<RangeInputOptions, 'currentMin' | 'currentMax'>>): void;
|
|
31
|
+
private render;
|
|
32
|
+
private hasValue;
|
|
33
|
+
private updateStateManager;
|
|
34
|
+
private get theme();
|
|
35
|
+
private getLabelStyle;
|
|
36
|
+
private getInputGroupStyle;
|
|
37
|
+
private getInputStyle;
|
|
38
|
+
private getSeparatorStyle;
|
|
39
|
+
private getApplyButtonStyle;
|
|
40
|
+
private getResetButtonStyle;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=range-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"range-input.d.ts","sourceRoot":"","sources":["../../src/components/range-input.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAC5E;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,OAAO,CAA0K;IACzL,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAS;gBAEjB,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB;IAgChE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,YAAY,CAAC,CAAC,GAAG,IAAI;IAYpF,OAAO,CAAC,MAAM;IAoJd,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,KAAK,KAAK,GAEhB;IAED,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,mBAAmB;CAe5B"}
|