@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/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/primitives/ImageDisplay.js +3 -3
- package/dist/components/suggestions/types.d.ts +2 -0
- package/dist/components/suggestions/types.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +6 -6
- package/dist/hooks/useFilters.d.ts.map +1 -1
- package/dist/hooks/useFilters.js +15 -4
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.esm.js +43 -25
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +43 -25
- 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
|
|
|
@@ -2185,6 +2187,8 @@ interface ProductDisplayConfig {
|
|
|
2185
2187
|
showImage?: boolean;
|
|
2186
2188
|
/** Image aspect ratio */
|
|
2187
2189
|
imageAspectRatio?: '1:1' | '4:3' | '3:4' | '16:9' | 'auto';
|
|
2190
|
+
/** Image object-fit: 'cover' crops to fill (default), 'contain' fits entire image */
|
|
2191
|
+
imageObjectFit?: 'cover' | 'contain';
|
|
2188
2192
|
/** Show price */
|
|
2189
2193
|
showPrice?: boolean;
|
|
2190
2194
|
/** Show compare/original price */
|
package/dist/src/index.esm.js
CHANGED
|
@@ -2709,11 +2709,8 @@ function getCurrentBreakpoint(width) {
|
|
|
2709
2709
|
* Hook to get current breakpoint
|
|
2710
2710
|
*/
|
|
2711
2711
|
function useBreakpoint() {
|
|
2712
|
-
|
|
2713
|
-
|
|
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
|
-
|
|
2732
|
-
|
|
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
|
-
|
|
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
|
|
3551
|
-
|
|
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
|
-
|
|
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
|
|
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
|
// -------------------------------------------------------------------
|
|
@@ -10327,7 +10345,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
10327
10345
|
if (variant === 'hover') {
|
|
10328
10346
|
const showSecond = safeImages.length > 1 && hovering;
|
|
10329
10347
|
const src = showSecond ? safeImages[1] : safeImages[0];
|
|
10330
|
-
const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
10348
|
+
const hoverImgStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
|
|
10331
10349
|
if (enableZoom) {
|
|
10332
10350
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
10333
10351
|
React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
|
|
@@ -10346,7 +10364,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
10346
10364
|
return next;
|
|
10347
10365
|
});
|
|
10348
10366
|
};
|
|
10349
|
-
const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
10367
|
+
const carouselImgStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
|
|
10350
10368
|
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" }));
|
|
10351
10369
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
|
|
10352
10370
|
mainImage,
|
|
@@ -10378,7 +10396,7 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
|
|
|
10378
10396
|
} })))))))));
|
|
10379
10397
|
}
|
|
10380
10398
|
if (variant === 'thumbStrip' || variant === 'thumbList') {
|
|
10381
|
-
const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
|
|
10399
|
+
const thumbMainStyle = style?.aspectRatio || style?.objectFit ? { ...imgBaseStyle, ...(style.aspectRatio ? { aspectRatio: style.aspectRatio } : {}), ...(style.objectFit ? { objectFit: style.objectFit } : {}) } : imgBaseStyle;
|
|
10382
10400
|
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" }));
|
|
10383
10401
|
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: responsiveGap, ...style } },
|
|
10384
10402
|
mainImage,
|
|
@@ -11561,15 +11579,15 @@ const imgPlaceholderStyle = {
|
|
|
11561
11579
|
borderRadius: BORDER_RADIUS$1.sm,
|
|
11562
11580
|
backgroundColor: 'var(--seekora-bg-secondary, rgba(0,0,0,0.04))',
|
|
11563
11581
|
};
|
|
11564
|
-
function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
|
|
11582
|
+
function ImageBlock({ images, title, imageVariant, aspectRatio, objectFit, enableZoom, zoomMode, zoomLevel }) {
|
|
11565
11583
|
const ar = aspectRatio
|
|
11566
11584
|
? (aspectRatio.includes(':') ? aspectRatio.replace(':', '/') : aspectRatio)
|
|
11567
11585
|
: '1';
|
|
11568
11586
|
if (images.length > 0) {
|
|
11569
11587
|
return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: BORDER_RADIUS$1.sm } },
|
|
11570
|
-
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 })));
|
|
11588
|
+
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 })));
|
|
11571
11589
|
}
|
|
11572
|
-
return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
|
|
11590
|
+
return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar, ...(objectFit ? { objectFit } : {}) }, "aria-hidden": true });
|
|
11573
11591
|
}
|
|
11574
11592
|
/** minimal: image, title, price (current default behavior) */
|
|
11575
11593
|
function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
|
|
@@ -11577,7 +11595,7 @@ function MinimalLayout({ images, title, price, product, imageVariant, displayCon
|
|
|
11577
11595
|
const titleFontSize = isMobile ? '0.9375rem' : '0.875rem'; // Slightly larger on mobile
|
|
11578
11596
|
const priceFontSize = isMobile ? '0.9375rem' : '0.875rem';
|
|
11579
11597
|
return (React.createElement(React.Fragment, null,
|
|
11580
|
-
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11598
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, objectFit: displayConfig.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11581
11599
|
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),
|
|
11582
11600
|
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 } },
|
|
11583
11601
|
product.currency ?? '$',
|
|
@@ -11595,7 +11613,7 @@ function StandardLayout({ images, title, price, comparePrice, brand, badges, opt
|
|
|
11595
11613
|
const isOverlayPosition = actionButtonsPosition?.startsWith('overlay');
|
|
11596
11614
|
return (React.createElement(React.Fragment, null,
|
|
11597
11615
|
React.createElement("div", { style: { position: 'relative' } },
|
|
11598
|
-
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11616
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, objectFit: cfg.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11599
11617
|
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
|
|
11600
11618
|
actionButtons && actionButtons.length > 0 && isOverlayPosition && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
|
|
11601
11619
|
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: bodyGap } },
|
|
@@ -11614,7 +11632,7 @@ function DetailedLayout({ images, title, price, comparePrice, brand, badges, pri
|
|
|
11614
11632
|
const isOverlayPosition = actionButtonsPosition?.startsWith('overlay');
|
|
11615
11633
|
return (React.createElement(React.Fragment, null,
|
|
11616
11634
|
React.createElement("div", { style: { position: 'relative' } },
|
|
11617
|
-
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11635
|
+
React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, objectFit: cfg.imageObjectFit, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
|
|
11618
11636
|
cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
|
|
11619
11637
|
actionButtons && actionButtons.length > 0 && isOverlayPosition && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
|
|
11620
11638
|
React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|