@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.
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);
@@ -3533,13 +3549,22 @@ const useFilters = (options) => {
3533
3549
  }
3534
3550
  }
3535
3551
  }, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
3536
- // Refetch when query or refinements change
3552
+ // Track query + refinements to only refetch when they actually change
3553
+ const prevKeyRef = React.useRef('');
3554
+ // Refetch when query or refinements change (not on every state update)
3537
3555
  React.useEffect(() => {
3538
3556
  if (!autoFetch)
3539
3557
  return;
3540
- const unsubscribe = stateManager.subscribe((_state) => {
3558
+ const unsubscribe = stateManager.subscribe((state) => {
3559
+ const key = `${state.query}|${state.refinements.map(r => `${r.field}:${r.value}`).sort().join(',')}`;
3560
+ if (key === prevKeyRef.current)
3561
+ return;
3562
+ prevKeyRef.current = key;
3541
3563
  fetchFilters();
3542
3564
  });
3565
+ // subscribe() immediately invokes the listener with current state,
3566
+ // which handles the initial fetch (prevKeyRef starts as '' so key
3567
+ // will differ). No explicit fetchFilters() call needed here.
3543
3568
  return unsubscribe;
3544
3569
  }, [stateManager, autoFetch, fetchFilters]);
3545
3570
  // Fetch schema once on mount
@@ -5201,9 +5226,6 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5201
5226
  opacity: 0.6,
5202
5227
  }, "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
5228
  };
5204
- if (refinements.length === 0) {
5205
- return null;
5206
- }
5207
5229
  // Group refinements by field for grouped layout
5208
5230
  const groupedRefinements = layout === 'grouped'
5209
5231
  ? refinements.reduce((acc, r) => {
@@ -5215,13 +5237,17 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5215
5237
  : null;
5216
5238
  const containerStyles = {
5217
5239
  ...style,
5240
+ // When a custom list theme is provided, make the container transparent to layout
5241
+ // so the list div becomes the direct layout child (enables overflow scroll from parent)
5242
+ ...(refinementsTheme.list && !style?.display ? { display: 'contents' } : {}),
5218
5243
  };
5219
5244
  const listStyles = {
5220
5245
  display: 'flex',
5221
- flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
5222
5246
  flexDirection: layout === 'vertical' ? 'column' : 'row',
5223
5247
  alignItems: layout === 'vertical' ? 'flex-start' : 'center',
5224
- marginBottom: showClearAll ? theme.spacing.medium : 0,
5248
+ marginBottom: showClearAll && refinements.length > 0 ? theme.spacing.medium : 0,
5249
+ // Only apply flex-wrap if no custom list theme is provided (let theme classes control wrapping)
5250
+ ...(!refinementsTheme.list ? { flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap' } : {}),
5225
5251
  };
5226
5252
  return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
5227
5253
  React.createElement("style", null, `
@@ -5230,34 +5256,35 @@ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, o
5230
5256
  to { opacity: 1; transform: scale(1); }
5231
5257
  }
5232
5258
  `),
5233
- layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
5234
- React.createElement("div", { className: refinementsTheme.groupLabel, style: {
5235
- fontSize: theme.typography.fontSize.small,
5236
- fontWeight: 600,
5237
- color: theme.colors.text,
5238
- marginBottom: theme.spacing.small,
5239
- textTransform: 'capitalize',
5240
- } }, items[0]?.label || field),
5241
- React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
5259
+ refinements.length > 0 && (React.createElement(React.Fragment, null,
5260
+ layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
5261
+ React.createElement("div", { className: refinementsTheme.groupLabel, style: {
5262
+ fontSize: theme.typography.fontSize.small,
5263
+ fontWeight: 600,
5264
+ color: theme.colors.text,
5265
+ marginBottom: theme.spacing.small,
5266
+ textTransform: 'capitalize',
5267
+ } }, items[0]?.label || field),
5268
+ React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
5269
+ return renderRefinement
5270
+ ? renderRefinement(refinement, index)
5271
+ : defaultRenderRefinement(refinement, index);
5272
+ })))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
5242
5273
  return renderRefinement
5243
5274
  ? renderRefinement(refinement, index)
5244
5275
  : defaultRenderRefinement(refinement, index);
5245
- })))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
5246
- return renderRefinement
5247
- ? renderRefinement(refinement, index)
5248
- : defaultRenderRefinement(refinement, index);
5249
- }))),
5250
- showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
5251
- padding: `${theme.spacing.small} ${theme.spacing.medium}`,
5252
- border: `1px solid ${theme.colors.border}`,
5253
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
5254
- backgroundColor: theme.colors.background,
5255
- color: theme.colors.text,
5256
- cursor: 'pointer',
5257
- fontSize: theme.typography.fontSize.small,
5258
- textDecoration: 'underline',
5259
- transition: `background-color ${TRANSITIONS$7.fast}`,
5260
- } }, "Clear all filters"))));
5276
+ }))),
5277
+ showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
5278
+ padding: `${theme.spacing.small} ${theme.spacing.medium}`,
5279
+ border: `1px solid ${theme.colors.border}`,
5280
+ borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
5281
+ backgroundColor: theme.colors.background,
5282
+ color: theme.colors.text,
5283
+ cursor: 'pointer',
5284
+ fontSize: theme.typography.fontSize.small,
5285
+ textDecoration: 'underline',
5286
+ transition: `background-color ${TRANSITIONS$7.fast}`,
5287
+ } }, "Clear all filters"))))));
5261
5288
  };
5262
5289
 
5263
5290
  /**
@@ -5325,16 +5352,17 @@ const ClearRefinements = ({ clearsQuery = false, resetLabel = 'Clear all filters
5325
5352
  padding: `${theme.spacing.small} ${theme.spacing.medium}`,
5326
5353
  fontSize: theme.typography.fontSize.medium,
5327
5354
  fontWeight: theme.typography.fontWeight?.medium || 500,
5328
- backgroundColor: canClear ? theme.colors.primary : theme.colors.hover,
5329
- color: canClear ? '#ffffff' : theme.colors.textSecondary,
5355
+ backgroundColor: theme.colors.primary,
5356
+ color: '#ffffff',
5330
5357
  border: 'none',
5331
5358
  borderRadius: typeof theme.borderRadius === 'string'
5332
5359
  ? theme.borderRadius
5333
5360
  : theme.borderRadius.medium,
5334
- cursor: canClear ? 'pointer' : 'not-allowed',
5335
- opacity: canClear ? 1 : 0.6,
5336
- transition: theme.transitions?.fast || '150ms ease-in-out',
5337
- }, "aria-label": canClear ? resetLabel : disabledLabel }, canClear ? resetLabel : disabledLabel)));
5361
+ cursor: canClear ? 'pointer' : 'default',
5362
+ opacity: canClear ? 1 : 0,
5363
+ pointerEvents: canClear ? 'auto' : 'none',
5364
+ transition: 'none',
5365
+ }, "aria-label": resetLabel }, resetLabel)));
5338
5366
  };
5339
5367
 
5340
5368
  /**
@@ -5372,7 +5400,7 @@ const SearchLayout = ({ sidebar, children, header, footer, sidebarWidth = '300px
5372
5400
  padding: responsivePadding,
5373
5401
  backgroundColor: theme.colors.background,
5374
5402
  color: theme.colors.text,
5375
- overflow: isMobile ? 'visible' : 'hidden',
5403
+ overflow: 'visible',
5376
5404
  } },
5377
5405
  sidebar && (!isMobile || showSidebarOnMobile) && (React.createElement("aside", { className: layoutTheme.sidebar, style: {
5378
5406
  width: isMobile ? '100%' : sidebarWidth,