@seekora-ai/ui-sdk-core 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/index.d.ts +449 -0
- package/dist/index.esm.js +1949 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1994 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Field Mapping Utilities
|
|
5
|
+
*
|
|
6
|
+
* Shared utilities for extracting and mapping fields from API responses
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Get nested value from object using dot notation path
|
|
10
|
+
*/
|
|
11
|
+
const getNestedValue$1 = (obj, path) => {
|
|
12
|
+
if (!path)
|
|
13
|
+
return undefined;
|
|
14
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Format price value with currency
|
|
18
|
+
*/
|
|
19
|
+
const formatPrice = (value, currency = '₹') => {
|
|
20
|
+
if (value === undefined || value === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (typeof value === 'number')
|
|
23
|
+
return `${currency}${value}`;
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const num = parseFloat(value);
|
|
26
|
+
if (!isNaN(num))
|
|
27
|
+
return `${currency}${num}`;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return String(value);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Extract field value using dot notation path
|
|
34
|
+
*/
|
|
35
|
+
const extractField = (item, path) => {
|
|
36
|
+
if (!path)
|
|
37
|
+
return undefined;
|
|
38
|
+
return getNestedValue$1(item, path);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Theme Management Utilities
|
|
43
|
+
*
|
|
44
|
+
* Shared theme utilities for creating and merging themes
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Create a complete theme from a partial theme configuration
|
|
48
|
+
*/
|
|
49
|
+
const createTheme = (config, baseTheme) => {
|
|
50
|
+
return {
|
|
51
|
+
colors: {
|
|
52
|
+
...baseTheme.colors,
|
|
53
|
+
...config.colors,
|
|
54
|
+
},
|
|
55
|
+
typography: {
|
|
56
|
+
...baseTheme.typography,
|
|
57
|
+
...config.typography,
|
|
58
|
+
fontSize: {
|
|
59
|
+
...baseTheme.typography.fontSize,
|
|
60
|
+
...config.typography?.fontSize,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
spacing: {
|
|
64
|
+
...baseTheme.spacing,
|
|
65
|
+
...config.spacing,
|
|
66
|
+
},
|
|
67
|
+
borderRadius: (config.borderRadius ?? baseTheme.borderRadius),
|
|
68
|
+
shadows: {
|
|
69
|
+
...baseTheme.shadows,
|
|
70
|
+
...config.shadows,
|
|
71
|
+
},
|
|
72
|
+
transitions: {
|
|
73
|
+
...baseTheme.transitions,
|
|
74
|
+
...config.transitions,
|
|
75
|
+
},
|
|
76
|
+
breakpoints: {
|
|
77
|
+
...baseTheme.breakpoints,
|
|
78
|
+
...config.breakpoints,
|
|
79
|
+
},
|
|
80
|
+
zIndex: {
|
|
81
|
+
...baseTheme.zIndex,
|
|
82
|
+
...config.zIndex,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Merge multiple themes together
|
|
88
|
+
*/
|
|
89
|
+
const mergeThemes = (baseTheme, ...themes) => {
|
|
90
|
+
return themes.reduce((acc, theme) => {
|
|
91
|
+
return {
|
|
92
|
+
colors: { ...acc.colors, ...theme.colors },
|
|
93
|
+
typography: {
|
|
94
|
+
...acc.typography,
|
|
95
|
+
...theme.typography,
|
|
96
|
+
fontSize: {
|
|
97
|
+
...acc.typography?.fontSize,
|
|
98
|
+
...theme.typography?.fontSize,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
spacing: { ...acc.spacing, ...theme.spacing },
|
|
102
|
+
borderRadius: (theme.borderRadius ?? acc.borderRadius),
|
|
103
|
+
shadows: { ...acc.shadows, ...theme.shadows },
|
|
104
|
+
transitions: { ...acc.transitions, ...theme.transitions },
|
|
105
|
+
breakpoints: { ...acc.breakpoints, ...theme.breakpoints },
|
|
106
|
+
zIndex: { ...acc.zIndex, ...theme.zIndex },
|
|
107
|
+
};
|
|
108
|
+
}, baseTheme);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* CSS Variables Theme System
|
|
113
|
+
*
|
|
114
|
+
* Generates CSS variables from theme configuration
|
|
115
|
+
* Provides utilities for injecting and managing CSS themes
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* Convert a theme object to CSS variables
|
|
119
|
+
*/
|
|
120
|
+
function themeToCSSVariables(theme, config = {}) {
|
|
121
|
+
const { scope = ':root', prefix = '--seekora' } = config;
|
|
122
|
+
const variables = [];
|
|
123
|
+
// Colors
|
|
124
|
+
Object.entries(theme.colors).forEach(([key, value]) => {
|
|
125
|
+
variables.push(`${prefix}-color-${key}: ${value};`);
|
|
126
|
+
});
|
|
127
|
+
// Typography
|
|
128
|
+
variables.push(`${prefix}-font-family: ${theme.typography.fontFamily};`);
|
|
129
|
+
Object.entries(theme.typography.fontSize).forEach(([key, value]) => {
|
|
130
|
+
variables.push(`${prefix}-font-size-${key}: ${value};`);
|
|
131
|
+
});
|
|
132
|
+
if (theme.typography.fontWeight) {
|
|
133
|
+
Object.entries(theme.typography.fontWeight).forEach(([key, value]) => {
|
|
134
|
+
variables.push(`${prefix}-font-weight-${key}: ${value};`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (theme.typography.lineHeight) {
|
|
138
|
+
Object.entries(theme.typography.lineHeight).forEach(([key, value]) => {
|
|
139
|
+
variables.push(`${prefix}-line-height-${key}: ${value};`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// Spacing
|
|
143
|
+
Object.entries(theme.spacing).forEach(([key, value]) => {
|
|
144
|
+
variables.push(`${prefix}-spacing-${key}: ${value};`);
|
|
145
|
+
});
|
|
146
|
+
// Border Radius
|
|
147
|
+
if (typeof theme.borderRadius === 'string') {
|
|
148
|
+
variables.push(`${prefix}-border-radius: ${theme.borderRadius};`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
Object.entries(theme.borderRadius).forEach(([key, value]) => {
|
|
152
|
+
variables.push(`${prefix}-border-radius-${key}: ${value};`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Shadows
|
|
156
|
+
if (theme.shadows) {
|
|
157
|
+
Object.entries(theme.shadows).forEach(([key, value]) => {
|
|
158
|
+
variables.push(`${prefix}-shadow-${key}: ${value};`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Transitions
|
|
162
|
+
if (theme.transitions) {
|
|
163
|
+
Object.entries(theme.transitions).forEach(([key, value]) => {
|
|
164
|
+
variables.push(`${prefix}-transition-${key}: ${value};`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Z-Index
|
|
168
|
+
if (theme.zIndex) {
|
|
169
|
+
Object.entries(theme.zIndex).forEach(([key, value]) => {
|
|
170
|
+
variables.push(`${prefix}-z-index-${key}: ${value};`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return `${scope} {\n ${variables.join('\n ')}\n}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Generate complete CSS stylesheet with variables and optional component classes
|
|
177
|
+
*/
|
|
178
|
+
function generateThemeStylesheet(theme, config = {}) {
|
|
179
|
+
const { prefix = '--seekora', includeClasses = true } = config;
|
|
180
|
+
const sections = [];
|
|
181
|
+
// CSS Variables
|
|
182
|
+
sections.push(themeToCSSVariables(theme, config));
|
|
183
|
+
// Component classes (BEM naming convention)
|
|
184
|
+
if (includeClasses) {
|
|
185
|
+
sections.push(generateComponentClasses(prefix));
|
|
186
|
+
}
|
|
187
|
+
return sections.join('\n\n');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Generate BEM-style component classes
|
|
191
|
+
*/
|
|
192
|
+
function generateComponentClasses(prefix) {
|
|
193
|
+
return `
|
|
194
|
+
/* Seekora UI SDK Component Classes */
|
|
195
|
+
|
|
196
|
+
/* Search Bar */
|
|
197
|
+
.seekora-search-bar {
|
|
198
|
+
position: relative;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
background-color: var(${prefix}-color-background);
|
|
202
|
+
border: 1px solid var(${prefix}-color-border);
|
|
203
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
204
|
+
transition: var(${prefix}-transition-fast);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.seekora-search-bar--focused {
|
|
208
|
+
border-color: var(${prefix}-color-primary);
|
|
209
|
+
box-shadow: 0 0 0 2px var(${prefix}-color-focus);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.seekora-search-bar__input {
|
|
213
|
+
flex: 1;
|
|
214
|
+
padding: var(${prefix}-spacing-medium);
|
|
215
|
+
font-size: var(${prefix}-font-size-medium);
|
|
216
|
+
font-family: var(${prefix}-font-family);
|
|
217
|
+
color: var(${prefix}-color-text);
|
|
218
|
+
background: transparent;
|
|
219
|
+
border: none;
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.seekora-search-bar__input::placeholder {
|
|
224
|
+
color: var(${prefix}-color-textSecondary);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.seekora-search-bar__button {
|
|
228
|
+
padding: var(${prefix}-spacing-small) var(${prefix}-spacing-medium);
|
|
229
|
+
font-size: var(${prefix}-font-size-medium);
|
|
230
|
+
color: #ffffff;
|
|
231
|
+
background-color: var(${prefix}-color-primary);
|
|
232
|
+
border: none;
|
|
233
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
transition: var(${prefix}-transition-fast);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.seekora-search-bar__button:hover {
|
|
239
|
+
background-color: var(${prefix}-color-hover);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* Search Results */
|
|
243
|
+
.seekora-results {
|
|
244
|
+
background-color: var(${prefix}-color-background);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.seekora-results__list {
|
|
248
|
+
list-style: none;
|
|
249
|
+
margin: 0;
|
|
250
|
+
padding: 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.seekora-results__item {
|
|
254
|
+
display: flex;
|
|
255
|
+
gap: var(${prefix}-spacing-medium);
|
|
256
|
+
padding: var(${prefix}-spacing-medium);
|
|
257
|
+
border-bottom: 1px solid var(${prefix}-color-border);
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
transition: var(${prefix}-transition-fast);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.seekora-results__item:hover {
|
|
263
|
+
background-color: var(${prefix}-color-hover);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.seekora-results__item--active {
|
|
267
|
+
background-color: var(${prefix}-color-hover);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.seekora-results__image {
|
|
271
|
+
width: 80px;
|
|
272
|
+
height: 80px;
|
|
273
|
+
object-fit: cover;
|
|
274
|
+
border-radius: var(${prefix}-border-radius-small, var(${prefix}-border-radius));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.seekora-results__content {
|
|
278
|
+
flex: 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.seekora-results__title {
|
|
282
|
+
margin: 0 0 var(${prefix}-spacing-small) 0;
|
|
283
|
+
font-size: var(${prefix}-font-size-medium);
|
|
284
|
+
font-weight: var(${prefix}-font-weight-semibold, 600);
|
|
285
|
+
color: var(${prefix}-color-text);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.seekora-results__description {
|
|
289
|
+
margin: 0;
|
|
290
|
+
font-size: var(${prefix}-font-size-small);
|
|
291
|
+
color: var(${prefix}-color-textSecondary);
|
|
292
|
+
line-height: var(${prefix}-line-height-normal, 1.5);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.seekora-results__price {
|
|
296
|
+
margin-top: var(${prefix}-spacing-small);
|
|
297
|
+
font-size: var(${prefix}-font-size-medium);
|
|
298
|
+
font-weight: var(${prefix}-font-weight-bold, 700);
|
|
299
|
+
color: var(${prefix}-color-primary);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.seekora-results__empty {
|
|
303
|
+
padding: var(${prefix}-spacing-large);
|
|
304
|
+
text-align: center;
|
|
305
|
+
color: var(${prefix}-color-textSecondary);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.seekora-results__loading {
|
|
309
|
+
padding: var(${prefix}-spacing-large);
|
|
310
|
+
text-align: center;
|
|
311
|
+
color: var(${prefix}-color-textSecondary);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Pagination */
|
|
315
|
+
.seekora-pagination {
|
|
316
|
+
display: flex;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
gap: var(${prefix}-spacing-small);
|
|
319
|
+
padding: var(${prefix}-spacing-medium) 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.seekora-pagination__item {
|
|
323
|
+
min-width: 36px;
|
|
324
|
+
height: 36px;
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
padding: 0 var(${prefix}-spacing-small);
|
|
329
|
+
font-size: var(${prefix}-font-size-medium);
|
|
330
|
+
color: var(${prefix}-color-text);
|
|
331
|
+
background-color: var(${prefix}-color-background);
|
|
332
|
+
border: 1px solid var(${prefix}-color-border);
|
|
333
|
+
border-radius: var(${prefix}-border-radius-small, var(${prefix}-border-radius));
|
|
334
|
+
cursor: pointer;
|
|
335
|
+
transition: var(${prefix}-transition-fast);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.seekora-pagination__item:hover:not(.seekora-pagination__item--disabled) {
|
|
339
|
+
background-color: var(${prefix}-color-hover);
|
|
340
|
+
border-color: var(${prefix}-color-primary);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.seekora-pagination__item--active {
|
|
344
|
+
background-color: var(${prefix}-color-primary);
|
|
345
|
+
border-color: var(${prefix}-color-primary);
|
|
346
|
+
color: #ffffff;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.seekora-pagination__item--disabled {
|
|
350
|
+
opacity: 0.5;
|
|
351
|
+
cursor: not-allowed;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Facets */
|
|
355
|
+
.seekora-facets {
|
|
356
|
+
font-family: var(${prefix}-font-family);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.seekora-facets__facet {
|
|
360
|
+
margin-bottom: var(${prefix}-spacing-large);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.seekora-facets__title {
|
|
364
|
+
margin: 0 0 var(${prefix}-spacing-small) 0;
|
|
365
|
+
font-size: var(${prefix}-font-size-medium);
|
|
366
|
+
font-weight: var(${prefix}-font-weight-semibold, 600);
|
|
367
|
+
color: var(${prefix}-color-text);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.seekora-facets__list {
|
|
371
|
+
list-style: none;
|
|
372
|
+
margin: 0;
|
|
373
|
+
padding: 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.seekora-facets__item {
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
gap: var(${prefix}-spacing-small);
|
|
380
|
+
padding: var(${prefix}-spacing-small) 0;
|
|
381
|
+
cursor: pointer;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.seekora-facets__checkbox {
|
|
385
|
+
width: 18px;
|
|
386
|
+
height: 18px;
|
|
387
|
+
accent-color: var(${prefix}-color-primary);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.seekora-facets__label {
|
|
391
|
+
flex: 1;
|
|
392
|
+
font-size: var(${prefix}-font-size-small);
|
|
393
|
+
color: var(${prefix}-color-text);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.seekora-facets__count {
|
|
397
|
+
font-size: var(${prefix}-font-size-small);
|
|
398
|
+
color: var(${prefix}-color-textSecondary);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.seekora-facets__show-more {
|
|
402
|
+
margin-top: var(${prefix}-spacing-small);
|
|
403
|
+
font-size: var(${prefix}-font-size-small);
|
|
404
|
+
color: var(${prefix}-color-primary);
|
|
405
|
+
background: none;
|
|
406
|
+
border: none;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
text-decoration: underline;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Stats */
|
|
412
|
+
.seekora-stats {
|
|
413
|
+
font-size: var(${prefix}-font-size-small);
|
|
414
|
+
color: var(${prefix}-color-textSecondary);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.seekora-stats__count {
|
|
418
|
+
font-weight: var(${prefix}-font-weight-semibold, 600);
|
|
419
|
+
color: var(${prefix}-color-text);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* Sort By */
|
|
423
|
+
.seekora-sort-by {
|
|
424
|
+
display: flex;
|
|
425
|
+
align-items: center;
|
|
426
|
+
gap: var(${prefix}-spacing-small);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.seekora-sort-by__label {
|
|
430
|
+
font-size: var(${prefix}-font-size-medium);
|
|
431
|
+
color: var(${prefix}-color-text);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.seekora-sort-by__select {
|
|
435
|
+
padding: var(${prefix}-spacing-small) var(${prefix}-spacing-medium);
|
|
436
|
+
font-size: var(${prefix}-font-size-medium);
|
|
437
|
+
color: var(${prefix}-color-text);
|
|
438
|
+
background-color: var(${prefix}-color-background);
|
|
439
|
+
border: 1px solid var(${prefix}-color-border);
|
|
440
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
441
|
+
cursor: pointer;
|
|
442
|
+
outline: none;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* Current Refinements */
|
|
446
|
+
.seekora-refinements {
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-wrap: wrap;
|
|
449
|
+
gap: var(${prefix}-spacing-small);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.seekora-refinements__item {
|
|
453
|
+
display: inline-flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: var(${prefix}-spacing-small);
|
|
456
|
+
padding: var(${prefix}-spacing-small) var(${prefix}-spacing-medium);
|
|
457
|
+
font-size: var(${prefix}-font-size-small);
|
|
458
|
+
background-color: var(${prefix}-color-hover);
|
|
459
|
+
border: 1px solid var(${prefix}-color-border);
|
|
460
|
+
border-radius: var(${prefix}-border-radius-full, 9999px);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.seekora-refinements__label {
|
|
464
|
+
font-weight: var(${prefix}-font-weight-medium, 500);
|
|
465
|
+
color: var(${prefix}-color-text);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.seekora-refinements__value {
|
|
469
|
+
color: var(${prefix}-color-text);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.seekora-refinements__clear {
|
|
473
|
+
width: 16px;
|
|
474
|
+
height: 16px;
|
|
475
|
+
display: flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
justify-content: center;
|
|
478
|
+
padding: 0;
|
|
479
|
+
font-size: 14px;
|
|
480
|
+
color: var(${prefix}-color-textSecondary);
|
|
481
|
+
background: none;
|
|
482
|
+
border: none;
|
|
483
|
+
border-radius: 50%;
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
transition: var(${prefix}-transition-fast);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.seekora-refinements__clear:hover {
|
|
489
|
+
color: var(${prefix}-color-text);
|
|
490
|
+
background-color: var(${prefix}-color-border);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.seekora-refinements__clear-all {
|
|
494
|
+
padding: var(${prefix}-spacing-small) var(${prefix}-spacing-medium);
|
|
495
|
+
font-size: var(${prefix}-font-size-small);
|
|
496
|
+
color: var(${prefix}-color-primary);
|
|
497
|
+
background: none;
|
|
498
|
+
border: none;
|
|
499
|
+
cursor: pointer;
|
|
500
|
+
text-decoration: underline;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* Range Input */
|
|
504
|
+
.seekora-range-input {
|
|
505
|
+
font-family: var(${prefix}-font-family);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.seekora-range-input__label {
|
|
509
|
+
display: block;
|
|
510
|
+
margin-bottom: var(${prefix}-spacing-small);
|
|
511
|
+
font-size: var(${prefix}-font-size-medium);
|
|
512
|
+
font-weight: var(${prefix}-font-weight-medium, 500);
|
|
513
|
+
color: var(${prefix}-color-text);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.seekora-range-input__group {
|
|
517
|
+
display: flex;
|
|
518
|
+
align-items: center;
|
|
519
|
+
gap: var(${prefix}-spacing-small);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.seekora-range-input__input {
|
|
523
|
+
flex: 1;
|
|
524
|
+
padding: var(${prefix}-spacing-small);
|
|
525
|
+
font-size: var(${prefix}-font-size-medium);
|
|
526
|
+
color: var(${prefix}-color-text);
|
|
527
|
+
background-color: var(${prefix}-color-background);
|
|
528
|
+
border: 1px solid var(${prefix}-color-border);
|
|
529
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
530
|
+
outline: none;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.seekora-range-input__separator {
|
|
534
|
+
color: var(${prefix}-color-textSecondary);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.seekora-range-input__button {
|
|
538
|
+
padding: var(${prefix}-spacing-small) var(${prefix}-spacing-medium);
|
|
539
|
+
font-size: var(${prefix}-font-size-medium);
|
|
540
|
+
color: #ffffff;
|
|
541
|
+
background-color: var(${prefix}-color-primary);
|
|
542
|
+
border: none;
|
|
543
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
544
|
+
cursor: pointer;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* Highlight */
|
|
548
|
+
.seekora-highlight mark,
|
|
549
|
+
.seekora-highlight__highlighted {
|
|
550
|
+
background-color: var(${prefix}-color-warning, #fff59d);
|
|
551
|
+
font-weight: var(${prefix}-font-weight-semibold, 600);
|
|
552
|
+
padding: 0 2px;
|
|
553
|
+
border-radius: 2px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* Infinite Hits */
|
|
557
|
+
.seekora-infinite-hits__show-more {
|
|
558
|
+
display: block;
|
|
559
|
+
width: 100%;
|
|
560
|
+
padding: var(${prefix}-spacing-medium);
|
|
561
|
+
margin-top: var(${prefix}-spacing-medium);
|
|
562
|
+
font-size: var(${prefix}-font-size-medium);
|
|
563
|
+
font-weight: var(${prefix}-font-weight-medium, 500);
|
|
564
|
+
color: #ffffff;
|
|
565
|
+
background-color: var(${prefix}-color-primary);
|
|
566
|
+
border: none;
|
|
567
|
+
border-radius: var(${prefix}-border-radius-medium, var(${prefix}-border-radius));
|
|
568
|
+
cursor: pointer;
|
|
569
|
+
transition: var(${prefix}-transition-fast);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.seekora-infinite-hits__show-more:disabled {
|
|
573
|
+
background-color: var(${prefix}-color-hover);
|
|
574
|
+
color: var(${prefix}-color-textSecondary);
|
|
575
|
+
cursor: not-allowed;
|
|
576
|
+
}
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Theme presets
|
|
581
|
+
*/
|
|
582
|
+
const lightThemeVariables = `
|
|
583
|
+
:root, [data-seekora-theme="light"] {
|
|
584
|
+
--seekora-color-primary: #0066cc;
|
|
585
|
+
--seekora-color-secondary: #6c757d;
|
|
586
|
+
--seekora-color-background: #ffffff;
|
|
587
|
+
--seekora-color-surface: #f8f9fa;
|
|
588
|
+
--seekora-color-text: #212529;
|
|
589
|
+
--seekora-color-textSecondary: #6c757d;
|
|
590
|
+
--seekora-color-border: #dee2e6;
|
|
591
|
+
--seekora-color-hover: #f1f3f5;
|
|
592
|
+
--seekora-color-focus: rgba(0, 102, 204, 0.25);
|
|
593
|
+
--seekora-color-error: #dc3545;
|
|
594
|
+
--seekora-color-success: #28a745;
|
|
595
|
+
--seekora-color-warning: #ffc107;
|
|
596
|
+
|
|
597
|
+
--seekora-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
598
|
+
--seekora-font-size-small: 0.875rem;
|
|
599
|
+
--seekora-font-size-medium: 1rem;
|
|
600
|
+
--seekora-font-size-large: 1.25rem;
|
|
601
|
+
--seekora-font-weight-normal: 400;
|
|
602
|
+
--seekora-font-weight-medium: 500;
|
|
603
|
+
--seekora-font-weight-semibold: 600;
|
|
604
|
+
--seekora-font-weight-bold: 700;
|
|
605
|
+
--seekora-line-height-tight: 1.25;
|
|
606
|
+
--seekora-line-height-normal: 1.5;
|
|
607
|
+
--seekora-line-height-relaxed: 1.75;
|
|
608
|
+
|
|
609
|
+
--seekora-spacing-small: 0.5rem;
|
|
610
|
+
--seekora-spacing-medium: 1rem;
|
|
611
|
+
--seekora-spacing-large: 1.5rem;
|
|
612
|
+
|
|
613
|
+
--seekora-border-radius: 4px;
|
|
614
|
+
--seekora-border-radius-none: 0;
|
|
615
|
+
--seekora-border-radius-small: 2px;
|
|
616
|
+
--seekora-border-radius-medium: 4px;
|
|
617
|
+
--seekora-border-radius-large: 8px;
|
|
618
|
+
--seekora-border-radius-full: 9999px;
|
|
619
|
+
|
|
620
|
+
--seekora-shadow-small: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
621
|
+
--seekora-shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
622
|
+
--seekora-shadow-large: 0 10px 15px rgba(0, 0, 0, 0.1);
|
|
623
|
+
|
|
624
|
+
--seekora-transition-fast: 150ms ease-in-out;
|
|
625
|
+
--seekora-transition-normal: 300ms ease-in-out;
|
|
626
|
+
--seekora-transition-slow: 500ms ease-in-out;
|
|
627
|
+
|
|
628
|
+
--seekora-z-index-dropdown: 1000;
|
|
629
|
+
--seekora-z-index-modal: 1050;
|
|
630
|
+
--seekora-z-index-tooltip: 1100;
|
|
631
|
+
}
|
|
632
|
+
`;
|
|
633
|
+
const darkThemeVariables = `
|
|
634
|
+
[data-seekora-theme="dark"] {
|
|
635
|
+
--seekora-color-primary: #4da6ff;
|
|
636
|
+
--seekora-color-secondary: #adb5bd;
|
|
637
|
+
--seekora-color-background: #1a1a1a;
|
|
638
|
+
--seekora-color-surface: #2d2d2d;
|
|
639
|
+
--seekora-color-text: #f8f9fa;
|
|
640
|
+
--seekora-color-textSecondary: #adb5bd;
|
|
641
|
+
--seekora-color-border: #495057;
|
|
642
|
+
--seekora-color-hover: #343a40;
|
|
643
|
+
--seekora-color-focus: rgba(77, 166, 255, 0.25);
|
|
644
|
+
--seekora-color-error: #f87171;
|
|
645
|
+
--seekora-color-success: #4ade80;
|
|
646
|
+
--seekora-color-warning: #fbbf24;
|
|
647
|
+
|
|
648
|
+
--seekora-shadow-small: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
649
|
+
--seekora-shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.4);
|
|
650
|
+
--seekora-shadow-large: 0 10px 15px rgba(0, 0, 0, 0.5);
|
|
651
|
+
}
|
|
652
|
+
`;
|
|
653
|
+
const minimalThemeVariables = `
|
|
654
|
+
[data-seekora-theme="minimal"] {
|
|
655
|
+
--seekora-color-primary: #000000;
|
|
656
|
+
--seekora-color-secondary: #666666;
|
|
657
|
+
--seekora-color-background: #ffffff;
|
|
658
|
+
--seekora-color-surface: #fafafa;
|
|
659
|
+
--seekora-color-text: #000000;
|
|
660
|
+
--seekora-color-textSecondary: #666666;
|
|
661
|
+
--seekora-color-border: #e5e5e5;
|
|
662
|
+
--seekora-color-hover: #f5f5f5;
|
|
663
|
+
--seekora-color-focus: rgba(0, 0, 0, 0.1);
|
|
664
|
+
|
|
665
|
+
--seekora-border-radius: 0;
|
|
666
|
+
--seekora-border-radius-none: 0;
|
|
667
|
+
--seekora-border-radius-small: 0;
|
|
668
|
+
--seekora-border-radius-medium: 0;
|
|
669
|
+
--seekora-border-radius-large: 0;
|
|
670
|
+
--seekora-border-radius-full: 0;
|
|
671
|
+
|
|
672
|
+
--seekora-shadow-small: none;
|
|
673
|
+
--seekora-shadow-medium: none;
|
|
674
|
+
--seekora-shadow-large: none;
|
|
675
|
+
}
|
|
676
|
+
`;
|
|
677
|
+
const highContrastThemeVariables = `
|
|
678
|
+
[data-seekora-theme="high-contrast"] {
|
|
679
|
+
--seekora-color-primary: #0000ff;
|
|
680
|
+
--seekora-color-secondary: #000000;
|
|
681
|
+
--seekora-color-background: #ffffff;
|
|
682
|
+
--seekora-color-surface: #ffffff;
|
|
683
|
+
--seekora-color-text: #000000;
|
|
684
|
+
--seekora-color-textSecondary: #000000;
|
|
685
|
+
--seekora-color-border: #000000;
|
|
686
|
+
--seekora-color-hover: #ffff00;
|
|
687
|
+
--seekora-color-focus: #0000ff;
|
|
688
|
+
--seekora-color-error: #ff0000;
|
|
689
|
+
--seekora-color-success: #008000;
|
|
690
|
+
--seekora-color-warning: #ffff00;
|
|
691
|
+
|
|
692
|
+
--seekora-font-weight-normal: 500;
|
|
693
|
+
--seekora-font-weight-medium: 600;
|
|
694
|
+
--seekora-font-weight-semibold: 700;
|
|
695
|
+
--seekora-font-weight-bold: 900;
|
|
696
|
+
}
|
|
697
|
+
`;
|
|
698
|
+
/**
|
|
699
|
+
* Complete stylesheet with all presets and component classes
|
|
700
|
+
*/
|
|
701
|
+
function generateCompleteStylesheet() {
|
|
702
|
+
return `
|
|
703
|
+
/* Seekora UI SDK Styles */
|
|
704
|
+
/* Generated CSS Variables and Component Classes */
|
|
705
|
+
|
|
706
|
+
${lightThemeVariables}
|
|
707
|
+
|
|
708
|
+
${darkThemeVariables}
|
|
709
|
+
|
|
710
|
+
${minimalThemeVariables}
|
|
711
|
+
|
|
712
|
+
${highContrastThemeVariables}
|
|
713
|
+
|
|
714
|
+
${generateComponentClasses('--seekora')}
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Inject styles into the document head
|
|
719
|
+
*/
|
|
720
|
+
function injectStyles(css, id = 'seekora-theme') {
|
|
721
|
+
if (typeof document === 'undefined')
|
|
722
|
+
return;
|
|
723
|
+
// Remove existing style if present
|
|
724
|
+
const existing = document.getElementById(id);
|
|
725
|
+
if (existing) {
|
|
726
|
+
existing.remove();
|
|
727
|
+
}
|
|
728
|
+
const style = document.createElement('style');
|
|
729
|
+
style.id = id;
|
|
730
|
+
style.textContent = css;
|
|
731
|
+
document.head.appendChild(style);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Set the active theme
|
|
735
|
+
*/
|
|
736
|
+
function setTheme(themeName) {
|
|
737
|
+
if (typeof document === 'undefined')
|
|
738
|
+
return;
|
|
739
|
+
document.documentElement.setAttribute('data-seekora-theme', themeName);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Get the current theme
|
|
743
|
+
*/
|
|
744
|
+
function getTheme() {
|
|
745
|
+
if (typeof document === 'undefined')
|
|
746
|
+
return 'light';
|
|
747
|
+
return document.documentElement.getAttribute('data-seekora-theme') || 'light';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Logger utility for Seekora UI SDK
|
|
752
|
+
*
|
|
753
|
+
* Provides structured logging with different log levels:
|
|
754
|
+
* - verbose: Detailed debugging information
|
|
755
|
+
* - info: General informational messages
|
|
756
|
+
* - warn: Warning messages for potential issues
|
|
757
|
+
* - error: Error messages for failures
|
|
758
|
+
*/
|
|
759
|
+
exports.LogLevel = void 0;
|
|
760
|
+
(function (LogLevel) {
|
|
761
|
+
LogLevel[LogLevel["VERBOSE"] = 0] = "VERBOSE";
|
|
762
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
763
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
764
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
765
|
+
LogLevel[LogLevel["SILENT"] = 4] = "SILENT";
|
|
766
|
+
})(exports.LogLevel || (exports.LogLevel = {}));
|
|
767
|
+
class Logger {
|
|
768
|
+
constructor(config = {}) {
|
|
769
|
+
this.level = config.level ?? this.getDefaultLevel();
|
|
770
|
+
this.prefix = config.prefix ?? '[Seekora UI SDK]';
|
|
771
|
+
this.enableTimestamp = config.enableTimestamp ?? false;
|
|
772
|
+
}
|
|
773
|
+
getDefaultLevel() {
|
|
774
|
+
// Check for environment variable or default to INFO
|
|
775
|
+
if (({})) {
|
|
776
|
+
const envLevel = undefined?.toUpperCase();
|
|
777
|
+
switch (envLevel) {
|
|
778
|
+
case 'VERBOSE':
|
|
779
|
+
return exports.LogLevel.VERBOSE;
|
|
780
|
+
case 'INFO':
|
|
781
|
+
return exports.LogLevel.INFO;
|
|
782
|
+
case 'WARN':
|
|
783
|
+
return exports.LogLevel.WARN;
|
|
784
|
+
case 'ERROR':
|
|
785
|
+
return exports.LogLevel.ERROR;
|
|
786
|
+
case 'SILENT':
|
|
787
|
+
return exports.LogLevel.SILENT;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Default to INFO in production, VERBOSE in development
|
|
791
|
+
if (typeof window !== 'undefined' && window.__SEEKORA_DEBUG__) {
|
|
792
|
+
return exports.LogLevel.VERBOSE;
|
|
793
|
+
}
|
|
794
|
+
return exports.LogLevel.INFO;
|
|
795
|
+
}
|
|
796
|
+
formatMessage(level, ...args) {
|
|
797
|
+
const parts = [];
|
|
798
|
+
if (this.enableTimestamp) {
|
|
799
|
+
parts.push(`[${new Date().toISOString()}]`);
|
|
800
|
+
}
|
|
801
|
+
parts.push(this.prefix, `[${level}]`);
|
|
802
|
+
return [...parts, ...args];
|
|
803
|
+
}
|
|
804
|
+
verbose(...args) {
|
|
805
|
+
if (this.level <= exports.LogLevel.VERBOSE) {
|
|
806
|
+
console.log(...this.formatMessage('VERBOSE', ...args));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
info(...args) {
|
|
810
|
+
if (this.level <= exports.LogLevel.INFO) {
|
|
811
|
+
console.info(...this.formatMessage('INFO', ...args));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
warn(...args) {
|
|
815
|
+
if (this.level <= exports.LogLevel.WARN) {
|
|
816
|
+
console.warn(...this.formatMessage('WARN', ...args));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
error(...args) {
|
|
820
|
+
if (this.level <= exports.LogLevel.ERROR) {
|
|
821
|
+
console.error(...this.formatMessage('ERROR', ...args));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
setLevel(level) {
|
|
825
|
+
this.level = level;
|
|
826
|
+
}
|
|
827
|
+
getLevel() {
|
|
828
|
+
return this.level;
|
|
829
|
+
}
|
|
830
|
+
setPrefix(prefix) {
|
|
831
|
+
this.prefix = prefix;
|
|
832
|
+
}
|
|
833
|
+
setTimestamp(enabled) {
|
|
834
|
+
this.enableTimestamp = enabled;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// Create default logger instance
|
|
838
|
+
let defaultLogger = null;
|
|
839
|
+
function createLogger(config = {}) {
|
|
840
|
+
return new Logger(config);
|
|
841
|
+
}
|
|
842
|
+
function getLogger() {
|
|
843
|
+
if (!defaultLogger) {
|
|
844
|
+
defaultLogger = createLogger();
|
|
845
|
+
}
|
|
846
|
+
return defaultLogger;
|
|
847
|
+
}
|
|
848
|
+
function setDefaultLogger(logger) {
|
|
849
|
+
defaultLogger = logger;
|
|
850
|
+
}
|
|
851
|
+
function setLogLevel(level) {
|
|
852
|
+
getLogger().setLevel(level);
|
|
853
|
+
}
|
|
854
|
+
function setLogPrefix(prefix) {
|
|
855
|
+
getLogger().setPrefix(prefix);
|
|
856
|
+
}
|
|
857
|
+
function setLogTimestamp(enabled) {
|
|
858
|
+
getLogger().setTimestamp(enabled);
|
|
859
|
+
}
|
|
860
|
+
// Convenience functions that use the default logger
|
|
861
|
+
const log = {
|
|
862
|
+
verbose: (...args) => getLogger().verbose(...args),
|
|
863
|
+
info: (...args) => getLogger().info(...args),
|
|
864
|
+
warn: (...args) => getLogger().warn(...args),
|
|
865
|
+
error: (...args) => getLogger().error(...args),
|
|
866
|
+
};
|
|
867
|
+
// Export default logger instance
|
|
868
|
+
getLogger();
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Highlight Utilities
|
|
872
|
+
*
|
|
873
|
+
* Core utilities for highlighting matching terms in search results
|
|
874
|
+
* Framework-agnostic functions shared across all packages
|
|
875
|
+
*/
|
|
876
|
+
/**
|
|
877
|
+
* Get highlighted value from a hit
|
|
878
|
+
* Looks for _highlightResult or highlight_result field
|
|
879
|
+
*/
|
|
880
|
+
function getHighlightedValue(options) {
|
|
881
|
+
const { attribute, hit, preTag = '<mark>', postTag = '</mark>', fallback, } = options;
|
|
882
|
+
// Check for various highlight result formats
|
|
883
|
+
const highlightResult = hit._highlightResult?.[attribute]?.value ||
|
|
884
|
+
hit.highlight_result?.[attribute]?.value ||
|
|
885
|
+
hit._highlight?.[attribute] ||
|
|
886
|
+
hit.highlight?.[attribute];
|
|
887
|
+
if (highlightResult) {
|
|
888
|
+
// Replace highlight markers with custom tags
|
|
889
|
+
return highlightResult
|
|
890
|
+
.replace(/<em>/g, preTag)
|
|
891
|
+
.replace(/<\/em>/g, postTag)
|
|
892
|
+
.replace(/<mark>/g, preTag)
|
|
893
|
+
.replace(/<\/mark>/g, postTag);
|
|
894
|
+
}
|
|
895
|
+
// Fall back to plain attribute value
|
|
896
|
+
const plainValue = getNestedValue(hit, attribute);
|
|
897
|
+
return plainValue !== undefined ? String(plainValue) : (fallback || '');
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Get snippet (truncated highlighted value) from a hit
|
|
901
|
+
*/
|
|
902
|
+
function getSnippetedValue(options) {
|
|
903
|
+
const { attribute, hit, preTag = '<mark>', postTag = '</mark>', fallback, maxLength = 100, ellipsis = '...', } = options;
|
|
904
|
+
// Check for snippet result
|
|
905
|
+
const snippetResult = hit._snippetResult?.[attribute]?.value ||
|
|
906
|
+
hit.snippet_result?.[attribute]?.value ||
|
|
907
|
+
hit._snippet?.[attribute] ||
|
|
908
|
+
hit.snippet?.[attribute];
|
|
909
|
+
let text = '';
|
|
910
|
+
if (snippetResult) {
|
|
911
|
+
text = snippetResult
|
|
912
|
+
.replace(/<em>/g, preTag)
|
|
913
|
+
.replace(/<\/em>/g, postTag)
|
|
914
|
+
.replace(/<mark>/g, preTag)
|
|
915
|
+
.replace(/<\/mark>/g, postTag);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
// Fall back to highlighted or plain value
|
|
919
|
+
text = getHighlightedValue(options);
|
|
920
|
+
}
|
|
921
|
+
// Truncate if needed
|
|
922
|
+
if (text.length > maxLength) {
|
|
923
|
+
// Try to truncate at word boundary
|
|
924
|
+
const truncated = text.substring(0, maxLength);
|
|
925
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
926
|
+
if (lastSpace > maxLength * 0.7) {
|
|
927
|
+
return truncated.substring(0, lastSpace) + ellipsis;
|
|
928
|
+
}
|
|
929
|
+
return truncated + ellipsis;
|
|
930
|
+
}
|
|
931
|
+
return text;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Parse highlighted value into parts for custom rendering
|
|
935
|
+
*/
|
|
936
|
+
function parseHighlightedParts(highlightedValue, preTag = '<mark>', postTag = '</mark>') {
|
|
937
|
+
const parts = [];
|
|
938
|
+
let remaining = highlightedValue;
|
|
939
|
+
while (remaining.length > 0) {
|
|
940
|
+
const startIdx = remaining.indexOf(preTag);
|
|
941
|
+
if (startIdx === -1) {
|
|
942
|
+
// No more highlights, add remaining as non-highlighted
|
|
943
|
+
if (remaining.length > 0) {
|
|
944
|
+
parts.push({ value: remaining, isHighlighted: false });
|
|
945
|
+
}
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
// Add text before highlight
|
|
949
|
+
if (startIdx > 0) {
|
|
950
|
+
parts.push({ value: remaining.substring(0, startIdx), isHighlighted: false });
|
|
951
|
+
}
|
|
952
|
+
// Find end of highlight
|
|
953
|
+
const afterPreTag = remaining.substring(startIdx + preTag.length);
|
|
954
|
+
const endIdx = afterPreTag.indexOf(postTag);
|
|
955
|
+
if (endIdx === -1) {
|
|
956
|
+
// Malformed, add rest as non-highlighted
|
|
957
|
+
parts.push({ value: remaining.substring(startIdx), isHighlighted: false });
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
// Add highlighted part
|
|
961
|
+
parts.push({ value: afterPreTag.substring(0, endIdx), isHighlighted: true });
|
|
962
|
+
// Continue with rest
|
|
963
|
+
remaining = afterPreTag.substring(endIdx + postTag.length);
|
|
964
|
+
}
|
|
965
|
+
return parts;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Highlight a query in text (client-side highlighting)
|
|
969
|
+
* Useful when server doesn't provide highlight data
|
|
970
|
+
*/
|
|
971
|
+
function highlightQuery(text, query, options = {}) {
|
|
972
|
+
const { preTag = '<mark>', postTag = '</mark>', caseSensitive = false } = options;
|
|
973
|
+
if (!query || !text)
|
|
974
|
+
return text;
|
|
975
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
976
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
977
|
+
const regex = new RegExp(`(${escapedQuery})`, flags);
|
|
978
|
+
return text.replace(regex, `${preTag}$1${postTag}`);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Parse query-highlighted text into parts
|
|
982
|
+
*/
|
|
983
|
+
function parseQueryHighlightParts(text, query, caseSensitive = false) {
|
|
984
|
+
if (!query || !text) {
|
|
985
|
+
return [{ value: text, isHighlighted: false }];
|
|
986
|
+
}
|
|
987
|
+
const parts = [];
|
|
988
|
+
const flags = caseSensitive ? 'g' : 'gi';
|
|
989
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
990
|
+
const regex = new RegExp(escapedQuery, flags);
|
|
991
|
+
let lastIndex = 0;
|
|
992
|
+
let match;
|
|
993
|
+
while ((match = regex.exec(text)) !== null) {
|
|
994
|
+
// Add text before match
|
|
995
|
+
if (match.index > lastIndex) {
|
|
996
|
+
parts.push({ value: text.substring(lastIndex, match.index), isHighlighted: false });
|
|
997
|
+
}
|
|
998
|
+
// Add highlighted match
|
|
999
|
+
parts.push({ value: match[0], isHighlighted: true });
|
|
1000
|
+
lastIndex = regex.lastIndex;
|
|
1001
|
+
}
|
|
1002
|
+
// Add remaining text
|
|
1003
|
+
if (lastIndex < text.length) {
|
|
1004
|
+
parts.push({ value: text.substring(lastIndex), isHighlighted: false });
|
|
1005
|
+
}
|
|
1006
|
+
return parts.length > 0 ? parts : [{ value: text, isHighlighted: false }];
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Get nested value from object using dot notation
|
|
1010
|
+
*/
|
|
1011
|
+
function getNestedValue(obj, path) {
|
|
1012
|
+
return path.split('.').reduce((current, key) => {
|
|
1013
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
1014
|
+
}, obj);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Strip HTML tags from highlighted text
|
|
1018
|
+
*/
|
|
1019
|
+
function stripHighlightTags(text) {
|
|
1020
|
+
return text
|
|
1021
|
+
.replace(/<mark>/g, '')
|
|
1022
|
+
.replace(/<\/mark>/g, '')
|
|
1023
|
+
.replace(/<em>/g, '')
|
|
1024
|
+
.replace(/<\/em>/g, '');
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Accessibility Utilities
|
|
1029
|
+
*
|
|
1030
|
+
* Shared accessibility helpers for keyboard navigation, focus management,
|
|
1031
|
+
* screen reader announcements, and ARIA attributes.
|
|
1032
|
+
*/
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
// Screen Reader Announcements (Live Regions)
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
let announcer = null;
|
|
1037
|
+
/**
|
|
1038
|
+
* Create or get the screen reader announcer element
|
|
1039
|
+
*/
|
|
1040
|
+
function getAnnouncer() {
|
|
1041
|
+
if (announcer)
|
|
1042
|
+
return announcer;
|
|
1043
|
+
if (typeof document === 'undefined') {
|
|
1044
|
+
throw new Error('Cannot create announcer: document is undefined');
|
|
1045
|
+
}
|
|
1046
|
+
announcer = document.createElement('div');
|
|
1047
|
+
announcer.id = 'seekora-announcer';
|
|
1048
|
+
announcer.setAttribute('role', 'status');
|
|
1049
|
+
announcer.setAttribute('aria-live', 'polite');
|
|
1050
|
+
announcer.setAttribute('aria-atomic', 'true');
|
|
1051
|
+
announcer.style.cssText = `
|
|
1052
|
+
position: absolute;
|
|
1053
|
+
width: 1px;
|
|
1054
|
+
height: 1px;
|
|
1055
|
+
padding: 0;
|
|
1056
|
+
margin: -1px;
|
|
1057
|
+
overflow: hidden;
|
|
1058
|
+
clip: rect(0, 0, 0, 0);
|
|
1059
|
+
white-space: nowrap;
|
|
1060
|
+
border: 0;
|
|
1061
|
+
`;
|
|
1062
|
+
document.body.appendChild(announcer);
|
|
1063
|
+
return announcer;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Announce a message to screen readers
|
|
1067
|
+
*/
|
|
1068
|
+
function announce(message, priority = 'polite') {
|
|
1069
|
+
if (typeof document === 'undefined')
|
|
1070
|
+
return;
|
|
1071
|
+
try {
|
|
1072
|
+
const el = getAnnouncer();
|
|
1073
|
+
el.setAttribute('aria-live', priority);
|
|
1074
|
+
// Clear and set message (needs a brief delay to trigger announcement)
|
|
1075
|
+
el.textContent = '';
|
|
1076
|
+
setTimeout(() => {
|
|
1077
|
+
el.textContent = message;
|
|
1078
|
+
}, 50);
|
|
1079
|
+
}
|
|
1080
|
+
catch (e) {
|
|
1081
|
+
// Ignore errors in SSR
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Announce search results count
|
|
1086
|
+
*/
|
|
1087
|
+
function announceResults(count, query) {
|
|
1088
|
+
let message = '';
|
|
1089
|
+
if (count === 0) {
|
|
1090
|
+
message = query
|
|
1091
|
+
? `No results found for "${query}"`
|
|
1092
|
+
: 'No results found';
|
|
1093
|
+
}
|
|
1094
|
+
else if (count === 1) {
|
|
1095
|
+
message = query
|
|
1096
|
+
? `1 result found for "${query}"`
|
|
1097
|
+
: '1 result found';
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
message = query
|
|
1101
|
+
? `${count} results found for "${query}"`
|
|
1102
|
+
: `${count} results found`;
|
|
1103
|
+
}
|
|
1104
|
+
announce(message);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Announce filter changes
|
|
1108
|
+
*/
|
|
1109
|
+
function announceFilterChange(action, filterName, filterValue) {
|
|
1110
|
+
let message = '';
|
|
1111
|
+
switch (action) {
|
|
1112
|
+
case 'added':
|
|
1113
|
+
message = `Filter ${filterName}: ${filterValue} applied`;
|
|
1114
|
+
break;
|
|
1115
|
+
case 'removed':
|
|
1116
|
+
message = `Filter ${filterName}: ${filterValue} removed`;
|
|
1117
|
+
break;
|
|
1118
|
+
case 'cleared':
|
|
1119
|
+
message = 'All filters cleared';
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
announce(message);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Handle keyboard navigation for a list of items
|
|
1126
|
+
*/
|
|
1127
|
+
function handleKeyboardNavigation(event, options) {
|
|
1128
|
+
const { items, activeIndex, onActiveChange, onSelect, onCancel, wrap = true, orientation = 'vertical', } = options;
|
|
1129
|
+
const itemCount = items.length;
|
|
1130
|
+
if (itemCount === 0)
|
|
1131
|
+
return false;
|
|
1132
|
+
let handled = false;
|
|
1133
|
+
let newIndex = activeIndex;
|
|
1134
|
+
switch (event.key) {
|
|
1135
|
+
case 'ArrowDown':
|
|
1136
|
+
if (orientation === 'vertical' || orientation === 'both') {
|
|
1137
|
+
event.preventDefault();
|
|
1138
|
+
if (activeIndex < itemCount - 1) {
|
|
1139
|
+
newIndex = activeIndex + 1;
|
|
1140
|
+
}
|
|
1141
|
+
else if (wrap) {
|
|
1142
|
+
newIndex = 0;
|
|
1143
|
+
}
|
|
1144
|
+
handled = true;
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
case 'ArrowUp':
|
|
1148
|
+
if (orientation === 'vertical' || orientation === 'both') {
|
|
1149
|
+
event.preventDefault();
|
|
1150
|
+
if (activeIndex > 0) {
|
|
1151
|
+
newIndex = activeIndex - 1;
|
|
1152
|
+
}
|
|
1153
|
+
else if (wrap) {
|
|
1154
|
+
newIndex = itemCount - 1;
|
|
1155
|
+
}
|
|
1156
|
+
handled = true;
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
case 'ArrowRight':
|
|
1160
|
+
if (orientation === 'horizontal' || orientation === 'both') {
|
|
1161
|
+
event.preventDefault();
|
|
1162
|
+
if (activeIndex < itemCount - 1) {
|
|
1163
|
+
newIndex = activeIndex + 1;
|
|
1164
|
+
}
|
|
1165
|
+
else if (wrap) {
|
|
1166
|
+
newIndex = 0;
|
|
1167
|
+
}
|
|
1168
|
+
handled = true;
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
case 'ArrowLeft':
|
|
1172
|
+
if (orientation === 'horizontal' || orientation === 'both') {
|
|
1173
|
+
event.preventDefault();
|
|
1174
|
+
if (activeIndex > 0) {
|
|
1175
|
+
newIndex = activeIndex - 1;
|
|
1176
|
+
}
|
|
1177
|
+
else if (wrap) {
|
|
1178
|
+
newIndex = itemCount - 1;
|
|
1179
|
+
}
|
|
1180
|
+
handled = true;
|
|
1181
|
+
}
|
|
1182
|
+
break;
|
|
1183
|
+
case 'Home':
|
|
1184
|
+
event.preventDefault();
|
|
1185
|
+
newIndex = 0;
|
|
1186
|
+
handled = true;
|
|
1187
|
+
break;
|
|
1188
|
+
case 'End':
|
|
1189
|
+
event.preventDefault();
|
|
1190
|
+
newIndex = itemCount - 1;
|
|
1191
|
+
handled = true;
|
|
1192
|
+
break;
|
|
1193
|
+
case 'Enter':
|
|
1194
|
+
case ' ':
|
|
1195
|
+
event.preventDefault();
|
|
1196
|
+
if (activeIndex >= 0 && onSelect) {
|
|
1197
|
+
onSelect(activeIndex);
|
|
1198
|
+
}
|
|
1199
|
+
handled = true;
|
|
1200
|
+
break;
|
|
1201
|
+
case 'Escape':
|
|
1202
|
+
if (onCancel) {
|
|
1203
|
+
event.preventDefault();
|
|
1204
|
+
onCancel();
|
|
1205
|
+
handled = true;
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
if (newIndex !== activeIndex && newIndex >= 0 && newIndex < itemCount) {
|
|
1210
|
+
onActiveChange(newIndex);
|
|
1211
|
+
// Focus the new item
|
|
1212
|
+
const item = items[newIndex];
|
|
1213
|
+
if (item) {
|
|
1214
|
+
item.focus();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return handled;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Create a roving tabindex manager for accessible list navigation
|
|
1221
|
+
*/
|
|
1222
|
+
function createRovingTabIndex(container, selector) {
|
|
1223
|
+
const items = Array.from(container.querySelectorAll(selector));
|
|
1224
|
+
// Initialize: first item gets tabindex 0, others get -1
|
|
1225
|
+
items.forEach((item, index) => {
|
|
1226
|
+
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|
1227
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
update: (activeIndex) => {
|
|
1230
|
+
items.forEach((item, index) => {
|
|
1231
|
+
item.setAttribute('tabindex', index === activeIndex ? '0' : '-1');
|
|
1232
|
+
});
|
|
1233
|
+
},
|
|
1234
|
+
destroy: () => {
|
|
1235
|
+
items.forEach(item => {
|
|
1236
|
+
item.removeAttribute('tabindex');
|
|
1237
|
+
});
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
// ============================================================================
|
|
1242
|
+
// Focus Management
|
|
1243
|
+
// ============================================================================
|
|
1244
|
+
/**
|
|
1245
|
+
* Get all focusable elements within a container
|
|
1246
|
+
*/
|
|
1247
|
+
function getFocusableElements(container) {
|
|
1248
|
+
const selector = [
|
|
1249
|
+
'button:not([disabled])',
|
|
1250
|
+
'[href]',
|
|
1251
|
+
'input:not([disabled])',
|
|
1252
|
+
'select:not([disabled])',
|
|
1253
|
+
'textarea:not([disabled])',
|
|
1254
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
1255
|
+
].join(', ');
|
|
1256
|
+
return Array.from(container.querySelectorAll(selector));
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Create a focus trap within a container (for modals/dialogs)
|
|
1260
|
+
*/
|
|
1261
|
+
function createFocusTrap(container) {
|
|
1262
|
+
let previouslyFocused = null;
|
|
1263
|
+
let handleKeyDown = null;
|
|
1264
|
+
return {
|
|
1265
|
+
activate: () => {
|
|
1266
|
+
previouslyFocused = document.activeElement;
|
|
1267
|
+
const focusable = getFocusableElements(container);
|
|
1268
|
+
if (focusable.length === 0)
|
|
1269
|
+
return;
|
|
1270
|
+
const firstFocusable = focusable[0];
|
|
1271
|
+
const lastFocusable = focusable[focusable.length - 1];
|
|
1272
|
+
handleKeyDown = (e) => {
|
|
1273
|
+
if (e.key !== 'Tab')
|
|
1274
|
+
return;
|
|
1275
|
+
if (e.shiftKey) {
|
|
1276
|
+
if (document.activeElement === firstFocusable) {
|
|
1277
|
+
e.preventDefault();
|
|
1278
|
+
lastFocusable.focus();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
if (document.activeElement === lastFocusable) {
|
|
1283
|
+
e.preventDefault();
|
|
1284
|
+
firstFocusable.focus();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
1289
|
+
firstFocusable.focus();
|
|
1290
|
+
},
|
|
1291
|
+
deactivate: () => {
|
|
1292
|
+
if (handleKeyDown) {
|
|
1293
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
1294
|
+
handleKeyDown = null;
|
|
1295
|
+
}
|
|
1296
|
+
if (previouslyFocused) {
|
|
1297
|
+
previouslyFocused.focus();
|
|
1298
|
+
previouslyFocused = null;
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Focus first error or first input in a form
|
|
1305
|
+
*/
|
|
1306
|
+
function focusFirstError(container) {
|
|
1307
|
+
const error = container.querySelector('[aria-invalid="true"]');
|
|
1308
|
+
if (error) {
|
|
1309
|
+
error.focus();
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
// ============================================================================
|
|
1315
|
+
// ARIA Helpers
|
|
1316
|
+
// ============================================================================
|
|
1317
|
+
/**
|
|
1318
|
+
* Generate a unique ID for ARIA relationships
|
|
1319
|
+
*/
|
|
1320
|
+
let idCounter = 0;
|
|
1321
|
+
function generateId(prefix = 'seekora') {
|
|
1322
|
+
return `${prefix}-${++idCounter}`;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Set up ARIA describedby relationship
|
|
1326
|
+
*/
|
|
1327
|
+
function setAriaDescribedBy(element, describer) {
|
|
1328
|
+
const id = describer.id || generateId('description');
|
|
1329
|
+
describer.id = id;
|
|
1330
|
+
const existing = element.getAttribute('aria-describedby');
|
|
1331
|
+
const newValue = existing ? `${existing} ${id}` : id;
|
|
1332
|
+
element.setAttribute('aria-describedby', newValue);
|
|
1333
|
+
return () => {
|
|
1334
|
+
const current = element.getAttribute('aria-describedby');
|
|
1335
|
+
if (current) {
|
|
1336
|
+
const ids = current.split(' ').filter(i => i !== id);
|
|
1337
|
+
if (ids.length > 0) {
|
|
1338
|
+
element.setAttribute('aria-describedby', ids.join(' '));
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
element.removeAttribute('aria-describedby');
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Check if user prefers reduced motion
|
|
1348
|
+
*/
|
|
1349
|
+
function prefersReducedMotion() {
|
|
1350
|
+
if (typeof window === 'undefined')
|
|
1351
|
+
return false;
|
|
1352
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Check if user prefers high contrast
|
|
1356
|
+
*/
|
|
1357
|
+
function prefersHighContrast() {
|
|
1358
|
+
if (typeof window === 'undefined')
|
|
1359
|
+
return false;
|
|
1360
|
+
return window.matchMedia('(prefers-contrast: more)').matches;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Create a keyboard shortcuts manager
|
|
1364
|
+
*/
|
|
1365
|
+
function createKeyboardShortcuts(shortcuts) {
|
|
1366
|
+
const handleKeyDown = (e) => {
|
|
1367
|
+
for (const shortcut of shortcuts) {
|
|
1368
|
+
const keyMatches = e.key.toLowerCase() === shortcut.key.toLowerCase();
|
|
1369
|
+
const ctrlMatches = !shortcut.ctrl || e.ctrlKey;
|
|
1370
|
+
const altMatches = !shortcut.alt || e.altKey;
|
|
1371
|
+
const shiftMatches = !shortcut.shift || e.shiftKey;
|
|
1372
|
+
const metaMatches = !shortcut.meta || e.metaKey;
|
|
1373
|
+
if (keyMatches && ctrlMatches && altMatches && shiftMatches && metaMatches) {
|
|
1374
|
+
e.preventDefault();
|
|
1375
|
+
shortcut.handler(e);
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
return {
|
|
1381
|
+
attach: () => {
|
|
1382
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
1383
|
+
},
|
|
1384
|
+
detach: () => {
|
|
1385
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
1386
|
+
},
|
|
1387
|
+
getShortcuts: () => [...shortcuts],
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* SearchStateManager
|
|
1393
|
+
*
|
|
1394
|
+
* Centralized state management for search operations
|
|
1395
|
+
* Framework-agnostic core logic shared across all packages
|
|
1396
|
+
* Handles query, refinements, pagination, sorting, and automatic search triggering
|
|
1397
|
+
* Similar to InstantSearch.js architecture
|
|
1398
|
+
*/
|
|
1399
|
+
class SearchStateManager {
|
|
1400
|
+
constructor(config) {
|
|
1401
|
+
this.listeners = [];
|
|
1402
|
+
this.debounceTimer = null;
|
|
1403
|
+
this.client = config.client;
|
|
1404
|
+
this.autoSearch = config.autoSearch !== false;
|
|
1405
|
+
this.debounceMs = config.debounceMs || 300;
|
|
1406
|
+
this.defaultSearchOptions = config.defaultSearchOptions || { widget_mode: true };
|
|
1407
|
+
this.state = {
|
|
1408
|
+
query: config.initialQuery || '',
|
|
1409
|
+
refinements: [],
|
|
1410
|
+
currentPage: 1,
|
|
1411
|
+
itemsPerPage: config.itemsPerPage || 10,
|
|
1412
|
+
sortBy: undefined,
|
|
1413
|
+
results: null,
|
|
1414
|
+
loading: false,
|
|
1415
|
+
error: null,
|
|
1416
|
+
};
|
|
1417
|
+
log.info('SearchStateManager: Initialized', {
|
|
1418
|
+
autoSearch: this.autoSearch,
|
|
1419
|
+
debounceMs: this.debounceMs,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
// State getters
|
|
1423
|
+
getState() {
|
|
1424
|
+
return { ...this.state };
|
|
1425
|
+
}
|
|
1426
|
+
getQuery() {
|
|
1427
|
+
return this.state.query;
|
|
1428
|
+
}
|
|
1429
|
+
getRefinements() {
|
|
1430
|
+
return [...this.state.refinements];
|
|
1431
|
+
}
|
|
1432
|
+
getCurrentPage() {
|
|
1433
|
+
return this.state.currentPage;
|
|
1434
|
+
}
|
|
1435
|
+
getResults() {
|
|
1436
|
+
return this.state.results;
|
|
1437
|
+
}
|
|
1438
|
+
getLoading() {
|
|
1439
|
+
return this.state.loading;
|
|
1440
|
+
}
|
|
1441
|
+
getError() {
|
|
1442
|
+
return this.state.error;
|
|
1443
|
+
}
|
|
1444
|
+
// State setters (automatically trigger search if autoSearch is enabled)
|
|
1445
|
+
setQuery(query, triggerSearch = true) {
|
|
1446
|
+
if (this.state.query === query) {
|
|
1447
|
+
log.verbose('SearchStateManager: Query unchanged, skipping update', { query });
|
|
1448
|
+
// Even if query is the same, trigger search if explicitly requested
|
|
1449
|
+
if (triggerSearch && this.autoSearch) {
|
|
1450
|
+
log.verbose('SearchStateManager: Query unchanged but triggerSearch=true, triggering search');
|
|
1451
|
+
this.debouncedSearch();
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
this.state.query = query;
|
|
1456
|
+
this.state.currentPage = 1; // Reset to first page on new query
|
|
1457
|
+
log.verbose('SearchStateManager: Query updated', { query, triggerSearch, autoSearch: this.autoSearch });
|
|
1458
|
+
this.notifyListeners();
|
|
1459
|
+
if (this.autoSearch && triggerSearch) {
|
|
1460
|
+
log.verbose('SearchStateManager: Triggering debounced search');
|
|
1461
|
+
this.debouncedSearch();
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
log.verbose('SearchStateManager: Search not triggered', { autoSearch: this.autoSearch, triggerSearch });
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
addRefinement(field, value, triggerSearch = true) {
|
|
1468
|
+
// Check if refinement already exists
|
|
1469
|
+
const exists = this.state.refinements.some(r => r.field === field && r.value === value);
|
|
1470
|
+
if (exists)
|
|
1471
|
+
return;
|
|
1472
|
+
this.state.refinements.push({ field, value });
|
|
1473
|
+
this.state.currentPage = 1; // Reset to first page on filter change
|
|
1474
|
+
log.verbose('SearchStateManager: Refinement added', { field, value });
|
|
1475
|
+
this.notifyListeners();
|
|
1476
|
+
if (this.autoSearch && triggerSearch) {
|
|
1477
|
+
this.debouncedSearch();
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
removeRefinement(field, value, triggerSearch = true) {
|
|
1481
|
+
const index = this.state.refinements.findIndex(r => r.field === field && r.value === value);
|
|
1482
|
+
if (index === -1)
|
|
1483
|
+
return;
|
|
1484
|
+
this.state.refinements.splice(index, 1);
|
|
1485
|
+
this.state.currentPage = 1; // Reset to first page on filter change
|
|
1486
|
+
log.verbose('SearchStateManager: Refinement removed', { field, value });
|
|
1487
|
+
this.notifyListeners();
|
|
1488
|
+
if (this.autoSearch && triggerSearch) {
|
|
1489
|
+
this.debouncedSearch();
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
clearRefinements(triggerSearch = true) {
|
|
1493
|
+
if (this.state.refinements.length === 0)
|
|
1494
|
+
return;
|
|
1495
|
+
this.state.refinements = [];
|
|
1496
|
+
this.state.currentPage = 1;
|
|
1497
|
+
log.verbose('SearchStateManager: Refinements cleared');
|
|
1498
|
+
this.notifyListeners();
|
|
1499
|
+
if (this.autoSearch && triggerSearch) {
|
|
1500
|
+
this.debouncedSearch();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
setPage(page, triggerSearch = true) {
|
|
1504
|
+
if (this.state.currentPage === page)
|
|
1505
|
+
return;
|
|
1506
|
+
this.state.currentPage = page;
|
|
1507
|
+
log.verbose('SearchStateManager: Page updated', { page });
|
|
1508
|
+
this.notifyListeners();
|
|
1509
|
+
if (this.autoSearch && triggerSearch) {
|
|
1510
|
+
this.debouncedSearch();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
setSortBy(sortBy, triggerSearch = true) {
|
|
1514
|
+
if (this.state.sortBy === sortBy)
|
|
1515
|
+
return;
|
|
1516
|
+
this.state.sortBy = sortBy;
|
|
1517
|
+
this.state.currentPage = 1; // Reset to first page on sort change
|
|
1518
|
+
log.verbose('SearchStateManager: Sort updated', { sortBy });
|
|
1519
|
+
this.notifyListeners();
|
|
1520
|
+
if (this.autoSearch && triggerSearch) {
|
|
1521
|
+
this.debouncedSearch();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
setItemsPerPage(itemsPerPage, triggerSearch = true) {
|
|
1525
|
+
if (this.state.itemsPerPage === itemsPerPage)
|
|
1526
|
+
return;
|
|
1527
|
+
this.state.itemsPerPage = itemsPerPage;
|
|
1528
|
+
this.state.currentPage = 1; // Reset to first page when changing items per page
|
|
1529
|
+
log.verbose('SearchStateManager: Items per page updated', { itemsPerPage });
|
|
1530
|
+
this.notifyListeners();
|
|
1531
|
+
if (this.autoSearch && triggerSearch) {
|
|
1532
|
+
this.debouncedSearch();
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
// Manual search trigger
|
|
1536
|
+
async search(additionalOptions) {
|
|
1537
|
+
// Clear debounce timer if exists
|
|
1538
|
+
if (this.debounceTimer) {
|
|
1539
|
+
clearTimeout(this.debounceTimer);
|
|
1540
|
+
this.debounceTimer = null;
|
|
1541
|
+
}
|
|
1542
|
+
this.setState({ loading: true, error: null });
|
|
1543
|
+
try {
|
|
1544
|
+
const searchOptions = this.buildSearchOptions(additionalOptions);
|
|
1545
|
+
log.verbose('SearchStateManager: Performing search', {
|
|
1546
|
+
query: this.state.query,
|
|
1547
|
+
refinementsCount: this.state.refinements.length,
|
|
1548
|
+
currentPage: this.state.currentPage,
|
|
1549
|
+
sortBy: this.state.sortBy,
|
|
1550
|
+
filter_by: searchOptions.filter_by,
|
|
1551
|
+
});
|
|
1552
|
+
const response = await this.client.search(this.state.query, searchOptions);
|
|
1553
|
+
this.setState({
|
|
1554
|
+
results: response,
|
|
1555
|
+
loading: false,
|
|
1556
|
+
error: null,
|
|
1557
|
+
});
|
|
1558
|
+
log.info('SearchStateManager: Search completed', {
|
|
1559
|
+
query: this.state.query,
|
|
1560
|
+
resultsCount: response?.results?.length || 0,
|
|
1561
|
+
totalResults: response?.totalResults || 0,
|
|
1562
|
+
});
|
|
1563
|
+
return response;
|
|
1564
|
+
}
|
|
1565
|
+
catch (err) {
|
|
1566
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1567
|
+
this.setState({
|
|
1568
|
+
results: null,
|
|
1569
|
+
loading: false,
|
|
1570
|
+
error,
|
|
1571
|
+
});
|
|
1572
|
+
log.error('SearchStateManager: Search failed', {
|
|
1573
|
+
query: this.state.query,
|
|
1574
|
+
error: error.message,
|
|
1575
|
+
});
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// Build search options from current state
|
|
1580
|
+
buildSearchOptions(additionalOptions) {
|
|
1581
|
+
const options = {
|
|
1582
|
+
...this.defaultSearchOptions,
|
|
1583
|
+
page: this.state.currentPage,
|
|
1584
|
+
per_page: this.state.itemsPerPage,
|
|
1585
|
+
...additionalOptions,
|
|
1586
|
+
};
|
|
1587
|
+
// Add sort_by if set and not "relevance" (relevance is the default, no sort field needed)
|
|
1588
|
+
if (this.state.sortBy && this.state.sortBy !== 'relevance') {
|
|
1589
|
+
options.sort_by = this.state.sortBy;
|
|
1590
|
+
}
|
|
1591
|
+
// Build filter_by from refinements
|
|
1592
|
+
if (this.state.refinements.length > 0) {
|
|
1593
|
+
const filtersByField = {};
|
|
1594
|
+
this.state.refinements.forEach((refinement) => {
|
|
1595
|
+
if (!filtersByField[refinement.field]) {
|
|
1596
|
+
filtersByField[refinement.field] = [];
|
|
1597
|
+
}
|
|
1598
|
+
filtersByField[refinement.field].push(refinement.value);
|
|
1599
|
+
});
|
|
1600
|
+
const filterParts = [];
|
|
1601
|
+
Object.entries(filtersByField).forEach(([field, values]) => {
|
|
1602
|
+
values.forEach((value) => {
|
|
1603
|
+
filterParts.push(`${field}:${value}`);
|
|
1604
|
+
});
|
|
1605
|
+
});
|
|
1606
|
+
if (filterParts.length > 0) {
|
|
1607
|
+
options.filter_by = filterParts.join(' && ');
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return options;
|
|
1611
|
+
}
|
|
1612
|
+
// Debounced search trigger
|
|
1613
|
+
debouncedSearch() {
|
|
1614
|
+
if (this.debounceTimer) {
|
|
1615
|
+
clearTimeout(this.debounceTimer);
|
|
1616
|
+
}
|
|
1617
|
+
log.verbose('SearchStateManager: Scheduling debounced search', { debounceMs: this.debounceMs });
|
|
1618
|
+
this.debounceTimer = setTimeout(() => {
|
|
1619
|
+
log.verbose('SearchStateManager: Debounce timer fired, calling search()');
|
|
1620
|
+
this.search();
|
|
1621
|
+
}, this.debounceMs);
|
|
1622
|
+
}
|
|
1623
|
+
// Subscribe to state changes
|
|
1624
|
+
subscribe(listener) {
|
|
1625
|
+
this.listeners.push(listener);
|
|
1626
|
+
// Immediately call listener with current state
|
|
1627
|
+
listener(this.getState());
|
|
1628
|
+
// Return unsubscribe function
|
|
1629
|
+
return () => {
|
|
1630
|
+
this.listeners = this.listeners.filter(l => l !== listener);
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
// Update state and notify listeners
|
|
1634
|
+
setState(updates) {
|
|
1635
|
+
this.state = { ...this.state, ...updates };
|
|
1636
|
+
this.notifyListeners();
|
|
1637
|
+
}
|
|
1638
|
+
// Notify all listeners of state changes
|
|
1639
|
+
notifyListeners() {
|
|
1640
|
+
const state = this.getState();
|
|
1641
|
+
this.listeners.forEach(listener => {
|
|
1642
|
+
try {
|
|
1643
|
+
listener(state);
|
|
1644
|
+
}
|
|
1645
|
+
catch (err) {
|
|
1646
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1647
|
+
log.error('SearchStateManager: Error in listener', {
|
|
1648
|
+
error: error.message,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
// Clear all state
|
|
1654
|
+
clear() {
|
|
1655
|
+
this.state = {
|
|
1656
|
+
query: '',
|
|
1657
|
+
refinements: [],
|
|
1658
|
+
currentPage: 1,
|
|
1659
|
+
itemsPerPage: this.state.itemsPerPage,
|
|
1660
|
+
sortBy: undefined,
|
|
1661
|
+
results: null,
|
|
1662
|
+
loading: false,
|
|
1663
|
+
error: null,
|
|
1664
|
+
};
|
|
1665
|
+
this.notifyListeners();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* URL Router
|
|
1671
|
+
*
|
|
1672
|
+
* Syncs search state with browser URL for shareable links and back/forward navigation
|
|
1673
|
+
* Framework-agnostic core logic shared across all packages
|
|
1674
|
+
*/
|
|
1675
|
+
class URLRouter {
|
|
1676
|
+
constructor(config) {
|
|
1677
|
+
this.unsubscribe = null;
|
|
1678
|
+
this.debounceTimer = null;
|
|
1679
|
+
this.isUpdatingFromUrl = false;
|
|
1680
|
+
/**
|
|
1681
|
+
* Handle browser back/forward navigation
|
|
1682
|
+
*/
|
|
1683
|
+
this.handlePopState = () => {
|
|
1684
|
+
if (this.readFromUrl) {
|
|
1685
|
+
// Clear current refinements first
|
|
1686
|
+
this.stateManager.clear();
|
|
1687
|
+
this.readStateFromUrl();
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
this.stateManager = config.stateManager;
|
|
1691
|
+
this.writeToUrl = config.writeToUrl !== false;
|
|
1692
|
+
this.readFromUrl = config.readFromUrl !== false;
|
|
1693
|
+
this.historyMode = config.historyMode || 'push';
|
|
1694
|
+
this.debounceMs = config.debounceMs ?? 400;
|
|
1695
|
+
this.basePath = config.basePath || (typeof window !== 'undefined' ? window.location.pathname : '/');
|
|
1696
|
+
this.triggerSearchOnChange = config.triggerSearchOnChange !== false;
|
|
1697
|
+
this.mapping = {
|
|
1698
|
+
query: config.mapping?.query || 'q',
|
|
1699
|
+
page: config.mapping?.page || 'page',
|
|
1700
|
+
hitsPerPage: config.mapping?.hitsPerPage || 'hitsPerPage',
|
|
1701
|
+
sortBy: config.mapping?.sortBy || 'sortBy',
|
|
1702
|
+
refinementPrefix: config.mapping?.refinementPrefix || '',
|
|
1703
|
+
refinementSerializer: config.mapping?.refinementSerializer,
|
|
1704
|
+
refinementDeserializer: config.mapping?.refinementDeserializer,
|
|
1705
|
+
};
|
|
1706
|
+
// Initialize
|
|
1707
|
+
this.init();
|
|
1708
|
+
}
|
|
1709
|
+
init() {
|
|
1710
|
+
if (typeof window === 'undefined') {
|
|
1711
|
+
log.warn('URLRouter: Not in browser environment, skipping initialization');
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
// Read initial state from URL
|
|
1715
|
+
if (this.readFromUrl) {
|
|
1716
|
+
this.readStateFromUrl();
|
|
1717
|
+
}
|
|
1718
|
+
// Subscribe to state changes
|
|
1719
|
+
if (this.writeToUrl) {
|
|
1720
|
+
this.unsubscribe = this.stateManager.subscribe((state) => {
|
|
1721
|
+
if (!this.isUpdatingFromUrl) {
|
|
1722
|
+
this.debouncedWriteToUrl(state);
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
// Listen for popstate (back/forward navigation)
|
|
1727
|
+
window.addEventListener('popstate', this.handlePopState);
|
|
1728
|
+
log.info('URLRouter: Initialized', {
|
|
1729
|
+
writeToUrl: this.writeToUrl,
|
|
1730
|
+
readFromUrl: this.readFromUrl,
|
|
1731
|
+
historyMode: this.historyMode,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Read state from current URL and update StateManager
|
|
1736
|
+
*/
|
|
1737
|
+
readStateFromUrl() {
|
|
1738
|
+
const params = new URLSearchParams(window.location.search);
|
|
1739
|
+
this.isUpdatingFromUrl = true;
|
|
1740
|
+
try {
|
|
1741
|
+
// Query
|
|
1742
|
+
const query = params.get(this.mapping.query);
|
|
1743
|
+
if (query !== null) {
|
|
1744
|
+
this.stateManager.setQuery(query, false);
|
|
1745
|
+
}
|
|
1746
|
+
// Page
|
|
1747
|
+
const page = params.get(this.mapping.page);
|
|
1748
|
+
if (page !== null) {
|
|
1749
|
+
const pageNum = parseInt(page, 10);
|
|
1750
|
+
if (!isNaN(pageNum) && pageNum > 0) {
|
|
1751
|
+
this.stateManager.setPage(pageNum, false);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
// Hits per page
|
|
1755
|
+
const hitsPerPage = params.get(this.mapping.hitsPerPage);
|
|
1756
|
+
if (hitsPerPage !== null) {
|
|
1757
|
+
const hpp = parseInt(hitsPerPage, 10);
|
|
1758
|
+
if (!isNaN(hpp) && hpp > 0) {
|
|
1759
|
+
this.stateManager.setItemsPerPage(hpp, false);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
// Sort by
|
|
1763
|
+
const sortBy = params.get(this.mapping.sortBy);
|
|
1764
|
+
if (sortBy !== null) {
|
|
1765
|
+
this.stateManager.setSortBy(sortBy, false);
|
|
1766
|
+
}
|
|
1767
|
+
// Refinements
|
|
1768
|
+
const refinements = this.deserializeRefinements(params);
|
|
1769
|
+
refinements.forEach(r => {
|
|
1770
|
+
this.stateManager.addRefinement(r.field, r.value, false);
|
|
1771
|
+
});
|
|
1772
|
+
// Trigger search if there's any state
|
|
1773
|
+
if (this.triggerSearchOnChange && (query || refinements.length > 0)) {
|
|
1774
|
+
this.stateManager.search();
|
|
1775
|
+
}
|
|
1776
|
+
log.verbose('URLRouter: Read state from URL', {
|
|
1777
|
+
query,
|
|
1778
|
+
page,
|
|
1779
|
+
refinements: refinements.length,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
finally {
|
|
1783
|
+
this.isUpdatingFromUrl = false;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Write state to URL
|
|
1788
|
+
*/
|
|
1789
|
+
writeStateToUrl(state) {
|
|
1790
|
+
const params = new URLSearchParams();
|
|
1791
|
+
// Query
|
|
1792
|
+
if (state.query) {
|
|
1793
|
+
params.set(this.mapping.query, state.query);
|
|
1794
|
+
}
|
|
1795
|
+
// Page (only if not 1)
|
|
1796
|
+
if (state.currentPage > 1) {
|
|
1797
|
+
params.set(this.mapping.page, state.currentPage.toString());
|
|
1798
|
+
}
|
|
1799
|
+
// Hits per page (only if not default)
|
|
1800
|
+
if (state.itemsPerPage !== 10) {
|
|
1801
|
+
params.set(this.mapping.hitsPerPage, state.itemsPerPage.toString());
|
|
1802
|
+
}
|
|
1803
|
+
// Sort by
|
|
1804
|
+
if (state.sortBy && state.sortBy !== 'relevance') {
|
|
1805
|
+
params.set(this.mapping.sortBy, state.sortBy);
|
|
1806
|
+
}
|
|
1807
|
+
// Refinements
|
|
1808
|
+
this.serializeRefinements(state.refinements, params);
|
|
1809
|
+
// Build URL
|
|
1810
|
+
const queryString = params.toString();
|
|
1811
|
+
const url = queryString ? `${this.basePath}?${queryString}` : this.basePath;
|
|
1812
|
+
// Update URL
|
|
1813
|
+
if (this.historyMode === 'push') {
|
|
1814
|
+
window.history.pushState({}, '', url);
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
window.history.replaceState({}, '', url);
|
|
1818
|
+
}
|
|
1819
|
+
log.verbose('URLRouter: Wrote state to URL', { url });
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Debounced write to URL
|
|
1823
|
+
*/
|
|
1824
|
+
debouncedWriteToUrl(state) {
|
|
1825
|
+
if (this.debounceTimer) {
|
|
1826
|
+
clearTimeout(this.debounceTimer);
|
|
1827
|
+
}
|
|
1828
|
+
this.debounceTimer = setTimeout(() => {
|
|
1829
|
+
this.writeStateToUrl(state);
|
|
1830
|
+
}, this.debounceMs);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Serialize refinements to URL params
|
|
1834
|
+
*/
|
|
1835
|
+
serializeRefinements(refinements, params) {
|
|
1836
|
+
if (this.mapping.refinementSerializer) {
|
|
1837
|
+
const serialized = this.mapping.refinementSerializer(refinements);
|
|
1838
|
+
Object.entries(serialized).forEach(([key, value]) => {
|
|
1839
|
+
params.set(key, value);
|
|
1840
|
+
});
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
// Default serialization: field[]=value
|
|
1844
|
+
const byField = {};
|
|
1845
|
+
refinements.forEach(r => {
|
|
1846
|
+
const key = this.mapping.refinementPrefix + r.field;
|
|
1847
|
+
if (!byField[key]) {
|
|
1848
|
+
byField[key] = [];
|
|
1849
|
+
}
|
|
1850
|
+
byField[key].push(r.value);
|
|
1851
|
+
});
|
|
1852
|
+
Object.entries(byField).forEach(([field, values]) => {
|
|
1853
|
+
values.forEach(value => {
|
|
1854
|
+
params.append(field, value);
|
|
1855
|
+
});
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Deserialize refinements from URL params
|
|
1860
|
+
*/
|
|
1861
|
+
deserializeRefinements(params) {
|
|
1862
|
+
if (this.mapping.refinementDeserializer) {
|
|
1863
|
+
return this.mapping.refinementDeserializer(params);
|
|
1864
|
+
}
|
|
1865
|
+
// Default deserialization
|
|
1866
|
+
const refinements = [];
|
|
1867
|
+
const knownParams = [
|
|
1868
|
+
this.mapping.query,
|
|
1869
|
+
this.mapping.page,
|
|
1870
|
+
this.mapping.hitsPerPage,
|
|
1871
|
+
this.mapping.sortBy,
|
|
1872
|
+
];
|
|
1873
|
+
params.forEach((value, key) => {
|
|
1874
|
+
// Skip known params
|
|
1875
|
+
if (knownParams.includes(key))
|
|
1876
|
+
return;
|
|
1877
|
+
// Remove prefix if present
|
|
1878
|
+
const field = this.mapping.refinementPrefix
|
|
1879
|
+
? key.replace(new RegExp(`^${this.mapping.refinementPrefix}`), '')
|
|
1880
|
+
: key;
|
|
1881
|
+
refinements.push({ field, value });
|
|
1882
|
+
});
|
|
1883
|
+
return refinements;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Manually update URL from current state
|
|
1887
|
+
*/
|
|
1888
|
+
refresh() {
|
|
1889
|
+
if (this.writeToUrl) {
|
|
1890
|
+
this.writeStateToUrl(this.stateManager.getState());
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Get current URL
|
|
1895
|
+
*/
|
|
1896
|
+
getUrl() {
|
|
1897
|
+
if (typeof window === 'undefined')
|
|
1898
|
+
return '';
|
|
1899
|
+
return window.location.href;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Generate shareable URL from state
|
|
1903
|
+
*/
|
|
1904
|
+
createUrl(state) {
|
|
1905
|
+
const currentState = this.stateManager.getState();
|
|
1906
|
+
const mergedState = state ? { ...currentState, ...state } : currentState;
|
|
1907
|
+
const params = new URLSearchParams();
|
|
1908
|
+
if (mergedState.query) {
|
|
1909
|
+
params.set(this.mapping.query, mergedState.query);
|
|
1910
|
+
}
|
|
1911
|
+
if (mergedState.currentPage > 1) {
|
|
1912
|
+
params.set(this.mapping.page, mergedState.currentPage.toString());
|
|
1913
|
+
}
|
|
1914
|
+
if (mergedState.itemsPerPage !== 10) {
|
|
1915
|
+
params.set(this.mapping.hitsPerPage, mergedState.itemsPerPage.toString());
|
|
1916
|
+
}
|
|
1917
|
+
if (mergedState.sortBy && mergedState.sortBy !== 'relevance') {
|
|
1918
|
+
params.set(this.mapping.sortBy, mergedState.sortBy);
|
|
1919
|
+
}
|
|
1920
|
+
this.serializeRefinements(mergedState.refinements, params);
|
|
1921
|
+
const queryString = params.toString();
|
|
1922
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
1923
|
+
return queryString ? `${origin}${this.basePath}?${queryString}` : `${origin}${this.basePath}`;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Destroy the router and cleanup
|
|
1927
|
+
*/
|
|
1928
|
+
destroy() {
|
|
1929
|
+
if (this.unsubscribe) {
|
|
1930
|
+
this.unsubscribe();
|
|
1931
|
+
this.unsubscribe = null;
|
|
1932
|
+
}
|
|
1933
|
+
if (this.debounceTimer) {
|
|
1934
|
+
clearTimeout(this.debounceTimer);
|
|
1935
|
+
this.debounceTimer = null;
|
|
1936
|
+
}
|
|
1937
|
+
if (typeof window !== 'undefined') {
|
|
1938
|
+
window.removeEventListener('popstate', this.handlePopState);
|
|
1939
|
+
}
|
|
1940
|
+
log.info('URLRouter: Destroyed');
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Create a URL router instance
|
|
1945
|
+
*/
|
|
1946
|
+
function createURLRouter(config) {
|
|
1947
|
+
return new URLRouter(config);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
exports.SearchStateManager = SearchStateManager;
|
|
1951
|
+
exports.URLRouter = URLRouter;
|
|
1952
|
+
exports.announce = announce;
|
|
1953
|
+
exports.announceFilterChange = announceFilterChange;
|
|
1954
|
+
exports.announceResults = announceResults;
|
|
1955
|
+
exports.createFocusTrap = createFocusTrap;
|
|
1956
|
+
exports.createKeyboardShortcuts = createKeyboardShortcuts;
|
|
1957
|
+
exports.createLogger = createLogger;
|
|
1958
|
+
exports.createRovingTabIndex = createRovingTabIndex;
|
|
1959
|
+
exports.createTheme = createTheme;
|
|
1960
|
+
exports.createURLRouter = createURLRouter;
|
|
1961
|
+
exports.darkThemeVariables = darkThemeVariables;
|
|
1962
|
+
exports.extractField = extractField;
|
|
1963
|
+
exports.focusFirstError = focusFirstError;
|
|
1964
|
+
exports.formatPrice = formatPrice;
|
|
1965
|
+
exports.generateCompleteStylesheet = generateCompleteStylesheet;
|
|
1966
|
+
exports.generateId = generateId;
|
|
1967
|
+
exports.generateThemeStylesheet = generateThemeStylesheet;
|
|
1968
|
+
exports.getFocusableElements = getFocusableElements;
|
|
1969
|
+
exports.getHighlightedValue = getHighlightedValue;
|
|
1970
|
+
exports.getLogger = getLogger;
|
|
1971
|
+
exports.getNestedValue = getNestedValue$1;
|
|
1972
|
+
exports.getSnippetedValue = getSnippetedValue;
|
|
1973
|
+
exports.getTheme = getTheme;
|
|
1974
|
+
exports.handleKeyboardNavigation = handleKeyboardNavigation;
|
|
1975
|
+
exports.highContrastThemeVariables = highContrastThemeVariables;
|
|
1976
|
+
exports.highlightQuery = highlightQuery;
|
|
1977
|
+
exports.injectStyles = injectStyles;
|
|
1978
|
+
exports.lightThemeVariables = lightThemeVariables;
|
|
1979
|
+
exports.log = log;
|
|
1980
|
+
exports.mergeThemes = mergeThemes;
|
|
1981
|
+
exports.minimalThemeVariables = minimalThemeVariables;
|
|
1982
|
+
exports.parseHighlightedParts = parseHighlightedParts;
|
|
1983
|
+
exports.parseQueryHighlightParts = parseQueryHighlightParts;
|
|
1984
|
+
exports.prefersHighContrast = prefersHighContrast;
|
|
1985
|
+
exports.prefersReducedMotion = prefersReducedMotion;
|
|
1986
|
+
exports.setAriaDescribedBy = setAriaDescribedBy;
|
|
1987
|
+
exports.setDefaultLogger = setDefaultLogger;
|
|
1988
|
+
exports.setLogLevel = setLogLevel;
|
|
1989
|
+
exports.setLogPrefix = setLogPrefix;
|
|
1990
|
+
exports.setLogTimestamp = setLogTimestamp;
|
|
1991
|
+
exports.setTheme = setTheme;
|
|
1992
|
+
exports.stripHighlightTags = stripHighlightTags;
|
|
1993
|
+
exports.themeToCSSVariables = themeToCSSVariables;
|
|
1994
|
+
//# sourceMappingURL=index.js.map
|