@seekora-ai/ui-sdk-react 0.2.23 → 0.2.25
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/ClearRefinements.d.ts.map +1 -1
- package/dist/components/ClearRefinements.js +7 -6
- package/dist/components/CurrentRefinements.d.ts.map +1 -1
- package/dist/components/CurrentRefinements.js +32 -30
- package/dist/components/SearchLayout.js +1 -1
- package/dist/hooks/useFilters.d.ts.map +1 -1
- package/dist/hooks/useFilters.js +11 -2
- package/dist/index.umd.js +1 -1
- package/dist/src/index.esm.js +94 -54
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +94 -54
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/index.esm.js
CHANGED
|
@@ -301,6 +301,9 @@ class SearchStateManager {
|
|
|
301
301
|
constructor(config) {
|
|
302
302
|
this.listeners = [];
|
|
303
303
|
this.debounceTimer = null;
|
|
304
|
+
this.notifyScheduled = false;
|
|
305
|
+
this.searchCoalesceTimer = null;
|
|
306
|
+
this.searchCoalesceResolvers = [];
|
|
304
307
|
this.client = config.client;
|
|
305
308
|
this.autoSearch = config.autoSearch !== false;
|
|
306
309
|
this.debounceMs = config.debounceMs || 300;
|
|
@@ -445,13 +448,27 @@ class SearchStateManager {
|
|
|
445
448
|
this.debouncedSearch();
|
|
446
449
|
}
|
|
447
450
|
}
|
|
448
|
-
// Manual search trigger
|
|
451
|
+
// Manual search trigger — coalesces rapid calls within 10ms into a single API request
|
|
449
452
|
async search(additionalOptions) {
|
|
450
453
|
// Clear debounce timer if exists
|
|
451
454
|
if (this.debounceTimer) {
|
|
452
455
|
clearTimeout(this.debounceTimer);
|
|
453
456
|
this.debounceTimer = null;
|
|
454
457
|
}
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
this.searchCoalesceResolvers.push({ resolve, reject });
|
|
460
|
+
if (this.searchCoalesceTimer) {
|
|
461
|
+
clearTimeout(this.searchCoalesceTimer);
|
|
462
|
+
}
|
|
463
|
+
this.searchCoalesceTimer = setTimeout(() => {
|
|
464
|
+
this.searchCoalesceTimer = null;
|
|
465
|
+
const resolvers = [...this.searchCoalesceResolvers];
|
|
466
|
+
this.searchCoalesceResolvers = [];
|
|
467
|
+
this._executeSearch(additionalOptions).then((result) => resolvers.forEach(r => r.resolve(result)), (err) => resolvers.forEach(r => r.reject(err)));
|
|
468
|
+
}, 10);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async _executeSearch(additionalOptions) {
|
|
455
472
|
this.setState({ loading: true, error: null });
|
|
456
473
|
try {
|
|
457
474
|
const searchOptions = this.buildSearchOptions(additionalOptions);
|
|
@@ -548,19 +565,27 @@ class SearchStateManager {
|
|
|
548
565
|
this.state = { ...this.state, ...updates };
|
|
549
566
|
this.notifyListeners();
|
|
550
567
|
}
|
|
551
|
-
// Notify all listeners of state changes
|
|
568
|
+
// Notify all listeners of state changes, batched via microtask.
|
|
569
|
+
// Multiple synchronous mutations (e.g. addRefinement + page reset)
|
|
570
|
+
// coalesce into a single listener notification.
|
|
552
571
|
notifyListeners() {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
572
|
+
if (this.notifyScheduled)
|
|
573
|
+
return;
|
|
574
|
+
this.notifyScheduled = true;
|
|
575
|
+
queueMicrotask(() => {
|
|
576
|
+
this.notifyScheduled = false;
|
|
577
|
+
const state = this.getState();
|
|
578
|
+
this.listeners.forEach(listener => {
|
|
579
|
+
try {
|
|
580
|
+
listener(state);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
584
|
+
log.error('SearchStateManager: Error in listener', {
|
|
585
|
+
error: error.message,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
});
|
|
564
589
|
});
|
|
565
590
|
}
|
|
566
591
|
/** Explicitly clear results (bypasses keepResultsOnClear) */
|
|
@@ -606,10 +631,13 @@ class SearchStateManager {
|
|
|
606
631
|
async fetchFilters(options) {
|
|
607
632
|
log.verbose('SearchStateManager: Fetching filters', { options });
|
|
608
633
|
try {
|
|
609
|
-
|
|
634
|
+
// Do NOT pass refinement-based filters to the Filters API.
|
|
635
|
+
// Facets should be generated from the search query only, not narrowed
|
|
636
|
+
// by active filter selections. This keeps facet options stable when
|
|
637
|
+
// users toggle filters (same behaviour as performSimplifiedFacetSearch
|
|
638
|
+
// in the search API).
|
|
610
639
|
const response = await this.client.getFilters({
|
|
611
640
|
q: this.state.query || undefined,
|
|
612
|
-
filter: filterString || undefined,
|
|
613
641
|
...options,
|
|
614
642
|
});
|
|
615
643
|
log.info('SearchStateManager: Filters fetched', {
|
|
@@ -3519,13 +3547,22 @@ const useFilters = (options) => {
|
|
|
3519
3547
|
}
|
|
3520
3548
|
}
|
|
3521
3549
|
}, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
|
|
3522
|
-
//
|
|
3550
|
+
// Track query + refinements to only refetch when they actually change
|
|
3551
|
+
const prevKeyRef = useRef('');
|
|
3552
|
+
// Refetch when query or refinements change (not on every state update)
|
|
3523
3553
|
useEffect(() => {
|
|
3524
3554
|
if (!autoFetch)
|
|
3525
3555
|
return;
|
|
3526
|
-
const unsubscribe = stateManager.subscribe((
|
|
3556
|
+
const unsubscribe = stateManager.subscribe((state) => {
|
|
3557
|
+
const key = `${state.query}|${state.refinements.map(r => `${r.field}:${r.value}`).sort().join(',')}`;
|
|
3558
|
+
if (key === prevKeyRef.current)
|
|
3559
|
+
return;
|
|
3560
|
+
prevKeyRef.current = key;
|
|
3527
3561
|
fetchFilters();
|
|
3528
3562
|
});
|
|
3563
|
+
// subscribe() immediately invokes the listener with current state,
|
|
3564
|
+
// which handles the initial fetch (prevKeyRef starts as '' so key
|
|
3565
|
+
// will differ). No explicit fetchFilters() call needed here.
|
|
3529
3566
|
return unsubscribe;
|
|
3530
3567
|
}, [stateManager, autoFetch, fetchFilters]);
|
|
3531
3568
|
// Fetch schema once on mount
|
|
@@ -5187,9 +5224,6 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5187
5224
|
opacity: 0.6,
|
|
5188
5225
|
}, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}`, onMouseEnter: e => (e.currentTarget.style.opacity = '1'), onMouseLeave: e => (e.currentTarget.style.opacity = '0.6') }, renderCloseIcon ? renderCloseIcon() : defaultCloseIcon())));
|
|
5189
5226
|
};
|
|
5190
|
-
if (refinements.length === 0) {
|
|
5191
|
-
return null;
|
|
5192
|
-
}
|
|
5193
5227
|
// Group refinements by field for grouped layout
|
|
5194
5228
|
const groupedRefinements = layout === 'grouped'
|
|
5195
5229
|
? refinements.reduce((acc, r) => {
|
|
@@ -5201,13 +5235,17 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5201
5235
|
: null;
|
|
5202
5236
|
const containerStyles = {
|
|
5203
5237
|
...style,
|
|
5238
|
+
// When a custom list theme is provided, make the container transparent to layout
|
|
5239
|
+
// so the list div becomes the direct layout child (enables overflow scroll from parent)
|
|
5240
|
+
...(refinementsTheme.list && !style?.display ? { display: 'contents' } : {}),
|
|
5204
5241
|
};
|
|
5205
5242
|
const listStyles = {
|
|
5206
5243
|
display: 'flex',
|
|
5207
|
-
flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
|
|
5208
5244
|
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
5209
5245
|
alignItems: layout === 'vertical' ? 'flex-start' : 'center',
|
|
5210
|
-
marginBottom: showClearAll ? theme.spacing.medium : 0,
|
|
5246
|
+
marginBottom: showClearAll && refinements.length > 0 ? theme.spacing.medium : 0,
|
|
5247
|
+
// Only apply flex-wrap if no custom list theme is provided (let theme classes control wrapping)
|
|
5248
|
+
...(!refinementsTheme.list ? { flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap' } : {}),
|
|
5211
5249
|
};
|
|
5212
5250
|
return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
|
|
5213
5251
|
React.createElement("style", null, `
|
|
@@ -5216,34 +5254,35 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5216
5254
|
to { opacity: 1; transform: scale(1); }
|
|
5217
5255
|
}
|
|
5218
5256
|
`),
|
|
5219
|
-
|
|
5220
|
-
React.createElement("div", { className: refinementsTheme.
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5257
|
+
refinements.length > 0 && (React.createElement(React.Fragment, null,
|
|
5258
|
+
layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
|
|
5259
|
+
React.createElement("div", { className: refinementsTheme.groupLabel, style: {
|
|
5260
|
+
fontSize: theme.typography.fontSize.small,
|
|
5261
|
+
fontWeight: 600,
|
|
5262
|
+
color: theme.colors.text,
|
|
5263
|
+
marginBottom: theme.spacing.small,
|
|
5264
|
+
textTransform: 'capitalize',
|
|
5265
|
+
} }, items[0]?.label || field),
|
|
5266
|
+
React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
|
|
5267
|
+
return renderRefinement
|
|
5268
|
+
? renderRefinement(refinement, index)
|
|
5269
|
+
: defaultRenderRefinement(refinement, index);
|
|
5270
|
+
})))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
|
|
5228
5271
|
return renderRefinement
|
|
5229
5272
|
? renderRefinement(refinement, index)
|
|
5230
5273
|
: defaultRenderRefinement(refinement, index);
|
|
5231
|
-
})))
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
fontSize: theme.typography.fontSize.small,
|
|
5244
|
-
textDecoration: 'underline',
|
|
5245
|
-
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5246
|
-
} }, "Clear all filters"))));
|
|
5274
|
+
}))),
|
|
5275
|
+
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
5276
|
+
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5277
|
+
border: `1px solid ${theme.colors.border}`,
|
|
5278
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
5279
|
+
backgroundColor: theme.colors.background,
|
|
5280
|
+
color: theme.colors.text,
|
|
5281
|
+
cursor: 'pointer',
|
|
5282
|
+
fontSize: theme.typography.fontSize.small,
|
|
5283
|
+
textDecoration: 'underline',
|
|
5284
|
+
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5285
|
+
} }, "Clear all filters"))))));
|
|
5247
5286
|
};
|
|
5248
5287
|
|
|
5249
5288
|
/**
|
|
@@ -5311,16 +5350,17 @@ const ClearRefinements = ({ clearsQuery = false, resetLabel = 'Clear all filters
|
|
|
5311
5350
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5312
5351
|
fontSize: theme.typography.fontSize.medium,
|
|
5313
5352
|
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
5314
|
-
backgroundColor:
|
|
5315
|
-
color:
|
|
5353
|
+
backgroundColor: theme.colors.primary,
|
|
5354
|
+
color: '#ffffff',
|
|
5316
5355
|
border: 'none',
|
|
5317
5356
|
borderRadius: typeof theme.borderRadius === 'string'
|
|
5318
5357
|
? theme.borderRadius
|
|
5319
5358
|
: theme.borderRadius.medium,
|
|
5320
|
-
cursor: canClear ? 'pointer' : '
|
|
5321
|
-
opacity: canClear ? 1 : 0
|
|
5322
|
-
|
|
5323
|
-
|
|
5359
|
+
cursor: canClear ? 'pointer' : 'default',
|
|
5360
|
+
opacity: canClear ? 1 : 0,
|
|
5361
|
+
pointerEvents: canClear ? 'auto' : 'none',
|
|
5362
|
+
transition: 'none',
|
|
5363
|
+
}, "aria-label": resetLabel }, resetLabel)));
|
|
5324
5364
|
};
|
|
5325
5365
|
|
|
5326
5366
|
/**
|
|
@@ -5358,7 +5398,7 @@ const SearchLayout = ({ sidebar, children, header, footer, sidebarWidth = '300px
|
|
|
5358
5398
|
padding: responsivePadding,
|
|
5359
5399
|
backgroundColor: theme.colors.background,
|
|
5360
5400
|
color: theme.colors.text,
|
|
5361
|
-
overflow:
|
|
5401
|
+
overflow: 'visible',
|
|
5362
5402
|
} },
|
|
5363
5403
|
sidebar && (!isMobile || showSidebarOnMobile) && (React.createElement("aside", { className: layoutTheme.sidebar, style: {
|
|
5364
5404
|
width: isMobile ? '100%' : sidebarWidth,
|