@seekora-ai/ui-sdk-react 0.2.24 → 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.
@@ -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);
@@ -3531,13 +3547,22 @@ const useFilters = (options) => {
3531
3547
  }
3532
3548
  }
3533
3549
  }, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
3534
- // Refetch when query or refinements change
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)
3535
3553
  useEffect(() => {
3536
3554
  if (!autoFetch)
3537
3555
  return;
3538
- const unsubscribe = stateManager.subscribe((_state) => {
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;
3539
3561
  fetchFilters();
3540
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.
3541
3566
  return unsubscribe;
3542
3567
  }, [stateManager, autoFetch, fetchFilters]);
3543
3568
  // Fetch schema once on mount
@@ -5199,9 +5224,6 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5199
5224
  opacity: 0.6,
5200
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())));
5201
5226
  };
5202
- if (refinements.length === 0) {
5203
- return null;
5204
- }
5205
5227
  // Group refinements by field for grouped layout
5206
5228
  const groupedRefinements = layout === 'grouped'
5207
5229
  ? refinements.reduce((acc, r) => {
@@ -5213,13 +5235,17 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5213
5235
  : null;
5214
5236
  const containerStyles = {
5215
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' } : {}),
5216
5241
  };
5217
5242
  const listStyles = {
5218
5243
  display: 'flex',
5219
- flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
5220
5244
  flexDirection: layout === 'vertical' ? 'column' : 'row',
5221
5245
  alignItems: layout === 'vertical' ? 'flex-start' : 'center',
5222
- 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' } : {}),
5223
5249
  };
5224
5250
  return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
5225
5251
  React.createElement("style", null, `
@@ -5228,34 +5254,35 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5228
5254
  to { opacity: 1; transform: scale(1); }
5229
5255
  }
5230
5256
  `),
5231
- layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
5232
- React.createElement("div", { className: refinementsTheme.groupLabel, style: {
5233
- fontSize: theme.typography.fontSize.small,
5234
- fontWeight: 600,
5235
- color: theme.colors.text,
5236
- marginBottom: theme.spacing.small,
5237
- textTransform: 'capitalize',
5238
- } }, items[0]?.label || field),
5239
- React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
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) => {
5240
5271
  return renderRefinement
5241
5272
  ? renderRefinement(refinement, index)
5242
5273
  : defaultRenderRefinement(refinement, index);
5243
- })))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
5244
- return renderRefinement
5245
- ? renderRefinement(refinement, index)
5246
- : defaultRenderRefinement(refinement, index);
5247
- }))),
5248
- showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
5249
- padding: `${theme.spacing.small} ${theme.spacing.medium}`,
5250
- border: `1px solid ${theme.colors.border}`,
5251
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
5252
- backgroundColor: theme.colors.background,
5253
- color: theme.colors.text,
5254
- cursor: 'pointer',
5255
- fontSize: theme.typography.fontSize.small,
5256
- textDecoration: 'underline',
5257
- transition: `background-color ${TRANSITIONS$7.fast}`,
5258
- } }, "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"))))));
5259
5286
  };
5260
5287
 
5261
5288
  /**
@@ -5323,16 +5350,17 @@ const ClearRefinements = ({ clearsQuery = false, resetLabel = 'Clear all filters
5323
5350
  padding: `${theme.spacing.small} ${theme.spacing.medium}`,
5324
5351
  fontSize: theme.typography.fontSize.medium,
5325
5352
  fontWeight: theme.typography.fontWeight?.medium || 500,
5326
- backgroundColor: canClear ? theme.colors.primary : theme.colors.hover,
5327
- color: canClear ? '#ffffff' : theme.colors.textSecondary,
5353
+ backgroundColor: theme.colors.primary,
5354
+ color: '#ffffff',
5328
5355
  border: 'none',
5329
5356
  borderRadius: typeof theme.borderRadius === 'string'
5330
5357
  ? theme.borderRadius
5331
5358
  : theme.borderRadius.medium,
5332
- cursor: canClear ? 'pointer' : 'not-allowed',
5333
- opacity: canClear ? 1 : 0.6,
5334
- transition: theme.transitions?.fast || '150ms ease-in-out',
5335
- }, "aria-label": canClear ? resetLabel : disabledLabel }, canClear ? resetLabel : disabledLabel)));
5359
+ cursor: canClear ? 'pointer' : 'default',
5360
+ opacity: canClear ? 1 : 0,
5361
+ pointerEvents: canClear ? 'auto' : 'none',
5362
+ transition: 'none',
5363
+ }, "aria-label": resetLabel }, resetLabel)));
5336
5364
  };
5337
5365
 
5338
5366
  /**
@@ -5370,7 +5398,7 @@ const SearchLayout = ({ sidebar, children, header, footer, sidebarWidth = '300px
5370
5398
  padding: responsivePadding,
5371
5399
  backgroundColor: theme.colors.background,
5372
5400
  color: theme.colors.text,
5373
- overflow: isMobile ? 'visible' : 'hidden',
5401
+ overflow: 'visible',
5374
5402
  } },
5375
5403
  sidebar && (!isMobile || showSidebarOnMobile) && (React.createElement("aside", { className: layoutTheme.sidebar, style: {
5376
5404
  width: isMobile ? '100%' : sidebarWidth,