@seekora-ai/ui-sdk-react 0.2.24 → 0.2.26
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/Facets.d.ts +2 -0
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +16 -3
- package/dist/components/SearchLayout.js +1 -1
- package/dist/hooks/useFilters.d.ts.map +1 -1
- package/dist/hooks/useFilters.js +23 -3
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.esm.js +99 -53
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +99 -53
- package/dist/src/index.js.map +1 -1
- package/dist/utils/responsive.d.ts.map +1 -1
- package/dist/utils/responsive.js +4 -10
- package/package.json +3 -3
package/dist/src/index.js
CHANGED
|
@@ -304,6 +304,8 @@ class SearchStateManager {
|
|
|
304
304
|
this.listeners = [];
|
|
305
305
|
this.debounceTimer = null;
|
|
306
306
|
this.notifyScheduled = false;
|
|
307
|
+
this.searchCoalesceTimer = null;
|
|
308
|
+
this.searchCoalesceResolvers = [];
|
|
307
309
|
this.client = config.client;
|
|
308
310
|
this.autoSearch = config.autoSearch !== false;
|
|
309
311
|
this.debounceMs = config.debounceMs || 300;
|
|
@@ -448,13 +450,27 @@ class SearchStateManager {
|
|
|
448
450
|
this.debouncedSearch();
|
|
449
451
|
}
|
|
450
452
|
}
|
|
451
|
-
// Manual search trigger
|
|
453
|
+
// Manual search trigger — coalesces rapid calls within 10ms into a single API request
|
|
452
454
|
async search(additionalOptions) {
|
|
453
455
|
// Clear debounce timer if exists
|
|
454
456
|
if (this.debounceTimer) {
|
|
455
457
|
clearTimeout(this.debounceTimer);
|
|
456
458
|
this.debounceTimer = null;
|
|
457
459
|
}
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
this.searchCoalesceResolvers.push({ resolve, reject });
|
|
462
|
+
if (this.searchCoalesceTimer) {
|
|
463
|
+
clearTimeout(this.searchCoalesceTimer);
|
|
464
|
+
}
|
|
465
|
+
this.searchCoalesceTimer = setTimeout(() => {
|
|
466
|
+
this.searchCoalesceTimer = null;
|
|
467
|
+
const resolvers = [...this.searchCoalesceResolvers];
|
|
468
|
+
this.searchCoalesceResolvers = [];
|
|
469
|
+
this._executeSearch(additionalOptions).then((result) => resolvers.forEach(r => r.resolve(result)), (err) => resolvers.forEach(r => r.reject(err)));
|
|
470
|
+
}, 10);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async _executeSearch(additionalOptions) {
|
|
458
474
|
this.setState({ loading: true, error: null });
|
|
459
475
|
try {
|
|
460
476
|
const searchOptions = this.buildSearchOptions(additionalOptions);
|
|
@@ -2695,11 +2711,8 @@ function getCurrentBreakpoint(width) {
|
|
|
2695
2711
|
* Hook to get current breakpoint
|
|
2696
2712
|
*/
|
|
2697
2713
|
function useBreakpoint() {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
return 'lg';
|
|
2701
|
-
return getCurrentBreakpoint(window.innerWidth);
|
|
2702
|
-
});
|
|
2714
|
+
// Always start with 'lg' to avoid hydration mismatch between server and client
|
|
2715
|
+
const [breakpoint, setBreakpoint] = React.useState('lg');
|
|
2703
2716
|
React.useEffect(() => {
|
|
2704
2717
|
const handleResize = () => {
|
|
2705
2718
|
setBreakpoint(getCurrentBreakpoint(window.innerWidth));
|
|
@@ -2714,11 +2727,8 @@ function useBreakpoint() {
|
|
|
2714
2727
|
* Hook to check if current viewport matches breakpoint
|
|
2715
2728
|
*/
|
|
2716
2729
|
function useMediaQuery(query) {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
return false;
|
|
2720
|
-
return window.matchMedia(query).matches;
|
|
2721
|
-
});
|
|
2730
|
+
// Always start with false to avoid hydration mismatch between server and client
|
|
2731
|
+
const [matches, setMatches] = React.useState(false);
|
|
2722
2732
|
React.useEffect(() => {
|
|
2723
2733
|
const mediaQuery = window.matchMedia(query);
|
|
2724
2734
|
const handleChange = () => setMatches(mediaQuery.matches);
|
|
@@ -3514,15 +3524,22 @@ const useFilters = (options) => {
|
|
|
3514
3524
|
const [error, setError] = React.useState(null);
|
|
3515
3525
|
const mountedRef = React.useRef(true);
|
|
3516
3526
|
const autoFetch = options?.autoFetch !== false;
|
|
3527
|
+
// Track whether we've completed the first fetch (to avoid skeleton flash on refetches)
|
|
3528
|
+
const hasDataRef = React.useRef(false);
|
|
3517
3529
|
// Extract non-autoFetch options to pass to fetchFilters
|
|
3518
3530
|
const fetchFilters = React.useCallback(async () => {
|
|
3519
|
-
|
|
3531
|
+
// Only show loading spinner on the very first fetch; subsequent refetches
|
|
3532
|
+
// keep previous data visible to avoid skeleton/flash on facet changes.
|
|
3533
|
+
if (!hasDataRef.current) {
|
|
3534
|
+
setLoading(true);
|
|
3535
|
+
}
|
|
3520
3536
|
setError(null);
|
|
3521
3537
|
try {
|
|
3522
3538
|
const { autoFetch: _, ...filterOptions } = options || {};
|
|
3523
3539
|
const response = await stateManager.fetchFilters(filterOptions);
|
|
3524
3540
|
if (mountedRef.current) {
|
|
3525
3541
|
setFilters(response?.filters || []);
|
|
3542
|
+
hasDataRef.current = true;
|
|
3526
3543
|
setLoading(false);
|
|
3527
3544
|
}
|
|
3528
3545
|
}
|
|
@@ -3533,13 +3550,26 @@ const useFilters = (options) => {
|
|
|
3533
3550
|
}
|
|
3534
3551
|
}
|
|
3535
3552
|
}, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
|
|
3536
|
-
//
|
|
3553
|
+
// Track query to only refetch filters when query actually changes.
|
|
3554
|
+
// Sentinel ensures the first subscribe callback always triggers a fetch.
|
|
3555
|
+
const prevKeyRef = React.useRef(null);
|
|
3556
|
+
// Refetch when query or refinements change (not on every state update)
|
|
3537
3557
|
React.useEffect(() => {
|
|
3538
3558
|
if (!autoFetch)
|
|
3539
3559
|
return;
|
|
3540
|
-
const unsubscribe = stateManager.subscribe((
|
|
3560
|
+
const unsubscribe = stateManager.subscribe((state) => {
|
|
3561
|
+
// Only track query changes — refinements are NOT passed to the Filters API
|
|
3562
|
+
// (facets are generated from search query only, not narrowed by active filters).
|
|
3563
|
+
// This prevents redundant filters refetches on every facet toggle.
|
|
3564
|
+
const key = state.query;
|
|
3565
|
+
if (key === prevKeyRef.current)
|
|
3566
|
+
return;
|
|
3567
|
+
prevKeyRef.current = key;
|
|
3541
3568
|
fetchFilters();
|
|
3542
3569
|
});
|
|
3570
|
+
// subscribe() immediately invokes the listener with current state,
|
|
3571
|
+
// which handles the initial fetch (prevKeyRef starts as '' so key
|
|
3572
|
+
// will differ). No explicit fetchFilters() call needed here.
|
|
3543
3573
|
return unsubscribe;
|
|
3544
3574
|
}, [stateManager, autoFetch, fetchFilters]);
|
|
3545
3575
|
// Fetch schema once on mount
|
|
@@ -3881,7 +3911,7 @@ const CSS_VAR_DEFAULTS = {
|
|
|
3881
3911
|
// ---------------------------------------------------------------------------
|
|
3882
3912
|
// Component
|
|
3883
3913
|
// ---------------------------------------------------------------------------
|
|
3884
|
-
const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, variant = 'checkbox', searchable = false, showCounts = true, colorMap, defaultCollapsed = false, size = 'medium', facetRanges, useFiltersApi = false, disjunctiveFacets, hideEmptyFacets = true, defaultCollapsedFields, }) => {
|
|
3914
|
+
const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, variant = 'checkbox', searchable = false, showCounts = true, colorMap, defaultCollapsed = false, size = 'medium', facetRanges, useFiltersApi = false, disjunctiveFacets, hideEmptyFacets = true, defaultCollapsedFields, onFacetsAvailable, }) => {
|
|
3885
3915
|
const { theme } = useSearchContext();
|
|
3886
3916
|
const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
|
|
3887
3917
|
const facetsTheme = customTheme || {};
|
|
@@ -3959,9 +3989,22 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
3959
3989
|
return extracted;
|
|
3960
3990
|
};
|
|
3961
3991
|
const rawFacetList = extractFacets();
|
|
3992
|
+
const hasStats = (stats) => stats != null && (stats.min != null || stats.max != null);
|
|
3962
3993
|
const facets = hideEmptyFacets
|
|
3963
|
-
? rawFacetList.filter(f => f.items.length > 0 || f.stats
|
|
3994
|
+
? rawFacetList.filter(f => f.items.length > 0 || hasStats(f.stats))
|
|
3964
3995
|
: rawFacetList;
|
|
3996
|
+
// Notify parent about facet availability
|
|
3997
|
+
const facetCount = facets.length;
|
|
3998
|
+
const prevFacetAvailableRef = React.useRef(null);
|
|
3999
|
+
React.useEffect(() => {
|
|
4000
|
+
if (!onFacetsAvailable)
|
|
4001
|
+
return;
|
|
4002
|
+
const available = facetCount > 0;
|
|
4003
|
+
if (prevFacetAvailableRef.current !== available) {
|
|
4004
|
+
prevFacetAvailableRef.current = available;
|
|
4005
|
+
onFacetsAvailable(available);
|
|
4006
|
+
}
|
|
4007
|
+
}, [facetCount, onFacetsAvailable]);
|
|
3965
4008
|
// -------------------------------------------------------------------
|
|
3966
4009
|
// Handlers
|
|
3967
4010
|
// -------------------------------------------------------------------
|
|
@@ -5201,9 +5244,6 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5201
5244
|
opacity: 0.6,
|
|
5202
5245
|
}, "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())));
|
|
5203
5246
|
};
|
|
5204
|
-
if (refinements.length === 0) {
|
|
5205
|
-
return null;
|
|
5206
|
-
}
|
|
5207
5247
|
// Group refinements by field for grouped layout
|
|
5208
5248
|
const groupedRefinements = layout === 'grouped'
|
|
5209
5249
|
? refinements.reduce((acc, r) => {
|
|
@@ -5215,13 +5255,17 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5215
5255
|
: null;
|
|
5216
5256
|
const containerStyles = {
|
|
5217
5257
|
...style,
|
|
5258
|
+
// When a custom list theme is provided, make the container transparent to layout
|
|
5259
|
+
// so the list div becomes the direct layout child (enables overflow scroll from parent)
|
|
5260
|
+
...(refinementsTheme.list && !style?.display ? { display: 'contents' } : {}),
|
|
5218
5261
|
};
|
|
5219
5262
|
const listStyles = {
|
|
5220
5263
|
display: 'flex',
|
|
5221
|
-
flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
|
|
5222
5264
|
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
5223
5265
|
alignItems: layout === 'vertical' ? 'flex-start' : 'center',
|
|
5224
|
-
marginBottom: showClearAll ? theme.spacing.medium : 0,
|
|
5266
|
+
marginBottom: showClearAll && refinements.length > 0 ? theme.spacing.medium : 0,
|
|
5267
|
+
// Only apply flex-wrap if no custom list theme is provided (let theme classes control wrapping)
|
|
5268
|
+
...(!refinementsTheme.list ? { flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap' } : {}),
|
|
5225
5269
|
};
|
|
5226
5270
|
return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
|
|
5227
5271
|
React.createElement("style", null, `
|
|
@@ -5230,34 +5274,35 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
|
|
|
5230
5274
|
to { opacity: 1; transform: scale(1); }
|
|
5231
5275
|
}
|
|
5232
5276
|
`),
|
|
5233
|
-
|
|
5234
|
-
React.createElement("div", { className: refinementsTheme.
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5277
|
+
refinements.length > 0 && (React.createElement(React.Fragment, null,
|
|
5278
|
+
layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
|
|
5279
|
+
React.createElement("div", { className: refinementsTheme.groupLabel, style: {
|
|
5280
|
+
fontSize: theme.typography.fontSize.small,
|
|
5281
|
+
fontWeight: 600,
|
|
5282
|
+
color: theme.colors.text,
|
|
5283
|
+
marginBottom: theme.spacing.small,
|
|
5284
|
+
textTransform: 'capitalize',
|
|
5285
|
+
} }, items[0]?.label || field),
|
|
5286
|
+
React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
|
|
5287
|
+
return renderRefinement
|
|
5288
|
+
? renderRefinement(refinement, index)
|
|
5289
|
+
: defaultRenderRefinement(refinement, index);
|
|
5290
|
+
})))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
|
|
5242
5291
|
return renderRefinement
|
|
5243
5292
|
? renderRefinement(refinement, index)
|
|
5244
5293
|
: defaultRenderRefinement(refinement, index);
|
|
5245
|
-
})))
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
fontSize: theme.typography.fontSize.small,
|
|
5258
|
-
textDecoration: 'underline',
|
|
5259
|
-
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5260
|
-
} }, "Clear all filters"))));
|
|
5294
|
+
}))),
|
|
5295
|
+
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
5296
|
+
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5297
|
+
border: `1px solid ${theme.colors.border}`,
|
|
5298
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
5299
|
+
backgroundColor: theme.colors.background,
|
|
5300
|
+
color: theme.colors.text,
|
|
5301
|
+
cursor: 'pointer',
|
|
5302
|
+
fontSize: theme.typography.fontSize.small,
|
|
5303
|
+
textDecoration: 'underline',
|
|
5304
|
+
transition: `background-color ${TRANSITIONS$7.fast}`,
|
|
5305
|
+
} }, "Clear all filters"))))));
|
|
5261
5306
|
};
|
|
5262
5307
|
|
|
5263
5308
|
/**
|
|
@@ -5325,16 +5370,17 @@ const ClearRefinements = ({ clearsQuery = false, resetLabel = 'Clear all filters
|
|
|
5325
5370
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
5326
5371
|
fontSize: theme.typography.fontSize.medium,
|
|
5327
5372
|
fontWeight: theme.typography.fontWeight?.medium || 500,
|
|
5328
|
-
backgroundColor:
|
|
5329
|
-
color:
|
|
5373
|
+
backgroundColor: theme.colors.primary,
|
|
5374
|
+
color: '#ffffff',
|
|
5330
5375
|
border: 'none',
|
|
5331
5376
|
borderRadius: typeof theme.borderRadius === 'string'
|
|
5332
5377
|
? theme.borderRadius
|
|
5333
5378
|
: theme.borderRadius.medium,
|
|
5334
|
-
cursor: canClear ? 'pointer' : '
|
|
5335
|
-
opacity: canClear ? 1 : 0
|
|
5336
|
-
|
|
5337
|
-
|
|
5379
|
+
cursor: canClear ? 'pointer' : 'default',
|
|
5380
|
+
opacity: canClear ? 1 : 0,
|
|
5381
|
+
pointerEvents: canClear ? 'auto' : 'none',
|
|
5382
|
+
transition: 'none',
|
|
5383
|
+
}, "aria-label": resetLabel }, resetLabel)));
|
|
5338
5384
|
};
|
|
5339
5385
|
|
|
5340
5386
|
/**
|
|
@@ -5372,7 +5418,7 @@ const SearchLayout = ({ sidebar, children, header, footer, sidebarWidth = '300px
|
|
|
5372
5418
|
padding: responsivePadding,
|
|
5373
5419
|
backgroundColor: theme.colors.background,
|
|
5374
5420
|
color: theme.colors.text,
|
|
5375
|
-
overflow:
|
|
5421
|
+
overflow: 'visible',
|
|
5376
5422
|
} },
|
|
5377
5423
|
sidebar && (!isMobile || showSidebarOnMobile) && (React.createElement("aside", { className: layoutTheme.sidebar, style: {
|
|
5378
5424
|
width: isMobile ? '100%' : sidebarWidth,
|