@seekora-ai/ui-sdk-react 0.2.25 → 0.2.27

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
@@ -2711,11 +2711,8 @@ function getCurrentBreakpoint(width) {
2711
2711
  * Hook to get current breakpoint
2712
2712
  */
2713
2713
  function useBreakpoint() {
2714
- const [breakpoint, setBreakpoint] = React.useState(() => {
2715
- if (typeof window === 'undefined')
2716
- return 'lg';
2717
- return getCurrentBreakpoint(window.innerWidth);
2718
- });
2714
+ // Always start with 'lg' to avoid hydration mismatch between server and client
2715
+ const [breakpoint, setBreakpoint] = React.useState('lg');
2719
2716
  React.useEffect(() => {
2720
2717
  const handleResize = () => {
2721
2718
  setBreakpoint(getCurrentBreakpoint(window.innerWidth));
@@ -2730,11 +2727,8 @@ function useBreakpoint() {
2730
2727
  * Hook to check if current viewport matches breakpoint
2731
2728
  */
2732
2729
  function useMediaQuery(query) {
2733
- const [matches, setMatches] = React.useState(() => {
2734
- if (typeof window === 'undefined')
2735
- return false;
2736
- return window.matchMedia(query).matches;
2737
- });
2730
+ // Always start with false to avoid hydration mismatch between server and client
2731
+ const [matches, setMatches] = React.useState(false);
2738
2732
  React.useEffect(() => {
2739
2733
  const mediaQuery = window.matchMedia(query);
2740
2734
  const handleChange = () => setMatches(mediaQuery.matches);
@@ -3530,15 +3524,22 @@ const useFilters = (options) => {
3530
3524
  const [error, setError] = React.useState(null);
3531
3525
  const mountedRef = React.useRef(true);
3532
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);
3533
3529
  // Extract non-autoFetch options to pass to fetchFilters
3534
3530
  const fetchFilters = React.useCallback(async () => {
3535
- setLoading(true);
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
+ }
3536
3536
  setError(null);
3537
3537
  try {
3538
3538
  const { autoFetch: _, ...filterOptions } = options || {};
3539
3539
  const response = await stateManager.fetchFilters(filterOptions);
3540
3540
  if (mountedRef.current) {
3541
3541
  setFilters(response?.filters || []);
3542
+ hasDataRef.current = true;
3542
3543
  setLoading(false);
3543
3544
  }
3544
3545
  }
@@ -3549,14 +3550,18 @@ const useFilters = (options) => {
3549
3550
  }
3550
3551
  }
3551
3552
  }, [stateManager, options?.facetBy, options?.maxFacetValues, options?.disjunctiveFacets?.join(',')]);
3552
- // Track query + refinements to only refetch when they actually change
3553
- const prevKeyRef = React.useRef('');
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);
3554
3556
  // Refetch when query or refinements change (not on every state update)
3555
3557
  React.useEffect(() => {
3556
3558
  if (!autoFetch)
3557
3559
  return;
3558
3560
  const unsubscribe = stateManager.subscribe((state) => {
3559
- const key = `${state.query}|${state.refinements.map(r => `${r.field}:${r.value}`).sort().join(',')}`;
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;
3560
3565
  if (key === prevKeyRef.current)
3561
3566
  return;
3562
3567
  prevKeyRef.current = key;
@@ -3906,7 +3911,7 @@ const CSS_VAR_DEFAULTS = {
3906
3911
  // ---------------------------------------------------------------------------
3907
3912
  // Component
3908
3913
  // ---------------------------------------------------------------------------
3909
- 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, }) => {
3910
3915
  const { theme } = useSearchContext();
3911
3916
  const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
3912
3917
  const facetsTheme = customTheme || {};
@@ -3984,9 +3989,22 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
3984
3989
  return extracted;
3985
3990
  };
3986
3991
  const rawFacetList = extractFacets();
3992
+ const hasStats = (stats) => stats != null && (stats.min != null || stats.max != null);
3987
3993
  const facets = hideEmptyFacets
3988
- ? rawFacetList.filter(f => f.items.length > 0 || f.stats != null)
3994
+ ? rawFacetList.filter(f => f.items.length > 0 || hasStats(f.stats))
3989
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]);
3990
4008
  // -------------------------------------------------------------------
3991
4009
  // Handlers
3992
4010
  // -------------------------------------------------------------------
@@ -10329,7 +10347,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
10329
10347
  if (variant === 'hover') {
10330
10348
  const showSecond = safeImages.length > 1 && hovering;
10331
10349
  const src = showSecond ? safeImages[1] : safeImages[0];
10332
- const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
10350
+ const hoverImgStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
10333
10351
  if (enableZoom) {
10334
10352
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
10335
10353
  React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
@@ -10348,7 +10366,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
10348
10366
  return next;
10349
10367
  });
10350
10368
  };
10351
- const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
10369
+ const carouselImgStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
10352
10370
  const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-carousel-main", style: carouselImgStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: carouselImgStyle, loading: "lazy" }));
10353
10371
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
10354
10372
  mainImage,
@@ -10380,7 +10398,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
10380
10398
  } })))))))));
10381
10399
  }
10382
10400
  if (variant === 'thumbStrip' || variant === 'thumbList') {
10383
- const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
10401
+ const thumbMainStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
10384
10402
  const mainImage = enableZoom ? (React.createElement(ImageZoom, { src: current, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: index, className: "seekora-img-thumb-main", style: thumbMainStyle })) : (React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: thumbMainStyle, loading: "lazy" }));
10385
10403
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: responsiveGap, ...style } },
10386
10404
  mainImage,
@@ -11563,15 +11581,15 @@ const imgPlaceholderStyle = {
11563
11581
  borderRadius: BORDER_RADIUS$1.sm,
11564
11582
  backgroundColor: 'var(--seekora-bg-secondary, rgba(0,0,0,0.04))',
11565
11583
  };
11566
- function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
11584
+ function ImageBlock({ images, title, imageVariant, aspectRatio, objectFit, enableZoom, zoomMode, zoomLevel }) {
11567
11585
  const ar = aspectRatio
11568
11586
  ? (aspectRatio.includes(':') ? aspectRatio.replace(':', '/') : aspectRatio)
11569
11587
  : '1';
11570
11588
  if (images.length > 0) {
11571
11589
  return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: BORDER_RADIUS$1.sm } },
11572
- React.createElement(ImageDisplay, { images: images, variant: images.length > 1 ? imageVariant : 'single', alt: title, className: "seekora-suggestions-product-card-image", style: { aspectRatio: ar }, enableZoom: enableZoom, zoomMode: zoomMode, zoomLevel: zoomLevel })));
11590
+ React.createElement(ImageDisplay, { images: images, variant: images.length > 1 ? imageVariant : 'single', alt: title, className: "seekora-suggestions-product-card-image", style: { aspectRatio: ar, ...(objectFit ? { objectFit } : {}) }, enableZoom: enableZoom, zoomMode: zoomMode, zoomLevel: zoomLevel })));
11573
11591
  }
11574
- return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
11592
+ return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar, ...(objectFit ? { objectFit } : {}) }, "aria-hidden": true });
11575
11593
  }
11576
11594
  /** minimal: image, title, price (current default behavior) */
11577
11595
  function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
@@ -11579,7 +11597,7 @@ function MinimalLayout({ images, title, price, product, imageVariant, displayCon
11579
11597
  const titleFontSize = isMobile ? '0.9375rem' : '0.875rem'; // Slightly larger on mobile
11580
11598
  const priceFontSize = isMobile ? '0.9375rem' : '0.875rem';
11581
11599
  return (React.createElement(React.Fragment, null,
11582
- React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11600
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, objectFit: displayConfig.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11583
11601
  React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: titleFontSize, fontWeight: 500, lineHeight: 1.4, overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' } }, title),
11584
11602
  price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price seekora-suggestions-product-card-price", style: { fontSize: priceFontSize, color: 'inherit', opacity: 0.6 } },
11585
11603
  product.currency ?? '$',
@@ -11597,7 +11615,7 @@ function StandardLayout({ images, title, price, comparePrice, brand, badges, opt
11597
11615
  const isOverlayPosition = actionButtonsPosition?.startsWith('overlay');
11598
11616
  return (React.createElement(React.Fragment, null,
11599
11617
  React.createElement("div", { style: { position: 'relative' } },
11600
- React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11618
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, objectFit: cfg.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11601
11619
  cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
11602
11620
  actionButtons && actionButtons.length > 0 && isOverlayPosition && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
11603
11621
  React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: bodyGap } },
@@ -11616,7 +11634,7 @@ function DetailedLayout({ images, title, price, comparePrice, brand, badges, pri
11616
11634
  const isOverlayPosition = actionButtonsPosition?.startsWith('overlay');
11617
11635
  return (React.createElement(React.Fragment, null,
11618
11636
  React.createElement("div", { style: { position: 'relative' } },
11619
- React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11637
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, objectFit: cfg.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
11620
11638
  cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
11621
11639
  actionButtons && actionButtons.length > 0 && isOverlayPosition && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
11622
11640
  React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },