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