@seekora-ai/ui-sdk-react 0.2.25 → 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.
@@ -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
 
@@ -2709,11 +2709,8 @@ function getCurrentBreakpoint(width) {
2709
2709
  * Hook to get current breakpoint
2710
2710
  */
2711
2711
  function useBreakpoint() {
2712
- const [breakpoint, setBreakpoint] = useState(() => {
2713
- if (typeof window === 'undefined')
2714
- return 'lg';
2715
- return getCurrentBreakpoint(window.innerWidth);
2716
- });
2712
+ // Always start with 'lg' to avoid hydration mismatch between server and client
2713
+ const [breakpoint, setBreakpoint] = useState('lg');
2717
2714
  useEffect(() => {
2718
2715
  const handleResize = () => {
2719
2716
  setBreakpoint(getCurrentBreakpoint(window.innerWidth));
@@ -2728,11 +2725,8 @@ function useBreakpoint() {
2728
2725
  * Hook to check if current viewport matches breakpoint
2729
2726
  */
2730
2727
  function useMediaQuery(query) {
2731
- const [matches, setMatches] = useState(() => {
2732
- if (typeof window === 'undefined')
2733
- return false;
2734
- return window.matchMedia(query).matches;
2735
- });
2728
+ // Always start with false to avoid hydration mismatch between server and client
2729
+ const [matches, setMatches] = useState(false);
2736
2730
  useEffect(() => {
2737
2731
  const mediaQuery = window.matchMedia(query);
2738
2732
  const handleChange = () => setMatches(mediaQuery.matches);
@@ -3528,15 +3522,22 @@ const useFilters = (options) => {
3528
3522
  const [error, setError] = useState(null);
3529
3523
  const mountedRef = useRef(true);
3530
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);
3531
3527
  // Extract non-autoFetch options to pass to fetchFilters
3532
3528
  const fetchFilters = useCallback(async () => {
3533
- setLoading(true);
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
+ }
3534
3534
  setError(null);
3535
3535
  try {
3536
3536
  const { autoFetch: _, ...filterOptions } = options || {};
3537
3537
  const response = await stateManager.fetchFilters(filterOptions);
3538
3538
  if (mountedRef.current) {
3539
3539
  setFilters(response?.filters || []);
3540
+ hasDataRef.current = true;
3540
3541
  setLoading(false);
3541
3542
  }
3542
3543
  }
@@ -3547,14 +3548,18 @@ const useFilters = (options) => {
3547
3548
  }
3548
3549
  }
3549
3550
  }, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
3550
- // Track query + refinements to only refetch when they actually change
3551
- const prevKeyRef = useRef('');
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);
3552
3554
  // Refetch when query or refinements change (not on every state update)
3553
3555
  useEffect(() => {
3554
3556
  if (!autoFetch)
3555
3557
  return;
3556
3558
  const unsubscribe = stateManager.subscribe((state) => {
3557
- const key = `${state.query}|${state.refinements.map(r => `${r.field}:${r.value}`).sort().join(',')}`;
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;
3558
3563
  if (key === prevKeyRef.current)
3559
3564
  return;
3560
3565
  prevKeyRef.current = key;
@@ -3904,7 +3909,7 @@ const CSS_VAR_DEFAULTS = {
3904
3909
  // ---------------------------------------------------------------------------
3905
3910
  // Component
3906
3911
  // ---------------------------------------------------------------------------
3907
- 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, }) => {
3908
3913
  const { theme } = useSearchContext();
3909
3914
  const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
3910
3915
  const facetsTheme = customTheme || {};
@@ -3982,9 +3987,22 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
3982
3987
  return extracted;
3983
3988
  };
3984
3989
  const rawFacetList = extractFacets();
3990
+ const hasStats = (stats) => stats != null && (stats.min != null || stats.max != null);
3985
3991
  const facets = hideEmptyFacets
3986
- ? rawFacetList.filter(f => f.items.length > 0 || f.stats != null)
3992
+ ? rawFacetList.filter(f => f.items.length > 0 || hasStats(f.stats))
3987
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]);
3988
4006
  // -------------------------------------------------------------------
3989
4007
  // Handlers
3990
4008
  // -------------------------------------------------------------------