@seekora-ai/ui-sdk-react 0.2.11 → 0.2.12

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.
Files changed (71) hide show
  1. package/dist/components/primitives/ActionButtons.d.ts +27 -0
  2. package/dist/components/primitives/ActionButtons.d.ts.map +1 -0
  3. package/dist/components/primitives/ActionButtons.js +78 -0
  4. package/dist/components/primitives/AnalyticsProvider.d.ts +22 -0
  5. package/dist/components/primitives/AnalyticsProvider.d.ts.map +1 -0
  6. package/dist/components/primitives/AnalyticsProvider.js +87 -0
  7. package/dist/components/primitives/BadgeList.d.ts +14 -0
  8. package/dist/components/primitives/BadgeList.d.ts.map +1 -0
  9. package/dist/components/primitives/BadgeList.js +45 -0
  10. package/dist/components/primitives/ImageDisplay.d.ts +10 -1
  11. package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
  12. package/dist/components/primitives/ImageDisplay.js +49 -9
  13. package/dist/components/primitives/ImageZoom.d.ts +33 -0
  14. package/dist/components/primitives/ImageZoom.d.ts.map +1 -0
  15. package/dist/components/primitives/ImageZoom.js +357 -0
  16. package/dist/components/primitives/PriceDisplay.d.ts +21 -0
  17. package/dist/components/primitives/PriceDisplay.d.ts.map +1 -0
  18. package/dist/components/primitives/PriceDisplay.js +44 -0
  19. package/dist/components/primitives/RatingDisplay.d.ts +43 -0
  20. package/dist/components/primitives/RatingDisplay.d.ts.map +1 -0
  21. package/dist/components/primitives/RatingDisplay.js +114 -0
  22. package/dist/components/primitives/VariantSelector.d.ts +30 -0
  23. package/dist/components/primitives/VariantSelector.d.ts.map +1 -0
  24. package/dist/components/primitives/VariantSelector.js +162 -0
  25. package/dist/components/primitives/VariantSwatches.d.ts +28 -0
  26. package/dist/components/primitives/VariantSwatches.d.ts.map +1 -0
  27. package/dist/components/primitives/VariantSwatches.js +173 -0
  28. package/dist/components/primitives/index.d.ts +9 -0
  29. package/dist/components/primitives/index.d.ts.map +1 -1
  30. package/dist/components/primitives/index.js +9 -0
  31. package/dist/components/primitives/withAnalytics.d.ts +24 -0
  32. package/dist/components/primitives/withAnalytics.d.ts.map +1 -0
  33. package/dist/components/primitives/withAnalytics.js +73 -0
  34. package/dist/components/product-page/ProductInfo.d.ts +25 -2
  35. package/dist/components/product-page/ProductInfo.d.ts.map +1 -1
  36. package/dist/components/product-page/ProductInfo.js +20 -5
  37. package/dist/components/suggestions/types.d.ts +24 -0
  38. package/dist/components/suggestions/types.d.ts.map +1 -1
  39. package/dist/components/suggestions/utils.d.ts +37 -0
  40. package/dist/components/suggestions/utils.d.ts.map +1 -1
  41. package/dist/components/suggestions/utils.js +118 -0
  42. package/dist/components/suggestions-primitives/ItemCard.d.ts +10 -1
  43. package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
  44. package/dist/components/suggestions-primitives/ItemCard.js +20 -6
  45. package/dist/components/suggestions-primitives/ProductCard.d.ts +27 -3
  46. package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
  47. package/dist/components/suggestions-primitives/ProductCard.js +124 -17
  48. package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts +44 -0
  49. package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -0
  50. package/dist/components/suggestions-primitives/ProductCardLayouts.js +105 -0
  51. package/dist/components/suggestions-primitives/ProductGrid.d.ts +6 -1
  52. package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -1
  53. package/dist/components/suggestions-primitives/ProductGrid.js +2 -2
  54. package/dist/hooks/useProductAnalytics.d.ts +49 -0
  55. package/dist/hooks/useProductAnalytics.d.ts.map +1 -0
  56. package/dist/hooks/useProductAnalytics.js +116 -0
  57. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  58. package/dist/hooks/useSuggestionsAnalytics.js +6 -0
  59. package/dist/hooks/useVariantSelection.d.ts +28 -0
  60. package/dist/hooks/useVariantSelection.d.ts.map +1 -0
  61. package/dist/hooks/useVariantSelection.js +44 -0
  62. package/dist/index.d.ts +8 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +5 -1
  65. package/dist/index.umd.js +1 -1
  66. package/dist/src/index.d.ts +1107 -679
  67. package/dist/src/index.esm.js +2267 -600
  68. package/dist/src/index.esm.js.map +1 -1
  69. package/dist/src/index.js +2283 -599
  70. package/dist/src/index.js.map +1 -1
  71. package/package.json +3 -3
@@ -6261,8 +6261,7 @@ const EVENTS = {
6261
6261
  TRENDING_CLICK: 'suggestions.trending_click',
6262
6262
  SEARCH_SUBMIT: 'suggestions.search_submit',
6263
6263
  DROPDOWN_OPEN: 'suggestions.dropdown_open',
6264
- DROPDOWN_CLOSE: 'suggestions.dropdown_close',
6265
- };
6264
+ DROPDOWN_CLOSE: 'suggestions.dropdown_close'};
6266
6265
  // ============================================================================
6267
6266
  // Hook Implementation
6268
6267
  // ============================================================================
@@ -7232,6 +7231,362 @@ function SuggestionList({ maxItems = 10, className, style, listClassName, enable
7232
7231
  }))));
7233
7232
  }
7234
7233
 
7234
+ /**
7235
+ * ImageZoom – zoom on hover and click (Amazon-style magnifier + lightbox)
7236
+ *
7237
+ * Supports three zoom modes:
7238
+ * - hover: Magnified view in a separate panel on the right (Amazon style)
7239
+ * - lens: Magnifying glass that follows cursor
7240
+ * - click: Full-screen lightbox modal
7241
+ */
7242
+ function ImageZoom({ src, alt = '', mode = 'both', zoomLevel = 2.5, className, style, showZoomIndicator = true, lensSize = 150, zoomPanelSize = { width: 400, height: 400 }, images, currentIndex = 0, }) {
7243
+ const [isHovering, setIsHovering] = useState(false);
7244
+ const [isLightboxOpen, setIsLightboxOpen] = useState(false);
7245
+ const [lightboxIndex, setLightboxIndex] = useState(currentIndex);
7246
+ const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
7247
+ const [imageLoaded, setImageLoaded] = useState(false);
7248
+ const [touchStart, setTouchStart] = useState(null);
7249
+ const [touchEnd, setTouchEnd] = useState(null);
7250
+ const imageRef = useRef(null);
7251
+ const containerRef = useRef(null);
7252
+ const allImages = images && images.length > 0 ? images : [src];
7253
+ const hasMultipleImages = allImages.length > 1;
7254
+ // Minimum swipe distance (in px) to trigger navigation
7255
+ const minSwipeDistance = 50;
7256
+ const supportsHover = mode === 'hover' || mode === 'both';
7257
+ const supportsClick = mode === 'click' || mode === 'both';
7258
+ const supportsLens = mode === 'lens';
7259
+ const handleMouseMove = useCallback((e) => {
7260
+ if (!containerRef.current)
7261
+ return;
7262
+ const rect = containerRef.current.getBoundingClientRect();
7263
+ const x = e.clientX - rect.left;
7264
+ const y = e.clientY - rect.top;
7265
+ setCursorPos({ x, y });
7266
+ }, []);
7267
+ const handleMouseEnter = useCallback(() => {
7268
+ setIsHovering(true);
7269
+ }, []);
7270
+ const handleMouseLeave = useCallback(() => {
7271
+ setIsHovering(false);
7272
+ }, []);
7273
+ const handleClick = useCallback((e) => {
7274
+ if (supportsClick) {
7275
+ e?.stopPropagation();
7276
+ setLightboxIndex(currentIndex);
7277
+ setIsLightboxOpen(true);
7278
+ }
7279
+ }, [supportsClick, currentIndex]);
7280
+ const closeLightbox = useCallback((e) => {
7281
+ e?.stopPropagation();
7282
+ setIsLightboxOpen(false);
7283
+ }, []);
7284
+ const goToNext = useCallback(() => {
7285
+ setLightboxIndex((i) => (i + 1) % allImages.length);
7286
+ }, [allImages.length]);
7287
+ const goToPrev = useCallback(() => {
7288
+ setLightboxIndex((i) => (i - 1 + allImages.length) % allImages.length);
7289
+ }, [allImages.length]);
7290
+ // Touch event handlers for swipe
7291
+ const handleTouchStart = useCallback((e) => {
7292
+ setTouchEnd(null);
7293
+ setTouchStart(e.targetTouches[0].clientX);
7294
+ }, []);
7295
+ const handleTouchMove = useCallback((e) => {
7296
+ setTouchEnd(e.targetTouches[0].clientX);
7297
+ }, []);
7298
+ const handleTouchEnd = useCallback(() => {
7299
+ if (!touchStart || !touchEnd)
7300
+ return;
7301
+ const distance = touchStart - touchEnd;
7302
+ const isLeftSwipe = distance > minSwipeDistance;
7303
+ const isRightSwipe = distance < -minSwipeDistance;
7304
+ if (isLeftSwipe) {
7305
+ goToNext();
7306
+ }
7307
+ else if (isRightSwipe) {
7308
+ goToPrev();
7309
+ }
7310
+ }, [touchStart, touchEnd, minSwipeDistance, goToNext, goToPrev]);
7311
+ // Close lightbox on Escape key, navigate with arrow keys
7312
+ useEffect(() => {
7313
+ if (!isLightboxOpen)
7314
+ return;
7315
+ const handleKeyboard = (e) => {
7316
+ if (e.key === 'Escape')
7317
+ closeLightbox();
7318
+ if (hasMultipleImages) {
7319
+ if (e.key === 'ArrowRight')
7320
+ goToNext();
7321
+ if (e.key === 'ArrowLeft')
7322
+ goToPrev();
7323
+ }
7324
+ };
7325
+ window.addEventListener('keydown', handleKeyboard);
7326
+ return () => window.removeEventListener('keydown', handleKeyboard);
7327
+ }, [isLightboxOpen, closeLightbox, hasMultipleImages, goToNext, goToPrev]);
7328
+ const containerStyle = {
7329
+ position: 'relative',
7330
+ display: 'inline-block',
7331
+ cursor: supportsClick ? 'zoom-in' : supportsHover || supportsLens ? 'crosshair' : 'default',
7332
+ overflow: 'hidden',
7333
+ ...style,
7334
+ };
7335
+ const imageStyle = {
7336
+ width: '100%',
7337
+ height: '100%',
7338
+ objectFit: 'cover',
7339
+ display: 'block',
7340
+ };
7341
+ // Calculate zoom panel background position (Amazon-style)
7342
+ const getZoomPanelStyle = () => {
7343
+ if (!containerRef.current || !imageLoaded)
7344
+ return { display: 'none' };
7345
+ const rect = containerRef.current.getBoundingClientRect();
7346
+ const bgPosX = (cursorPos.x / rect.width) * 100;
7347
+ const bgPosY = (cursorPos.y / rect.height) * 100;
7348
+ return {
7349
+ position: 'fixed', // Changed from absolute to fixed for better positioning
7350
+ top: rect.top,
7351
+ left: rect.right + 16, // 16px gap from the image
7352
+ width: zoomPanelSize.width,
7353
+ height: zoomPanelSize.height,
7354
+ backgroundImage: `url(${src})`,
7355
+ backgroundSize: `${zoomLevel * 100}%`,
7356
+ backgroundPosition: `${bgPosX}% ${bgPosY}%`,
7357
+ backgroundRepeat: 'no-repeat',
7358
+ border: '2px solid var(--seekora-border-color, #e5e7eb)',
7359
+ borderRadius: 8,
7360
+ boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
7361
+ backgroundColor: '#fff',
7362
+ pointerEvents: 'none',
7363
+ zIndex: 9998,
7364
+ };
7365
+ };
7366
+ // Calculate lens position and zoom
7367
+ const getLensStyle = () => {
7368
+ if (!containerRef.current)
7369
+ return { display: 'none' };
7370
+ return {
7371
+ position: 'absolute',
7372
+ width: lensSize,
7373
+ height: lensSize,
7374
+ left: cursorPos.x - lensSize / 2,
7375
+ top: cursorPos.y - lensSize / 2,
7376
+ border: '2px solid rgba(255,255,255,0.8)',
7377
+ borderRadius: '50%',
7378
+ boxShadow: '0 0 0 1px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(0,0,0,0.3)',
7379
+ pointerEvents: 'none',
7380
+ overflow: 'hidden',
7381
+ zIndex: 100,
7382
+ };
7383
+ };
7384
+ const getLensImageStyle = () => {
7385
+ if (!containerRef.current || !imageRef.current)
7386
+ return {};
7387
+ const rect = containerRef.current.getBoundingClientRect();
7388
+ imageRef.current.getBoundingClientRect();
7389
+ return {
7390
+ position: 'absolute',
7391
+ width: rect.width * zoomLevel,
7392
+ height: rect.height * zoomLevel,
7393
+ left: -(cursorPos.x * zoomLevel - lensSize / 2),
7394
+ top: -(cursorPos.y * zoomLevel - lensSize / 2),
7395
+ objectFit: 'cover',
7396
+ };
7397
+ };
7398
+ return (React.createElement(React.Fragment, null,
7399
+ React.createElement("div", { ref: containerRef, className: clsx('seekora-image-zoom', className), style: containerStyle, onMouseMove: handleMouseMove, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: handleClick },
7400
+ React.createElement("img", { ref: imageRef, src: src, alt: alt, style: imageStyle, onLoad: () => setImageLoaded(true) }),
7401
+ showZoomIndicator && supportsClick && (React.createElement("div", { style: {
7402
+ position: 'absolute',
7403
+ top: 8,
7404
+ right: 8,
7405
+ width: 32,
7406
+ height: 32,
7407
+ borderRadius: '50%',
7408
+ backgroundColor: 'rgba(0,0,0,0.6)',
7409
+ color: '#fff',
7410
+ display: 'flex',
7411
+ alignItems: 'center',
7412
+ justifyContent: 'center',
7413
+ fontSize: '1.25rem',
7414
+ pointerEvents: 'none',
7415
+ opacity: isHovering ? 1 : 0.7,
7416
+ transition: 'opacity 150ms',
7417
+ } }, "\uD83D\uDD0D")),
7418
+ supportsLens && isHovering && imageLoaded && (React.createElement("div", { style: getLensStyle() },
7419
+ React.createElement("img", { src: src, alt: "", style: getLensImageStyle() }))),
7420
+ supportsHover && isHovering && imageLoaded && containerRef.current && (React.createElement("div", { style: {
7421
+ position: 'absolute',
7422
+ left: cursorPos.x - 75,
7423
+ top: cursorPos.y - 75,
7424
+ width: 150,
7425
+ height: 150,
7426
+ border: '2px solid rgba(0,0,0,0.3)',
7427
+ backgroundColor: 'rgba(255,255,255,0.1)',
7428
+ pointerEvents: 'none',
7429
+ zIndex: 50,
7430
+ } })),
7431
+ supportsHover && isHovering && imageLoaded && (React.createElement("div", { style: getZoomPanelStyle() }))),
7432
+ isLightboxOpen && (React.createElement("div", { className: "seekora-image-zoom-lightbox", style: {
7433
+ position: 'fixed',
7434
+ top: 0,
7435
+ left: 0,
7436
+ right: 0,
7437
+ bottom: 0,
7438
+ backgroundColor: 'rgba(0,0,0,0.95)',
7439
+ zIndex: 9999,
7440
+ display: 'flex',
7441
+ alignItems: 'center',
7442
+ justifyContent: 'center',
7443
+ cursor: 'zoom-out',
7444
+ padding: 20,
7445
+ }, onClick: closeLightbox, onTouchStart: hasMultipleImages ? handleTouchStart : undefined, onTouchMove: hasMultipleImages ? handleTouchMove : undefined, onTouchEnd: hasMultipleImages ? handleTouchEnd : undefined },
7446
+ React.createElement("button", { type: "button", "aria-label": "Close zoom", style: {
7447
+ position: 'absolute',
7448
+ top: 20,
7449
+ right: 20,
7450
+ width: 44,
7451
+ height: 44,
7452
+ borderRadius: '50%',
7453
+ border: 'none',
7454
+ backgroundColor: 'rgba(255,255,255,0.2)',
7455
+ color: '#fff',
7456
+ fontSize: '1.5rem',
7457
+ cursor: 'pointer',
7458
+ display: 'flex',
7459
+ alignItems: 'center',
7460
+ justifyContent: 'center',
7461
+ transition: 'background-color 150ms',
7462
+ zIndex: 10001,
7463
+ }, onClick: (e) => {
7464
+ e.stopPropagation();
7465
+ closeLightbox();
7466
+ }, onMouseEnter: (e) => {
7467
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7468
+ }, onMouseLeave: (e) => {
7469
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7470
+ } }, "\u2715"),
7471
+ hasMultipleImages && (React.createElement(React.Fragment, null,
7472
+ React.createElement("button", { type: "button", "aria-label": "Previous image", style: {
7473
+ position: 'absolute',
7474
+ left: 20,
7475
+ top: '50%',
7476
+ transform: 'translateY(-50%)',
7477
+ width: 56,
7478
+ height: 56,
7479
+ borderRadius: '50%',
7480
+ border: 'none',
7481
+ backgroundColor: 'rgba(255,255,255,0.2)',
7482
+ color: '#fff',
7483
+ fontSize: '2rem',
7484
+ fontWeight: 'bold',
7485
+ cursor: 'pointer',
7486
+ display: 'flex',
7487
+ alignItems: 'center',
7488
+ justifyContent: 'center',
7489
+ transition: 'background-color 150ms',
7490
+ zIndex: 10001,
7491
+ }, onClick: (e) => {
7492
+ e.stopPropagation();
7493
+ goToPrev();
7494
+ }, onMouseEnter: (e) => {
7495
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7496
+ }, onMouseLeave: (e) => {
7497
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7498
+ } }, "\u2039"),
7499
+ React.createElement("button", { type: "button", "aria-label": "Next image", style: {
7500
+ position: 'absolute',
7501
+ right: 20,
7502
+ top: '50%',
7503
+ transform: 'translateY(-50%)',
7504
+ width: 56,
7505
+ height: 56,
7506
+ borderRadius: '50%',
7507
+ border: 'none',
7508
+ backgroundColor: 'rgba(255,255,255,0.2)',
7509
+ color: '#fff',
7510
+ fontSize: '2rem',
7511
+ fontWeight: 'bold',
7512
+ cursor: 'pointer',
7513
+ display: 'flex',
7514
+ alignItems: 'center',
7515
+ justifyContent: 'center',
7516
+ transition: 'background-color 150ms',
7517
+ zIndex: 10001,
7518
+ }, onClick: (e) => {
7519
+ e.stopPropagation();
7520
+ goToNext();
7521
+ }, onMouseEnter: (e) => {
7522
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.3)';
7523
+ }, onMouseLeave: (e) => {
7524
+ e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
7525
+ } }, "\u203A"))),
7526
+ React.createElement("img", { src: allImages[lightboxIndex], alt: alt, style: {
7527
+ maxWidth: '90%',
7528
+ maxHeight: '90%',
7529
+ objectFit: 'contain',
7530
+ borderRadius: 4,
7531
+ cursor: 'default',
7532
+ }, onClick: (e) => e.stopPropagation() }),
7533
+ hasMultipleImages && (React.createElement("div", { style: {
7534
+ position: 'absolute',
7535
+ bottom: 20,
7536
+ left: '50%',
7537
+ transform: 'translateX(-50%)',
7538
+ display: 'flex',
7539
+ flexDirection: 'column',
7540
+ alignItems: 'center',
7541
+ gap: 12,
7542
+ }, onClick: (e) => e.stopPropagation() },
7543
+ React.createElement("div", { style: { display: 'flex', gap: 8, overflowX: 'auto', maxWidth: '80vw', padding: '8px 0' } }, allImages.map((img, i) => (React.createElement("button", { key: i, type: "button", onClick: (e) => {
7544
+ e.stopPropagation();
7545
+ setLightboxIndex(i);
7546
+ }, style: {
7547
+ width: 60,
7548
+ height: 60,
7549
+ padding: 0,
7550
+ border: i === lightboxIndex ? '3px solid #fff' : '2px solid rgba(255,255,255,0.3)',
7551
+ borderRadius: 4,
7552
+ overflow: 'hidden',
7553
+ cursor: 'pointer',
7554
+ opacity: i === lightboxIndex ? 1 : 0.6,
7555
+ transition: 'all 150ms ease',
7556
+ flexShrink: 0,
7557
+ background: 'none',
7558
+ }, onMouseEnter: (e) => {
7559
+ e.currentTarget.style.opacity = '1';
7560
+ }, onMouseLeave: (e) => {
7561
+ if (i !== lightboxIndex)
7562
+ e.currentTarget.style.opacity = '0.6';
7563
+ } },
7564
+ React.createElement("img", { src: img, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } }))))),
7565
+ React.createElement("div", { style: {
7566
+ color: 'rgba(255,255,255,0.9)',
7567
+ fontSize: '0.875rem',
7568
+ textAlign: 'center',
7569
+ backgroundColor: 'rgba(0,0,0,0.5)',
7570
+ padding: '4px 12px',
7571
+ borderRadius: 12,
7572
+ } },
7573
+ lightboxIndex + 1,
7574
+ " / ",
7575
+ allImages.length))),
7576
+ React.createElement("div", { style: {
7577
+ position: 'absolute',
7578
+ top: 20,
7579
+ left: '50%',
7580
+ transform: 'translateX(-50%)',
7581
+ color: 'rgba(255,255,255,0.7)',
7582
+ fontSize: '0.875rem',
7583
+ textAlign: 'center',
7584
+ backgroundColor: 'rgba(0,0,0,0.5)',
7585
+ padding: '8px 16px',
7586
+ borderRadius: 12,
7587
+ } }, hasMultipleImages ? 'Use arrow keys or click thumbnails to navigate • ESC to close' : 'Click outside or press ESC to close')))));
7588
+ }
7589
+
7235
7590
  /**
7236
7591
  * ImageDisplay – configurable multi-image display (primitive)
7237
7592
  *
@@ -7245,7 +7600,7 @@ const imgBaseStyle = {
7245
7600
  borderRadius: 4,
7246
7601
  backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7247
7602
  };
7248
- function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
7603
+ function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, enableZoom = false, zoomMode = 'both', zoomLevel = 2.5, showDots = true, }) {
7249
7604
  const [index, setIndex] = useState(0);
7250
7605
  const [hovering, setHovering] = useState(false);
7251
7606
  const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
@@ -7254,13 +7609,21 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
7254
7609
  return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
7255
7610
  }
7256
7611
  if (variant === 'single') {
7612
+ if (enableZoom) {
7613
+ return (React.createElement(ImageZoom, { src: safeImages[0], alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: 0, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style } }));
7614
+ }
7257
7615
  return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
7258
7616
  }
7259
7617
  if (variant === 'hover') {
7260
7618
  const showSecond = safeImages.length > 1 && hovering;
7261
7619
  const src = showSecond ? safeImages[1] : safeImages[0];
7620
+ const hoverImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7621
+ if (enableZoom) {
7622
+ return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
7623
+ React.createElement(ImageZoom, { src: src, alt: alt, mode: zoomMode, zoomLevel: zoomLevel, images: safeImages, currentIndex: showSecond ? 1 : 0, className: "seekora-img-hover-img", style: hoverImgStyle })));
7624
+ }
7262
7625
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
7263
- React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: imgBaseStyle, loading: "lazy" })));
7626
+ React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: hoverImgStyle, loading: "lazy" })));
7264
7627
  }
7265
7628
  if (variant === 'carousel') {
7266
7629
  const go = (delta) => {
@@ -7273,16 +7636,41 @@ function ImageDisplay({ images, variant = 'single', alt = '', className, style,
7273
7636
  return next;
7274
7637
  });
7275
7638
  };
7639
+ const carouselImgStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7640
+ 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" }));
7276
7641
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
7277
- React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: imgBaseStyle, loading: "lazy" }),
7642
+ mainImage,
7278
7643
  safeImages.length > 1 && (React.createElement(React.Fragment, null,
7279
- React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
7280
- React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
7644
+ React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(-1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u2039"),
7645
+ React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); go(1); }, onClick: (e) => e.stopPropagation(), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,1)'; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.9)'; } }, "\u203A"),
7646
+ showDots && (React.createElement("div", { className: "seekora-img-carousel-dots", style: {
7647
+ position: 'absolute',
7648
+ bottom: 8,
7649
+ left: '50%',
7650
+ transform: 'translateX(-50%)',
7651
+ display: 'flex',
7652
+ gap: 6,
7653
+ padding: '6px 12px',
7654
+ backgroundColor: 'rgba(0,0,0,0.5)',
7655
+ borderRadius: 12,
7656
+ zIndex: 10,
7657
+ } }, safeImages.map((_, i) => (React.createElement("button", { key: i, type: "button", "aria-label": `Go to image ${i + 1}`, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); }, onClick: (e) => { e.stopPropagation(); setIndex(i); }, style: {
7658
+ width: 8,
7659
+ height: 8,
7660
+ borderRadius: '50%',
7661
+ border: 'none',
7662
+ padding: 0,
7663
+ backgroundColor: i === index ? '#fff' : 'rgba(255,255,255,0.5)',
7664
+ cursor: 'pointer',
7665
+ transition: 'all 150ms ease',
7666
+ } })))))))));
7281
7667
  }
7282
7668
  if (variant === 'thumbStrip' || variant === 'thumbList') {
7669
+ const thumbMainStyle = style?.aspectRatio ? { ...imgBaseStyle, aspectRatio: style.aspectRatio } : imgBaseStyle;
7670
+ 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" }));
7283
7671
  return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
7284
- React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: imgBaseStyle, loading: "lazy" }),
7285
- React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: () => setIndex(i) },
7672
+ mainImage,
7673
+ React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: (e) => { e.stopPropagation(); e.preventDefault(); setIndex(i); }, onClick: (e) => e.stopPropagation() },
7286
7674
  React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
7287
7675
  }
7288
7676
  return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
@@ -7296,13 +7684,96 @@ function arrowStyle(left) {
7296
7684
  width: 32,
7297
7685
  height: 32,
7298
7686
  borderRadius: '50%',
7299
- border: '1px solid var(--seekora-border-color)',
7300
- backgroundColor: 'var(--seekora-bg-surface)',
7687
+ border: 'none',
7688
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
7689
+ color: '#111',
7690
+ fontSize: '1.25rem',
7691
+ fontWeight: 'bold',
7301
7692
  cursor: 'pointer',
7302
7693
  display: 'flex',
7303
7694
  alignItems: 'center',
7304
7695
  justifyContent: 'center',
7696
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
7697
+ zIndex: 10,
7698
+ transition: 'all 150ms ease',
7699
+ };
7700
+ }
7701
+
7702
+ /**
7703
+ * ActionButtons – card action buttons (add to cart, wishlist, buy now, quick view)
7704
+ *
7705
+ * Renders a set of action buttons for product cards. Can be positioned absolutely
7706
+ * over the image (on hover) or inline below the card content.
7707
+ */
7708
+ const DEFAULT_ICONS = {
7709
+ addToCart: '🛒',
7710
+ wishlist: '♡',
7711
+ buyNow: '⚡',
7712
+ quickView: '👁',
7713
+ compare: '⚖',
7714
+ };
7715
+ const DEFAULT_LABELS = {
7716
+ addToCart: 'Add to Cart',
7717
+ wishlist: 'Wishlist',
7718
+ buyNow: 'Buy Now',
7719
+ quickView: 'Quick View',
7720
+ compare: 'Compare',
7721
+ };
7722
+ const BUTTON_SIZES = {
7723
+ small: { width: 28, height: 28, fontSize: '0.75rem', iconSize: '1rem' },
7724
+ medium: { width: 36, height: 36, fontSize: '0.875rem', iconSize: '1.25rem' },
7725
+ large: { width: 44, height: 44, fontSize: '1rem', iconSize: '1.5rem' },
7726
+ };
7727
+ function ActionButtons({ buttons, layout = 'horizontal', position = 'inline', showLabels = false, size = 'medium', className, style, }) {
7728
+ const sizeConfig = BUTTON_SIZES[size];
7729
+ const isOverlay = position !== 'inline';
7730
+ const containerStyle = {
7731
+ display: 'flex',
7732
+ flexDirection: layout === 'vertical' ? 'column' : 'row',
7733
+ gap: 6,
7734
+ ...(isOverlay ? {
7735
+ position: 'absolute',
7736
+ ...(position === 'top-right' ? { top: 8, right: 8 } : {}),
7737
+ ...(position === 'bottom-center' ? { bottom: 8, left: '50%', transform: 'translateX(-50%)' } : {}),
7738
+ } : {}),
7739
+ ...style,
7740
+ };
7741
+ const buttonBaseStyle = {
7742
+ display: 'flex',
7743
+ alignItems: 'center',
7744
+ justifyContent: 'center',
7745
+ gap: 4,
7746
+ padding: showLabels ? '0 12px' : 0,
7747
+ width: showLabels ? 'auto' : sizeConfig.width,
7748
+ height: sizeConfig.height,
7749
+ fontSize: sizeConfig.fontSize,
7750
+ fontWeight: 500,
7751
+ border: 'none',
7752
+ borderRadius: 6,
7753
+ backgroundColor: 'var(--seekora-bg-surface, #fff)',
7754
+ color: 'var(--seekora-text, #111827)',
7755
+ cursor: 'pointer',
7756
+ transition: 'all 150ms ease',
7757
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
7758
+ };
7759
+ const handleClick = (btn, e) => {
7760
+ e.stopPropagation();
7761
+ e.preventDefault();
7762
+ if (btn.onClick && !btn.disabled && !btn.loading) {
7763
+ btn.onClick(e);
7764
+ }
7305
7765
  };
7766
+ return (React.createElement("div", { className: clsx('seekora-action-buttons', `seekora-action-buttons--${layout}`, className), style: containerStyle }, buttons.map((btn, i) => {
7767
+ const icon = btn.icon ?? DEFAULT_ICONS[btn.type];
7768
+ const label = btn.label ?? DEFAULT_LABELS[btn.type];
7769
+ return (React.createElement("button", { key: i, type: "button", className: clsx('seekora-action-button', `seekora-action-button--${btn.type}`, btn.disabled && 'seekora-action-button--disabled', btn.loading && 'seekora-action-button--loading'), style: {
7770
+ ...buttonBaseStyle,
7771
+ opacity: btn.disabled ? 0.5 : 1,
7772
+ cursor: btn.disabled ? 'not-allowed' : 'pointer',
7773
+ }, onClick: (e) => handleClick(btn, e), disabled: btn.disabled, "aria-label": label, title: label }, btn.loading ? (React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, "\u23F3")) : (React.createElement(React.Fragment, null,
7774
+ React.createElement("span", { style: { fontSize: sizeConfig.iconSize } }, icon),
7775
+ showLabels && React.createElement("span", null, label)))));
7776
+ })));
7306
7777
  }
7307
7778
 
7308
7779
  /**
@@ -7333,18 +7804,31 @@ const imgStyle$1 = {
7333
7804
  borderRadius: 4,
7334
7805
  backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7335
7806
  };
7336
- function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', }) {
7807
+ function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', layout = 'vertical', actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, }) {
7337
7808
  const images = item.images?.length ? item.images : item.image ?? item.imageUrl ? [String(item.image ?? item.imageUrl)] : [];
7338
7809
  const title = item.title ?? item.primaryText ?? '';
7339
7810
  const description = item.description ?? item.secondaryText;
7340
7811
  const href = item.url;
7341
- const content = (React.createElement(React.Fragment, null,
7342
- images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" })) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: imgStyle$1, "aria-hidden": true })),
7812
+ const isHorizontal = layout === 'horizontal';
7813
+ const imageBlock = images.length > 0 ? (React.createElement("div", { style: { position: 'relative', ...(isHorizontal ? { width: 80, flexShrink: 0 } : {}) } },
7814
+ React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" }),
7815
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" })))) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: { ...imgStyle$1, ...(isHorizontal ? { width: 80, height: 80, flexShrink: 0 } : {}) }, "aria-hidden": true }));
7816
+ const textBlock = (React.createElement("div", { style: isHorizontal ? { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } : undefined },
7343
7817
  React.createElement("span", { className: "seekora-item-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, String(title)),
7344
- description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null));
7818
+ description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null,
7819
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" }))));
7820
+ const content = isHorizontal ? (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
7821
+ imageBlock,
7822
+ textBlock)) : (React.createElement(React.Fragment, null,
7823
+ imageBlock,
7824
+ textBlock));
7345
7825
  const commonProps = {
7346
- className: clsx('seekora-item-card', className),
7347
- style: { ...cardStyle$1, ...style },
7826
+ className: clsx('seekora-item-card', isHorizontal && 'seekora-item-card--horizontal', className),
7827
+ style: {
7828
+ ...cardStyle$1,
7829
+ ...(isHorizontal ? { flexDirection: 'row' } : {}),
7830
+ ...style,
7831
+ },
7348
7832
  'data-position': position,
7349
7833
  onClick: onSelect,
7350
7834
  onMouseDown: onSelect ? (e) => { e.preventDefault(); onSelect(); } : undefined,
@@ -7396,563 +7880,259 @@ function ItemGrid({ items, onItemClick, getItemId = (i) => i.id, getItemTitle =
7396
7880
  }
7397
7881
 
7398
7882
  /**
7399
- * ProductCard one product tile (primitive)
7400
- *
7401
- * Minimal layout: image (via ImageDisplay when imageVariant set), title, price.
7402
- * onClick calls context selectProduct. Overridable via className/style.
7883
+ * Utility functions for Query Suggestions components
7403
7884
  */
7404
- const cardStyle = {
7405
- display: 'flex',
7406
- flexDirection: 'column',
7407
- gap: 8,
7408
- padding: 8,
7409
- cursor: 'pointer',
7410
- border: 'none',
7411
- borderRadius: 'var(--seekora-border-radius, 6px)',
7412
- backgroundColor: 'transparent',
7413
- textAlign: 'left',
7414
- transition: 'background-color 120ms ease',
7885
+ // ============================================================================
7886
+ // Field Extraction
7887
+ // ============================================================================
7888
+ /**
7889
+ * Get nested value from object using dot notation
7890
+ * @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
7891
+ */
7892
+ const getNestedValue = (obj, path) => {
7893
+ if (!obj || !path)
7894
+ return undefined;
7895
+ return path.split('.').reduce((current, key) => {
7896
+ if (current === null || current === undefined)
7897
+ return undefined;
7898
+ return current[key];
7899
+ }, obj);
7415
7900
  };
7416
- const imgStyle = {
7417
- width: '100%',
7418
- aspectRatio: '1',
7419
- objectFit: 'cover',
7420
- borderRadius: 4,
7421
- backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
7901
+ /**
7902
+ * Extract suggestion fields from raw data
7903
+ */
7904
+ const extractSuggestion = (item, mapping = { query: 'query' }) => {
7905
+ return {
7906
+ query: getNestedValue(item, mapping.query) ?? '',
7907
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7908
+ id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
7909
+ categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
7910
+ highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
7911
+ _raw: item,
7912
+ };
7422
7913
  };
7423
- function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', }) {
7424
- const images = product.images?.length
7425
- ? product.images
7426
- : product.image ?? product.imageUrl
7427
- ? [String(product.image ?? product.imageUrl)]
7428
- : [];
7429
- const title = product.title ?? product.name ?? '';
7430
- const price = product.price != null ? (typeof product.price === 'number' ? product.price : Number(product.price)) : null;
7431
- return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
7432
- e.preventDefault();
7433
- onSelect();
7434
- }, "data-position": position, "data-section": section, "data-tab-id": tabId },
7435
- images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image" })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
7436
- React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
7437
- price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
7438
- product.currency ?? '$',
7439
- price.toFixed(2))) : null));
7440
- }
7441
-
7442
7914
  /**
7443
- * ProductGrid – grid of product cards from context (primitive)
7444
- *
7445
- * Uses trendingProducts or active tab products; each click calls context selectProduct.
7915
+ * Extract product fields from raw data
7446
7916
  */
7447
- function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, }) {
7448
- const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
7449
- const products = useMemo(() => {
7450
- if (source === 'trending')
7451
- return trendingProducts;
7452
- const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
7453
- return tab?.products ?? [];
7454
- }, [source, activeTabId, trendingProducts, filteredTabs]);
7455
- const items = products.slice(0, maxItems);
7456
- const navigableItems = getAllNavigableItems();
7457
- const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
7458
- if (items.length === 0)
7459
- return null;
7460
- const gridStyle = {
7461
- display: 'grid',
7462
- gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
7463
- gap: 12,
7464
- padding: 12,
7917
+ const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
7918
+ return {
7919
+ id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
7920
+ title: getNestedValue(item, mapping.title) ?? '',
7921
+ image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7922
+ price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
7923
+ comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
7924
+ url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
7925
+ brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
7926
+ category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
7927
+ rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
7928
+ reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
7929
+ discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
7930
+ inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
7931
+ currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
7932
+ images: mapping.images ? getNestedValue(item, mapping.images) : item?.images,
7933
+ originalPrice: mapping.originalPrice ? getNestedValue(item, mapping.originalPrice) : (item?.original_price ?? item?.compare_at_price),
7934
+ available: mapping.available ? getNestedValue(item, mapping.available) : item?.available,
7935
+ options: mapping.options ? getNestedValue(item, mapping.options) : item?.options,
7936
+ variants: mapping.variants ? getNestedValue(item, mapping.variants) : item?.variants,
7937
+ tags: mapping.tags ? getNestedValue(item, mapping.tags) : item?.tags,
7938
+ _raw: item,
7465
7939
  };
7466
- return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
7467
- React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
7468
- const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
7469
- const section = source === 'trending' ? 'products' : 'filtered_tab';
7470
- const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
7471
- return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId) }));
7472
- }))));
7473
- }
7474
-
7940
+ };
7475
7941
  /**
7476
- * CategoriesTabs horizontal tabs (e.g. filtered tabs) (primitive)
7477
- *
7478
- * Active tab from context; on select updates context and tracks analytics.
7942
+ * Extract category fields from raw data
7479
7943
  */
7480
- function CategoriesTabs({ className, style, tabClassName }) {
7481
- const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
7482
- if (filteredTabs.length === 0)
7483
- return null;
7484
- return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
7485
- display: 'flex',
7486
- gap: 4,
7487
- padding: '8px 12px',
7488
- borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
7489
- overflowX: 'auto',
7490
- ...style,
7491
- }, role: "tablist" }, filteredTabs.map((tab) => {
7492
- const isActive = activeTabId === tab.id;
7493
- return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
7494
- padding: '8px 12px',
7495
- border: 'none',
7496
- borderRadius: 'var(--seekora-border-radius, 6px)',
7497
- backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
7498
- color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
7499
- cursor: 'pointer',
7500
- fontSize: '0.875rem',
7501
- fontWeight: isActive ? 600 : 400,
7502
- whiteSpace: 'nowrap',
7503
- transition: 'background-color 120ms ease',
7504
- }, onClick: () => setActiveTab(tab) }, tab.label));
7505
- })));
7506
- }
7507
-
7944
+ const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
7945
+ return {
7946
+ id: getNestedValue(item, mapping.id) ?? item?.id,
7947
+ label: getNestedValue(item, mapping.label) ?? '',
7948
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7949
+ icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
7950
+ image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7951
+ _raw: item,
7952
+ };
7953
+ };
7508
7954
  /**
7509
- * RecentSearchesList list of recent queries (primitive)
7510
- *
7511
- * Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
7955
+ * Extract brand fields from raw data
7512
7956
  */
7513
- const itemStyle$1 = {
7514
- padding: '10px 12px',
7515
- cursor: 'pointer',
7516
- border: 'none',
7517
- width: '100%',
7518
- textAlign: 'left',
7519
- fontSize: 'inherit',
7520
- fontFamily: 'inherit',
7521
- backgroundColor: 'transparent',
7522
- color: 'var(--seekora-text-primary, #111827)',
7523
- transition: 'background-color 120ms ease',
7957
+ const extractBrand = (item, mapping = { name: 'name' }) => {
7958
+ return {
7959
+ id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
7960
+ name: getNestedValue(item, mapping.name) ?? '',
7961
+ logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
7962
+ count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7963
+ _raw: item,
7964
+ };
7524
7965
  };
7525
- function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
7526
- const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
7527
- const items = recentSearches.slice(0, maxItems);
7528
- if (items.length === 0)
7529
- return null;
7530
- return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
7531
- title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
7532
- React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
7533
- const onSelect = () => selectRecentSearch(search);
7534
- if (renderItem) {
7535
- return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
7536
- }
7537
- return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
7538
- React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
7539
- e.preventDefault();
7540
- onSelect();
7541
- } }, search.query)));
7542
- }))));
7543
- }
7544
-
7966
+ // ============================================================================
7967
+ // Formatting
7968
+ // ============================================================================
7545
7969
  /**
7546
- * TrendingList list of trending searches (primitive)
7547
- *
7548
- * Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
7970
+ * Format price with currency
7549
7971
  */
7550
- const itemStyle = {
7551
- padding: '10px 12px',
7552
- cursor: 'pointer',
7553
- border: 'none',
7554
- width: '100%',
7555
- textAlign: 'left',
7556
- fontSize: 'inherit',
7557
- fontFamily: 'inherit',
7558
- backgroundColor: 'transparent',
7559
- color: 'var(--seekora-text-primary, #111827)',
7560
- transition: 'background-color 120ms ease',
7972
+ const formatPrice = (value, config = {}) => {
7973
+ if (value === undefined || value === null)
7974
+ return '';
7975
+ const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
7976
+ const num = typeof value === 'string' ? parseFloat(value) : value;
7977
+ if (isNaN(num))
7978
+ return String(value);
7979
+ const formatted = num.toLocaleString(undefined, {
7980
+ minimumFractionDigits: priceDecimals,
7981
+ maximumFractionDigits: priceDecimals,
7982
+ });
7983
+ return currencyPosition === 'before'
7984
+ ? `${currency}${formatted}`
7985
+ : `${formatted}${currency}`;
7561
7986
  };
7562
- function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
7563
- const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
7564
- const items = trendingSearches.slice(0, maxItems);
7565
- if (items.length === 0)
7566
- return null;
7567
- return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
7568
- title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
7569
- React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
7570
- const onSelect = () => selectTrendingSearch(trending, i);
7571
- if (renderItem) {
7572
- return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
7573
- }
7574
- return (React.createElement("li", { key: `${trending.query}-${i}` },
7575
- React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
7576
- e.preventDefault();
7577
- onSelect();
7578
- } },
7579
- trending.query,
7580
- trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
7581
- }))));
7582
- }
7583
-
7584
7987
  /**
7585
- * SuggestionsError error message (primitive)
7988
+ * Calculate discount percentage
7586
7989
  */
7587
- function SuggestionsError({ className, style, render }) {
7588
- const { error } = useSuggestionsContext();
7589
- if (!error)
7590
- return null;
7591
- if (render)
7592
- return React.createElement(React.Fragment, null, render(error));
7593
- return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
7594
- padding: 16,
7595
- color: 'var(--seekora-error, #dc2626)',
7596
- fontSize: '0.875rem',
7597
- ...style,
7598
- } }, error.message));
7599
- }
7600
-
7990
+ const calculateDiscount = (price, comparePrice) => {
7991
+ if (!price || !comparePrice || comparePrice <= price)
7992
+ return undefined;
7993
+ return Math.round(((comparePrice - price) / comparePrice) * 100);
7994
+ };
7995
+ // ============================================================================
7996
+ // Text Processing
7997
+ // ============================================================================
7601
7998
  /**
7602
- * SuggestionsDropdownComposition reference composition
7603
- *
7604
- * Example layout built from primitives: SearchInput + DropdownPanel containing
7605
- * RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
7606
- * Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
7607
- * with your own arrangement of the same primitives.
7999
+ * Escape HTML special characters
7608
8000
  */
7609
- function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
7610
- return (React.createElement(SuggestionsProvider, { ...providerProps },
7611
- React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
7612
- React.createElement(SearchInput, { placeholder: placeholder }),
7613
- React.createElement(DropdownPanel, null,
7614
- React.createElement(SuggestionsError, null),
7615
- React.createElement(SuggestionsLoading, null),
7616
- showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
7617
- React.createElement(SuggestionList, null),
7618
- showTabs ? React.createElement(CategoriesTabs, null) : null,
7619
- showProducts ? React.createElement(ProductGrid, null) : null,
7620
- showTrending ? React.createElement(TrendingList, null) : null))));
7621
- }
7622
-
8001
+ const escapeHtml = (text) => {
8002
+ const map = {
8003
+ '&': '&amp;',
8004
+ '<': '&lt;',
8005
+ '>': '&gt;',
8006
+ '"': '&quot;',
8007
+ "'": '&#39;',
8008
+ };
8009
+ return text.replace(/[&<>"']/g, (char) => map[char]);
8010
+ };
7623
8011
  /**
7624
- * SectionSearchContext preset query/filter section state
7625
- *
7626
- * For menus, sidebar, front-page blocks. Independent of main search state.
8012
+ * Highlight matching text in a string
7627
8013
  */
7628
- const SectionSearchContext = createContext(null);
7629
- function useSectionSearchContext() {
7630
- const context = useContext(SectionSearchContext);
7631
- if (!context) {
7632
- const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
7633
- log.error('SectionSearchContext: not available', { error: error.message });
7634
- throw error;
7635
- }
7636
- return context;
7637
- }
7638
-
8014
+ const highlightText = (text, query, options = {}) => {
8015
+ if (!query || !text)
8016
+ return escapeHtml(text);
8017
+ const { tag = 'mark', className = '', style } = options;
8018
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8019
+ const styleAttr = style
8020
+ ? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
8021
+ : '';
8022
+ const classAttr = className ? ` class="${className}"` : '';
8023
+ return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
8024
+ };
8025
+ // ============================================================================
8026
+ // Variant Utilities
8027
+ // ============================================================================
7639
8028
  /**
7640
- * SectionSearchProvider preset query + filter section
7641
- *
7642
- * Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
7643
- * query/filters change. Does not use global SearchStateManager. Use for menus,
7644
- * sidebar, front-page blocks (e.g. "New arrivals", "On sale").
8029
+ * Extract badges from product tags (Shopify convention: "badge:new", "badge:limited")
8030
+ * and auto-generate sale/soldOut badges from product data.
7645
8031
  */
7646
- function extractItems(response) {
7647
- if (!response)
7648
- return [];
7649
- if (Array.isArray(response.results))
7650
- return response.results;
7651
- if (Array.isArray(response.hits))
7652
- return response.hits;
7653
- const data = response.data;
7654
- if (data && Array.isArray(data.results))
7655
- return data.results;
7656
- if (data && Array.isArray(data.data))
7657
- return data.data;
7658
- return [];
7659
- }
7660
- function extractTotal(response) {
7661
- if (!response)
7662
- return 0;
7663
- const n = response.totalResults ?? response.total ?? response.total_results;
7664
- if (typeof n === 'number')
7665
- return n;
7666
- const data = response.data;
7667
- if (data?.total_results != null)
7668
- return Number(data.total_results);
7669
- if (data?.data?.total_results != null)
7670
- return Number(data.data.total_results);
7671
- return 0;
7672
- }
7673
- function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
7674
- const { client } = useSearchContext();
7675
- const [items, setItems] = useState([]);
7676
- const [loading, setLoading] = useState(true);
7677
- const [error, setError] = useState(null);
7678
- const [totalCount, setTotalCount] = useState(0);
7679
- useEffect(() => {
7680
- if (!enabled || !client?.search) {
7681
- setItems([]);
7682
- setLoading(false);
7683
- setError(null);
7684
- setTotalCount(0);
7685
- return;
8032
+ const extractBadges = (tags, product) => {
8033
+ const badges = [];
8034
+ // Extract from tags (Shopify convention)
8035
+ if (tags) {
8036
+ for (const tag of tags) {
8037
+ const lower = tag.toLowerCase().trim();
8038
+ if (lower.startsWith('badge:') || lower.startsWith('badge: ')) {
8039
+ const text = tag.slice(tag.indexOf(':') + 1).trim();
8040
+ if (text) {
8041
+ badges.push({ text, type: 'custom' });
8042
+ }
8043
+ }
8044
+ else if (lower === 'new' || lower === 'new arrival') {
8045
+ badges.push({ text: 'New', type: 'new' });
8046
+ }
8047
+ else if (lower === 'limited' || lower === 'limited edition') {
8048
+ badges.push({ text: 'Limited', type: 'limited' });
8049
+ }
7686
8050
  }
7687
- let cancelled = false;
7688
- setLoading(true);
7689
- setError(null);
7690
- const options = {
7691
- per_page: maxItems,
7692
- page: 1,
7693
- };
7694
- if (sortBy)
7695
- options.sort_by = sortBy;
7696
- if (refinements.length > 0) {
7697
- options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
8051
+ }
8052
+ // Auto-generate sale badge
8053
+ if (product) {
8054
+ const comparePrice = product.original_price ?? product.compare_at_price;
8055
+ if (comparePrice && product.price && comparePrice > product.price) {
8056
+ const discount = Math.round(((comparePrice - product.price) / comparePrice) * 100);
8057
+ badges.push({ text: `${discount}% Off`, type: 'sale' });
7698
8058
  }
7699
- client
7700
- .search(query, options)
7701
- .then((response) => {
7702
- if (cancelled)
7703
- return;
7704
- setItems(extractItems(response));
7705
- setTotalCount(extractTotal(response));
7706
- setLoading(false);
7707
- })
7708
- .catch((err) => {
7709
- if (cancelled)
7710
- return;
7711
- setError(err instanceof Error ? err : new Error(String(err)));
7712
- setItems([]);
7713
- setLoading(false);
7714
- });
7715
- return () => {
7716
- cancelled = true;
7717
- };
7718
- }, [client, enabled, query, maxItems, sortBy, refinements]);
7719
- const trackClick = useCallback((item, position) => {
7720
- if (!client?.trackEvent)
7721
- return;
7722
- const id = item?.id ?? item?.objectID;
7723
- client.trackEvent({
7724
- event_name: 'section_result_click',
7725
- clicked_item_id: id,
7726
- position,
7727
- section: sectionId,
7728
- metadata: { section_id: sectionId },
7729
- }, undefined);
7730
- }, [client, sectionId]);
7731
- const value = useMemo(() => ({
7732
- items,
7733
- loading,
7734
- error,
7735
- totalCount,
7736
- sectionId,
7737
- trackClick,
7738
- }), [items, loading, error, totalCount, sectionId, trackClick]);
7739
- return React.createElement(SectionSearchContext.Provider, { value: value }, children);
7740
- }
7741
-
8059
+ }
8060
+ // Auto-generate sold out badge
8061
+ if (product && product.available === false) {
8062
+ badges.push({ text: 'Sold Out', type: 'soldOut' });
8063
+ }
8064
+ return badges;
8065
+ };
7742
8066
  /**
7743
- * SectionLoading loading state for section (primitive)
8067
+ * Compute min/max price from variants. Returns null if all variants have the same price.
7744
8068
  */
7745
- function SectionLoading({ className, style, text = 'Loading...' }) {
7746
- const { loading } = useSectionSearchContext();
7747
- if (!loading)
8069
+ const getPriceRange = (variants) => {
8070
+ if (!variants || variants.length === 0)
7748
8071
  return null;
7749
- return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
7750
- }
7751
-
7752
- /**
7753
- * SectionError – error state for section (primitive)
7754
- */
7755
- function SectionError({ className, style, render }) {
7756
- const { error } = useSectionSearchContext();
7757
- if (!error)
8072
+ const prices = variants
8073
+ .map((v) => v.price)
8074
+ .filter((p) => p != null && !isNaN(p));
8075
+ if (prices.length === 0)
7758
8076
  return null;
7759
- if (render)
7760
- return React.createElement(React.Fragment, null, render(error));
7761
- return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
7762
- }
7763
-
7764
- /**
7765
- * SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
7766
- */
7767
- function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
7768
- const { items, loading, error, trackClick } = useSectionSearchContext();
7769
- if (loading)
7770
- return React.createElement(SectionLoading, { className: className, style: style });
7771
- if (error)
7772
- return React.createElement(SectionError, { className: className, style: style });
7773
- return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
7774
- }
7775
-
7776
- /**
7777
- * ProductGallery – product detail image gallery (primitive)
7778
- *
7779
- * Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
7780
- * For use on individual product page.
7781
- */
7782
- function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
7783
- return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
7784
- React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
7785
- }
7786
-
7787
- /**
7788
- * ProductInfo – product detail block (primitive)
7789
- *
7790
- * Title, description, price, optional variant selector and CTA. Minimal layout;
7791
- * override with className/style. For use on individual product page.
7792
- */
7793
- function ProductInfo({ title, description, price, currency = '$', renderVariantSelector, renderCTA, className, style, }) {
7794
- const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
7795
- return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
7796
- React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
7797
- priceNum != null && !Number.isNaN(priceNum) ? (React.createElement("span", { className: "seekora-product-info-price", style: { fontSize: '1.125rem', fontWeight: 600 } },
7798
- currency,
7799
- priceNum.toFixed(2))) : null,
7800
- description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
7801
- renderVariantSelector?.(),
7802
- renderCTA?.()));
7803
- }
7804
-
7805
- /**
7806
- * ProductRecommendations – related / frequently bought (primitive)
7807
- *
7808
- * Renders a section of recommended items (generic ItemGrid or product list).
7809
- * Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
7810
- * For use on individual product page.
7811
- */
7812
- function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
7813
- if (!items?.length)
8077
+ const min = Math.min(...prices);
8078
+ const max = Math.max(...prices);
8079
+ if (min === max)
7814
8080
  return null;
7815
- return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
7816
- React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
7817
- React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
7818
- }
7819
-
7820
- /**
7821
- * Utility functions for Query Suggestions components
7822
- */
7823
- // ============================================================================
7824
- // Field Extraction
7825
- // ============================================================================
7826
- /**
7827
- * Get nested value from object using dot notation
7828
- * @example getNestedValue({ a: { b: 'value' } }, 'a.b') => 'value'
7829
- */
7830
- const getNestedValue = (obj, path) => {
7831
- if (!obj || !path)
7832
- return undefined;
7833
- return path.split('.').reduce((current, key) => {
7834
- if (current === null || current === undefined)
7835
- return undefined;
7836
- return current[key];
7837
- }, obj);
7838
- };
7839
- /**
7840
- * Extract suggestion fields from raw data
7841
- */
7842
- const extractSuggestion = (item, mapping = { query: 'query' }) => {
7843
- return {
7844
- query: getNestedValue(item, mapping.query) ?? '',
7845
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7846
- id: mapping.id ? getNestedValue(item, mapping.id) : item?.objectID || item?.id,
7847
- categories: mapping.categories ? getNestedValue(item, mapping.categories) : undefined,
7848
- highlighted: mapping.highlighted ? getNestedValue(item, mapping.highlighted) : undefined,
7849
- _raw: item,
7850
- };
7851
- };
7852
- /**
7853
- * Extract product fields from raw data
7854
- */
7855
- const extractProduct = (item, mapping = { id: 'id', title: 'title' }) => {
7856
- return {
7857
- id: getNestedValue(item, mapping.id) ?? item?.objectID ?? item?.id,
7858
- title: getNestedValue(item, mapping.title) ?? '',
7859
- image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7860
- price: mapping.price ? getNestedValue(item, mapping.price) : undefined,
7861
- comparePrice: mapping.comparePrice ? getNestedValue(item, mapping.comparePrice) : undefined,
7862
- url: mapping.url ? getNestedValue(item, mapping.url) : undefined,
7863
- brand: mapping.brand ? getNestedValue(item, mapping.brand) : undefined,
7864
- category: mapping.category ? getNestedValue(item, mapping.category) : undefined,
7865
- rating: mapping.rating ? getNestedValue(item, mapping.rating) : undefined,
7866
- reviewCount: mapping.reviewCount ? getNestedValue(item, mapping.reviewCount) : undefined,
7867
- discount: mapping.discount ? getNestedValue(item, mapping.discount) : undefined,
7868
- inStock: mapping.inStock ? getNestedValue(item, mapping.inStock) : undefined,
7869
- currency: mapping.currency ? getNestedValue(item, mapping.currency) : undefined,
7870
- _raw: item,
7871
- };
7872
- };
7873
- /**
7874
- * Extract category fields from raw data
7875
- */
7876
- const extractCategory = (item, mapping = { id: 'id', label: 'label' }) => {
7877
- return {
7878
- id: getNestedValue(item, mapping.id) ?? item?.id,
7879
- label: getNestedValue(item, mapping.label) ?? '',
7880
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7881
- icon: mapping.icon ? getNestedValue(item, mapping.icon) : undefined,
7882
- image: mapping.image ? getNestedValue(item, mapping.image) : undefined,
7883
- _raw: item,
7884
- };
8081
+ return { min, max };
7885
8082
  };
7886
8083
  /**
7887
- * Extract brand fields from raw data
8084
+ * Format a price range like "$54.00 - $72.00"
7888
8085
  */
7889
- const extractBrand = (item, mapping = { name: 'name' }) => {
7890
- return {
7891
- id: mapping.id ? getNestedValue(item, mapping.id) : item?.id,
7892
- name: getNestedValue(item, mapping.name) ?? '',
7893
- logo: mapping.logo ? getNestedValue(item, mapping.logo) : undefined,
7894
- count: mapping.count ? getNestedValue(item, mapping.count) : undefined,
7895
- _raw: item,
7896
- };
8086
+ const formatPriceRange = (range, config = {}) => {
8087
+ return `${formatPrice(range.min, config)} - ${formatPrice(range.max, config)}`;
7897
8088
  };
7898
- // ============================================================================
7899
- // Formatting
7900
- // ============================================================================
7901
8089
  /**
7902
- * Format price with currency
8090
+ * Given current selections, return which values for an option are still available
8091
+ * based on variant availability.
7903
8092
  */
7904
- const formatPrice = (value, config = {}) => {
7905
- if (value === undefined || value === null)
7906
- return '';
7907
- const { currency = '$', currencyPosition = 'before', priceDecimals = 2 } = config;
7908
- const num = typeof value === 'string' ? parseFloat(value) : value;
7909
- if (isNaN(num))
7910
- return String(value);
7911
- const formatted = num.toLocaleString(undefined, {
7912
- minimumFractionDigits: priceDecimals,
7913
- maximumFractionDigits: priceDecimals,
8093
+ const getAvailableValuesForOption = (optionName, options, variants, selections) => {
8094
+ const option = options.find((o) => o.name === optionName);
8095
+ if (!option)
8096
+ return [];
8097
+ const optionIndex = options.indexOf(option);
8098
+ const optionKey = `option${optionIndex + 1}`;
8099
+ return option.values.map((value) => {
8100
+ // Check if any variant with this value and current other selections is available
8101
+ const available = variants.some((variant) => {
8102
+ if (variant[optionKey] !== value)
8103
+ return false;
8104
+ if (variant.available === false)
8105
+ return false;
8106
+ // Check other selections match
8107
+ for (const [selName, selValue] of Object.entries(selections)) {
8108
+ if (selName === optionName)
8109
+ continue;
8110
+ const selOption = options.find((o) => o.name === selName);
8111
+ if (!selOption)
8112
+ continue;
8113
+ const selIdx = options.indexOf(selOption);
8114
+ const selKey = `option${selIdx + 1}`;
8115
+ if (variant[selKey] !== selValue)
8116
+ return false;
8117
+ }
8118
+ return true;
8119
+ });
8120
+ return { value, available };
7914
8121
  });
7915
- return currencyPosition === 'before'
7916
- ? `${currency}${formatted}`
7917
- : `${formatted}${currency}`;
7918
- };
7919
- /**
7920
- * Calculate discount percentage
7921
- */
7922
- const calculateDiscount = (price, comparePrice) => {
7923
- if (!price || !comparePrice || comparePrice <= price)
7924
- return undefined;
7925
- return Math.round(((comparePrice - price) / comparePrice) * 100);
7926
- };
7927
- // ============================================================================
7928
- // Text Processing
7929
- // ============================================================================
7930
- /**
7931
- * Escape HTML special characters
7932
- */
7933
- const escapeHtml = (text) => {
7934
- const map = {
7935
- '&': '&amp;',
7936
- '<': '&lt;',
7937
- '>': '&gt;',
7938
- '"': '&quot;',
7939
- "'": '&#39;',
7940
- };
7941
- return text.replace(/[&<>"']/g, (char) => map[char]);
7942
8122
  };
7943
8123
  /**
7944
- * Highlight matching text in a string
8124
+ * Find the exact variant matching all selected options.
7945
8125
  */
7946
- const highlightText = (text, query, options = {}) => {
7947
- if (!query || !text)
7948
- return escapeHtml(text);
7949
- const { tag = 'mark', className = '', style } = options;
7950
- const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7951
- const styleAttr = style
7952
- ? ` style="${Object.entries(style).map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`).join(';')}"`
7953
- : '';
7954
- const classAttr = className ? ` class="${className}"` : '';
7955
- return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
8126
+ const findVariantBySelections = (options, variants, selections) => {
8127
+ return variants.find((variant) => {
8128
+ return options.every((option, idx) => {
8129
+ const key = `option${idx + 1}`;
8130
+ const selected = selections[option.name];
8131
+ if (!selected)
8132
+ return true; // not yet selected
8133
+ return variant[key] === selected;
8134
+ });
8135
+ }) ?? null;
7956
8136
  };
7957
8137
  // ============================================================================
7958
8138
  // Theme & Styling
@@ -8111,80 +8291,1409 @@ class SuggestionsCache {
8111
8291
  this.cache.delete(key);
8112
8292
  return null;
8113
8293
  }
8114
- return entry.data;
8115
- }
8116
- /**
8117
- * Store data in cache
8118
- */
8119
- set(key, data, ttlMs) {
8120
- // Evict oldest entries if at max size
8121
- if (this.cache.size >= this.maxSize) {
8122
- const oldestKey = this.cache.keys().next().value;
8123
- if (oldestKey)
8124
- this.cache.delete(oldestKey);
8294
+ return entry.data;
8295
+ }
8296
+ /**
8297
+ * Store data in cache
8298
+ */
8299
+ set(key, data, ttlMs) {
8300
+ // Evict oldest entries if at max size
8301
+ if (this.cache.size >= this.maxSize) {
8302
+ const oldestKey = this.cache.keys().next().value;
8303
+ if (oldestKey)
8304
+ this.cache.delete(oldestKey);
8305
+ }
8306
+ this.cache.set(key, {
8307
+ data,
8308
+ timestamp: Date.now(),
8309
+ ttl: ttlMs ?? this.defaultTtl,
8310
+ });
8311
+ }
8312
+ /**
8313
+ * Check if key exists and is valid
8314
+ */
8315
+ has(key) {
8316
+ return this.get(key) !== null;
8317
+ }
8318
+ /**
8319
+ * Clear all cached entries
8320
+ */
8321
+ clear() {
8322
+ this.cache.clear();
8323
+ }
8324
+ /**
8325
+ * Clear expired entries
8326
+ */
8327
+ cleanup() {
8328
+ const now = Date.now();
8329
+ for (const [key, entry] of this.cache.entries()) {
8330
+ if (now - entry.timestamp > entry.ttl) {
8331
+ this.cache.delete(key);
8332
+ }
8333
+ }
8334
+ }
8335
+ /**
8336
+ * Get cache statistics
8337
+ */
8338
+ getStats() {
8339
+ return {
8340
+ size: this.cache.size,
8341
+ maxSize: this.maxSize,
8342
+ };
8343
+ }
8344
+ }
8345
+ // Global cache instance for suggestions (shared across components)
8346
+ let globalSuggestionsCache = null;
8347
+ /**
8348
+ * Get the global suggestions cache instance
8349
+ */
8350
+ const getSuggestionsCache = (options) => {
8351
+ if (!globalSuggestionsCache) {
8352
+ globalSuggestionsCache = new SuggestionsCache(options);
8353
+ }
8354
+ return globalSuggestionsCache;
8355
+ };
8356
+ /**
8357
+ * Create a new cache instance (for isolated caching per component)
8358
+ */
8359
+ const createSuggestionsCache = (options) => {
8360
+ return new SuggestionsCache(options);
8361
+ };
8362
+ /**
8363
+ * Clear the global cache
8364
+ */
8365
+ const clearSuggestionsCache = () => {
8366
+ globalSuggestionsCache?.clear();
8367
+ };
8368
+
8369
+ /**
8370
+ * PriceDisplay – reusable price display primitive
8371
+ *
8372
+ * Handles single price, compare price with strikethrough, discount percentage,
8373
+ * and price range display.
8374
+ */
8375
+ const formatNum = (value, currency, position, decimals) => {
8376
+ const formatted = value.toLocaleString(undefined, {
8377
+ minimumFractionDigits: decimals,
8378
+ maximumFractionDigits: decimals,
8379
+ });
8380
+ return position === 'before' ? `${currency}${formatted}` : `${formatted}${currency}`;
8381
+ };
8382
+ function PriceDisplay({ price, comparePrice, priceRange, currency = '$', currencyPosition = 'before', priceDecimals = 2, showDiscount = true, className, style, }) {
8383
+ const fmt = (v) => formatNum(v, currency, currencyPosition, priceDecimals);
8384
+ // Price range mode
8385
+ if (priceRange) {
8386
+ return (React.createElement("span", { className: clsx('seekora-price-display', 'seekora-price-range', className), style: style },
8387
+ fmt(priceRange.min),
8388
+ " \u2013 ",
8389
+ fmt(priceRange.max)));
8390
+ }
8391
+ if (price == null)
8392
+ return null;
8393
+ const hasCompare = comparePrice != null && comparePrice > price;
8394
+ const discount = hasCompare ? Math.round(((comparePrice - price) / comparePrice) * 100) : 0;
8395
+ return (React.createElement("span", { className: clsx('seekora-price-display', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', ...style } },
8396
+ React.createElement("span", { className: "seekora-price-current", style: { fontWeight: 600 } }, fmt(price)),
8397
+ hasCompare && (React.createElement("span", { className: "seekora-price-compare", style: {
8398
+ textDecoration: 'line-through',
8399
+ color: 'var(--seekora-text-secondary, #6b7280)',
8400
+ fontSize: '0.875em',
8401
+ } }, fmt(comparePrice))),
8402
+ hasCompare && showDiscount && discount > 0 && (React.createElement("span", { className: "seekora-price-discount", style: {
8403
+ color: 'var(--seekora-error, #ef4444)',
8404
+ fontSize: '0.8125em',
8405
+ fontWeight: 600,
8406
+ } },
8407
+ "-",
8408
+ discount,
8409
+ "%"))));
8410
+ }
8411
+
8412
+ /**
8413
+ * BadgeList – renders product badges (sale, new, sold out, custom)
8414
+ */
8415
+ const positionStyles = {
8416
+ 'top-left': { position: 'absolute', top: 6, left: 6 },
8417
+ 'top-right': { position: 'absolute', top: 6, right: 6 },
8418
+ 'bottom-left': { position: 'absolute', bottom: 6, left: 6 },
8419
+ 'bottom-right': { position: 'absolute', bottom: 6, right: 6 },
8420
+ inline: {},
8421
+ };
8422
+ const typeColors = {
8423
+ sale: { bg: '#ef4444', text: '#fff' },
8424
+ new: { bg: '#3b82f6', text: '#fff' },
8425
+ soldOut: { bg: '#6b7280', text: '#fff' },
8426
+ limited: { bg: '#f59e0b', text: '#fff' },
8427
+ custom: { bg: '#111827', text: '#fff' },
8428
+ };
8429
+ function BadgeList({ badges, maxBadges, position = 'top-left', className, style, }) {
8430
+ if (!badges || badges.length === 0)
8431
+ return null;
8432
+ const visible = maxBadges ? badges.slice(0, maxBadges) : badges;
8433
+ return (React.createElement("div", { className: clsx('seekora-badge-list', className), style: {
8434
+ display: 'flex',
8435
+ flexWrap: 'wrap',
8436
+ gap: 4,
8437
+ zIndex: 1,
8438
+ ...positionStyles[position],
8439
+ ...style,
8440
+ } }, visible.map((badge, i) => {
8441
+ const colors = typeColors[badge.type ?? 'custom'];
8442
+ return (React.createElement("span", { key: `${badge.text}-${i}`, className: clsx('seekora-badge', badge.type && `seekora-badge--${badge.type === 'soldOut' ? 'sold-out' : badge.type}`), style: {
8443
+ display: 'inline-block',
8444
+ padding: '2px 8px',
8445
+ borderRadius: 4,
8446
+ fontSize: '0.6875rem',
8447
+ fontWeight: 600,
8448
+ lineHeight: 1.4,
8449
+ backgroundColor: badge.color ?? colors.bg,
8450
+ color: badge.textColor ?? colors.text,
8451
+ whiteSpace: 'nowrap',
8452
+ } }, badge.text));
8453
+ })));
8454
+ }
8455
+
8456
+ /**
8457
+ * RatingDisplay – star rating display with review count
8458
+ *
8459
+ * Supports multiple variants: stars-only, compact, full, inline.
8460
+ * Can be read-only (display) or interactive (for reviews).
8461
+ */
8462
+ const sizeMap = {
8463
+ small: 14,
8464
+ medium: 18,
8465
+ large: 24,
8466
+ };
8467
+ const fontSizeMap = {
8468
+ small: '0.75rem',
8469
+ medium: '0.875rem',
8470
+ large: '1rem',
8471
+ };
8472
+ function StarIcon({ filled, half, size, color, emptyColor, interactive, onHover, onClick, }) {
8473
+ if (half) {
8474
+ return (React.createElement("span", { className: clsx('seekora-rating-star', 'seekora-rating-star--half', interactive && 'seekora-rating-star--interactive'), style: {
8475
+ position: 'relative',
8476
+ display: 'inline-block',
8477
+ width: size,
8478
+ height: size,
8479
+ cursor: interactive ? 'pointer' : 'default',
8480
+ }, onMouseEnter: onHover, onClick: onClick },
8481
+ React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: { position: 'absolute', top: 0, left: 0 } },
8482
+ React.createElement("defs", null,
8483
+ React.createElement("linearGradient", { id: "half-fill" },
8484
+ React.createElement("stop", { offset: "50%", stopColor: color }),
8485
+ React.createElement("stop", { offset: "50%", stopColor: emptyColor }))),
8486
+ React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", fill: "url(#half-fill)", stroke: color, strokeWidth: "1" }))));
8487
+ }
8488
+ return (React.createElement("span", { className: clsx('seekora-rating-star', filled ? 'seekora-rating-star--filled' : 'seekora-rating-star--empty', interactive && 'seekora-rating-star--interactive'), style: {
8489
+ display: 'inline-block',
8490
+ width: size,
8491
+ height: size,
8492
+ cursor: interactive ? 'pointer' : 'default',
8493
+ }, onMouseEnter: onHover, onClick: onClick },
8494
+ React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: filled ? color : 'none', xmlns: "http://www.w3.org/2000/svg" },
8495
+ React.createElement("path", { d: "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z", stroke: filled ? color : emptyColor, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))));
8496
+ }
8497
+ function RatingDisplay({ rating, reviewCount, variant = 'compact', size = 'medium', maxRating = 5, showNumeric = false, showHalfStars = true, interactive = false, onRatingChange, starColor = '#f59e0b', emptyStarColor = '#d1d5db', textColor = 'var(--seekora-text-secondary, #6b7280)', showReviewCount = true, reviewCountFormat, className, style, }) {
8498
+ const [hoverRating, setHoverRating] = useState(null);
8499
+ const clampedRating = Math.max(0, Math.min(maxRating, rating));
8500
+ const displayRating = interactive && hoverRating !== null ? hoverRating : clampedRating;
8501
+ const starSize = sizeMap[size];
8502
+ const fontSize = fontSizeMap[size];
8503
+ const formatReviewCount = (count) => {
8504
+ if (reviewCountFormat)
8505
+ return reviewCountFormat(count);
8506
+ if (count >= 1000000)
8507
+ return `${(count / 1000000).toFixed(1)}M`;
8508
+ if (count >= 1000)
8509
+ return `${(count / 1000).toFixed(1)}K`;
8510
+ return count.toString();
8511
+ };
8512
+ const renderStars = () => {
8513
+ const stars = [];
8514
+ for (let i = 1; i <= maxRating; i++) {
8515
+ const filled = i <= Math.floor(displayRating);
8516
+ const half = showHalfStars && i === Math.ceil(displayRating) && displayRating % 1 >= 0.25 && displayRating % 1 < 0.75;
8517
+ stars.push(React.createElement(StarIcon, { key: i, filled: filled, half: half, size: starSize, color: starColor, emptyColor: emptyStarColor, interactive: interactive, onHover: interactive ? () => setHoverRating(i) : undefined, onClick: interactive
8518
+ ? () => {
8519
+ setHoverRating(null);
8520
+ onRatingChange?.(i);
8521
+ }
8522
+ : undefined }));
8523
+ }
8524
+ return stars;
8525
+ };
8526
+ const handleMouseLeave = () => {
8527
+ if (interactive) {
8528
+ setHoverRating(null);
8529
+ }
8530
+ };
8531
+ if (variant === 'stars-only') {
8532
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--stars-only', className), style: { display: 'inline-flex', alignItems: 'center', gap: 2, ...style }, onMouseLeave: handleMouseLeave }, renderStars()));
8533
+ }
8534
+ if (variant === 'compact') {
8535
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--compact', className), style: { display: 'inline-flex', alignItems: 'center', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8536
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8537
+ showNumeric && (React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: textColor } }, clampedRating.toFixed(1))),
8538
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
8539
+ "(",
8540
+ formatReviewCount(reviewCount),
8541
+ ")"))));
8542
+ }
8543
+ if (variant === 'full') {
8544
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--full', className), style: { display: 'flex', flexDirection: 'column', gap: 4, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8545
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: 6 } },
8546
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8547
+ React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
8548
+ React.createElement("span", { className: "seekora-rating-max", style: { color: textColor } },
8549
+ "/ ",
8550
+ maxRating)),
8551
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-text", style: { fontSize: '0.875em', color: textColor } },
8552
+ "Based on ",
8553
+ formatReviewCount(reviewCount),
8554
+ " ",
8555
+ reviewCount === 1 ? 'review' : 'reviews'))));
8556
+ }
8557
+ if (variant === 'inline') {
8558
+ return (React.createElement("div", { className: clsx('seekora-rating-display', 'seekora-rating-display--inline', className), style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize, ...style }, onMouseLeave: handleMouseLeave },
8559
+ React.createElement("span", { className: "seekora-rating-numeric", style: { fontWeight: 600, color: 'var(--seekora-text-primary, #111827)' } }, clampedRating.toFixed(1)),
8560
+ React.createElement("div", { style: { display: 'inline-flex', alignItems: 'center', gap: 2 } }, renderStars()),
8561
+ showReviewCount && reviewCount != null && reviewCount > 0 && (React.createElement("span", { className: "seekora-rating-review-count", style: { color: textColor } },
8562
+ "(",
8563
+ formatReviewCount(reviewCount),
8564
+ ")"))));
8565
+ }
8566
+ return null;
8567
+ }
8568
+
8569
+ /**
8570
+ * VariantSwatches – inline variant indicators for card display
8571
+ *
8572
+ * Shows color dots, size labels, and "+N more" overflow for compact card views.
8573
+ */
8574
+ const COLOR_NAMES$1 = {
8575
+ black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
8576
+ green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
8577
+ pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
8578
+ navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
8579
+ khaki: '#c3b091', olive: '#808000', teal: '#0d9488', coral: '#ff7f50',
8580
+ maroon: '#800000', tan: '#d2b48c', charcoal: '#36454f', burgundy: '#800020',
8581
+ sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
8582
+ plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
8583
+ silver: '#c0c0c0', rose: '#ff007f', mauve: '#e0b0ff', wine: '#722f37',
8584
+ raven: '#0a0a0a', natural: '#f5f0e1', bone: '#e3dac9', sand: '#c2b280',
8585
+ };
8586
+ const isColorOption$1 = (name) => {
8587
+ const lower = name.toLowerCase();
8588
+ return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
8589
+ };
8590
+ const resolveColor$1 = (value, colorMap) => {
8591
+ if (colorMap?.[value])
8592
+ return colorMap[value];
8593
+ const lower = value.toLowerCase();
8594
+ if (colorMap?.[lower])
8595
+ return colorMap[lower];
8596
+ return COLOR_NAMES$1[lower] ?? null;
8597
+ };
8598
+ const getAvailability$1 = (optionName, value, options, variants, selectedValues) => {
8599
+ if (!variants || variants.length === 0)
8600
+ return true;
8601
+ const optionIndex = options.findIndex((o) => o.name === optionName);
8602
+ if (optionIndex === -1)
8603
+ return true;
8604
+ const optionKey = `option${optionIndex + 1}`;
8605
+ return variants.some((variant) => {
8606
+ // Must match the current option value
8607
+ if (variant[optionKey] !== value)
8608
+ return false;
8609
+ // Must be available
8610
+ if (variant.available === false)
8611
+ return false;
8612
+ // Must match all other selected values
8613
+ if (selectedValues) {
8614
+ for (const [selName, selValue] of Object.entries(selectedValues)) {
8615
+ if (selName === optionName)
8616
+ continue; // Skip current option
8617
+ const selIdx = options.findIndex((o) => o.name === selName);
8618
+ if (selIdx === -1)
8619
+ continue;
8620
+ const selKey = `option${selIdx + 1}`;
8621
+ if (variant[selKey] !== selValue)
8622
+ return false;
8623
+ }
8624
+ }
8625
+ return true;
8626
+ });
8627
+ };
8628
+ function VariantSwatches({ options, visibleOptions, maxValues = 5, colorMap, selectedValues, variants, showAvailability = true, onSwatchHover, onSwatchClick, className, style, }) {
8629
+ const [expandedOptions, setExpandedOptions] = useState(new Set());
8630
+ if (!options || options.length === 0)
8631
+ return null;
8632
+ const filtered = visibleOptions
8633
+ ? options.filter((o) => visibleOptions.includes(o.name))
8634
+ : options;
8635
+ if (filtered.length === 0)
8636
+ return null;
8637
+ const toggleExpanded = (optionName, e) => {
8638
+ e.stopPropagation();
8639
+ setExpandedOptions((prev) => {
8640
+ const next = new Set(prev);
8641
+ if (next.has(optionName)) {
8642
+ next.delete(optionName);
8643
+ }
8644
+ else {
8645
+ next.add(optionName);
8646
+ }
8647
+ return next;
8648
+ });
8649
+ };
8650
+ return (React.createElement("div", { className: clsx('seekora-variant-swatches', className), style: { display: 'flex', flexDirection: 'column', gap: 4, ...style } }, filtered.map((option) => {
8651
+ const isColor = isColorOption$1(option.name);
8652
+ const isExpanded = expandedOptions.has(option.name);
8653
+ const visible = isExpanded ? option.values : option.values.slice(0, maxValues);
8654
+ const overflow = option.values.length - maxValues;
8655
+ const hasOverflow = overflow > 0;
8656
+ const selectedValue = selectedValues?.[option.name];
8657
+ return (React.createElement("div", { key: option.name, className: "seekora-variant-swatch-group", style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
8658
+ visible.map((value) => {
8659
+ const color = isColor ? resolveColor$1(value, colorMap) : null;
8660
+ const isSelected = selectedValue === value;
8661
+ const isAvailable = showAvailability
8662
+ ? getAvailability$1(option.name, value, options, variants, selectedValues)
8663
+ : true;
8664
+ if (color) {
8665
+ return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--color', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), title: `${value}${!isAvailable ? ' (Unavailable)' : ''}`, style: {
8666
+ display: 'inline-block',
8667
+ width: 14,
8668
+ height: 14,
8669
+ borderRadius: '50%',
8670
+ backgroundColor: color,
8671
+ border: isSelected
8672
+ ? '2px solid var(--seekora-primary, #111827)'
8673
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
8674
+ outline: isSelected ? '2px solid var(--seekora-primary, #111827)' : 'none',
8675
+ outlineOffset: 2,
8676
+ cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
8677
+ flexShrink: 0,
8678
+ boxShadow: isSelected ? '0 0 0 1px #fff' : 'none',
8679
+ opacity: isAvailable ? 1 : 0.3,
8680
+ position: 'relative',
8681
+ }, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
8682
+ e.stopPropagation();
8683
+ e.preventDefault();
8684
+ }, onClick: (e) => {
8685
+ e.stopPropagation();
8686
+ if (isAvailable) {
8687
+ onSwatchClick?.(option.name, value);
8688
+ }
8689
+ } }, !isAvailable && (React.createElement("span", { style: {
8690
+ position: 'absolute',
8691
+ top: '50%',
8692
+ left: '-2px',
8693
+ right: '-2px',
8694
+ height: 1,
8695
+ backgroundColor: '#ef4444',
8696
+ transform: 'translateY(-50%) rotate(-45deg)',
8697
+ } }))));
8698
+ }
8699
+ return (React.createElement("span", { key: value, className: clsx('seekora-variant-swatch', 'seekora-variant-swatch--text', isSelected && 'seekora-variant-swatch--selected', !isAvailable && 'seekora-variant-swatch--unavailable'), style: {
8700
+ display: 'inline-block',
8701
+ padding: '1px 6px',
8702
+ fontSize: '0.6875rem',
8703
+ borderRadius: 3,
8704
+ border: isSelected
8705
+ ? '1px solid var(--seekora-primary, #111827)'
8706
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
8707
+ backgroundColor: isSelected
8708
+ ? 'var(--seekora-primary, #111827)'
8709
+ : 'transparent',
8710
+ color: isSelected
8711
+ ? '#fff'
8712
+ : 'var(--seekora-text-secondary, #6b7280)',
8713
+ cursor: onSwatchClick && isAvailable ? 'pointer' : 'not-allowed',
8714
+ whiteSpace: 'nowrap',
8715
+ fontWeight: isSelected ? 600 : 400,
8716
+ opacity: isAvailable ? 1 : 0.4,
8717
+ textDecoration: !isAvailable ? 'line-through' : 'none',
8718
+ }, onMouseEnter: () => isAvailable && onSwatchHover?.(option.name, value), onMouseDown: (e) => {
8719
+ e.stopPropagation();
8720
+ e.preventDefault();
8721
+ }, onClick: (e) => {
8722
+ e.stopPropagation();
8723
+ if (isAvailable) {
8724
+ onSwatchClick?.(option.name, value);
8725
+ }
8726
+ } }, value));
8727
+ }),
8728
+ hasOverflow && (React.createElement("button", { type: "button", className: "seekora-variant-swatch--overflow", onClick: (e) => toggleExpanded(option.name, e), style: {
8729
+ fontSize: '0.6875rem',
8730
+ color: 'var(--seekora-primary, #2563eb)',
8731
+ background: 'none',
8732
+ border: 'none',
8733
+ padding: 0,
8734
+ cursor: 'pointer',
8735
+ textDecoration: 'underline',
8736
+ fontWeight: 500,
8737
+ } }, isExpanded ? 'Show less' : `+${overflow} more`))));
8738
+ })));
8739
+ }
8740
+
8741
+ /**
8742
+ * ProductCardLayouts – internal layout sub-components for ProductCard
8743
+ *
8744
+ * Not exported from the package. Each layout renders the same product data
8745
+ * with different visual emphasis.
8746
+ */
8747
+ const imgPlaceholderStyle = {
8748
+ width: '100%',
8749
+ aspectRatio: '1',
8750
+ objectFit: 'cover',
8751
+ borderRadius: 4,
8752
+ backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
8753
+ };
8754
+ function ImageBlock({ images, title, imageVariant, aspectRatio, enableZoom, zoomMode, zoomLevel }) {
8755
+ const ar = aspectRatio ? aspectRatio.replace(':', '/') : '1';
8756
+ if (images.length > 0) {
8757
+ return (React.createElement("div", { className: "seekora-product-card__image", style: { position: 'relative', overflow: 'hidden', borderRadius: 4 } },
8758
+ 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 })));
8759
+ }
8760
+ return React.createElement("div", { className: "seekora-product-card__image seekora-suggestions-product-card-placeholder", style: { ...imgPlaceholderStyle, aspectRatio: ar }, "aria-hidden": true });
8761
+ }
8762
+ /** minimal: image, title, price (current default behavior) */
8763
+ function MinimalLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
8764
+ return (React.createElement(React.Fragment, null,
8765
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8766
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8767
+ price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8768
+ product.currency ?? '$',
8769
+ price.toFixed(2)))));
8770
+ }
8771
+ /** standard: image, badges, brand, title, price + compare price, color swatches */
8772
+ function StandardLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8773
+ const cfg = displayConfig;
8774
+ return (React.createElement(React.Fragment, null,
8775
+ React.createElement("div", { style: { position: 'relative' } },
8776
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8777
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 2 })),
8778
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8779
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
8780
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
8781
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8782
+ React.createElement("div", { className: "seekora-product-card__price" },
8783
+ React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
8784
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8785
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8786
+ }
8787
+ /** detailed: image, badges, brand, title, price + compare + discount, rating, all swatches, stock */
8788
+ function DetailedLayout({ images, title, price, comparePrice, brand, badges, priceRange, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8789
+ const cfg = displayConfig;
8790
+ const available = product.available;
8791
+ return (React.createElement(React.Fragment, null,
8792
+ React.createElement("div", { style: { position: 'relative' } },
8793
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: cfg.imageAspectRatio, enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8794
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left" })),
8795
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8796
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4 } },
8797
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
8798
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8799
+ cfg.showRating !== false && product.rating != null && (React.createElement(RatingDisplay, { rating: product.rating, reviewCount: product.reviewCount, variant: "compact", size: "small", showNumeric: false, showReviewCount: true, className: "seekora-product-card__rating" })),
8800
+ React.createElement("div", { className: "seekora-product-card__price" }, cfg.showPriceRange && priceRange ? (React.createElement(PriceDisplay, { priceRange: priceRange, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, style: { fontSize: '0.875rem' } })) : (React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } }))),
8801
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8802
+ cfg.showStock !== false && available != null && (React.createElement("span", { className: "seekora-product-card__stock", style: {
8803
+ fontSize: '0.75rem',
8804
+ color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
8805
+ } }, available ? 'In Stock' : 'Out of Stock')),
8806
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8807
+ }
8808
+ /** compact: smaller image, 1-line title, price */
8809
+ function CompactLayout({ images, title, price, product, imageVariant, displayConfig, enableImageZoom, imageZoomMode, imageZoomLevel }) {
8810
+ return (React.createElement(React.Fragment, null,
8811
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: displayConfig.imageAspectRatio ?? '1:1', enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8812
+ React.createElement("span", { className: "seekora-product-card__title", style: {
8813
+ fontSize: '0.8125rem',
8814
+ fontWeight: 500,
8815
+ overflow: 'hidden',
8816
+ textOverflow: 'ellipsis',
8817
+ whiteSpace: 'nowrap',
8818
+ } }, title),
8819
+ price != null && !Number.isNaN(price) && (React.createElement("span", { className: "seekora-product-card__price", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8820
+ product.currency ?? '$',
8821
+ price.toFixed(2)))));
8822
+ }
8823
+ /** horizontal: image left + content right (title, brand, price, swatches) */
8824
+ function HorizontalLayout({ images, title, price, comparePrice, brand, badges, options, variants, product, imageVariant, displayConfig, onVariantHover, onVariantClick, selectedVariants, actionButtons, actionButtonsPosition, showActionLabels, enableImageZoom, imageZoomMode, imageZoomLevel, }) {
8825
+ const cfg = displayConfig;
8826
+ return (React.createElement("div", { style: { display: 'flex', gap: 12, alignItems: 'flex-start' } },
8827
+ React.createElement("div", { style: { position: 'relative', width: 80, flexShrink: 0 } },
8828
+ React.createElement(ImageBlock, { images: images, title: title, imageVariant: imageVariant, aspectRatio: "1:1", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel }),
8829
+ cfg.showBadges !== false && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "top-left", maxBadges: 1 })),
8830
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition?.startsWith('overlay') && (React.createElement(ActionButtons, { buttons: actionButtons, position: actionButtonsPosition === 'overlay-top-right' ? 'top-right' : 'bottom-center', showLabels: showActionLabels, size: "small" }))),
8831
+ React.createElement("div", { className: "seekora-product-card__body", style: { display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 0 } },
8832
+ cfg.showBrand !== false && brand && (React.createElement("span", { className: "seekora-product-card__brand", style: { fontSize: '0.75rem', color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, brand)),
8833
+ React.createElement("span", { className: "seekora-product-card__title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8834
+ React.createElement("div", { className: "seekora-product-card__price" },
8835
+ React.createElement(PriceDisplay, { price: price ?? undefined, comparePrice: comparePrice ?? undefined, currency: cfg.currency ?? product.currency, currencyPosition: cfg.currencyPosition, priceDecimals: cfg.priceDecimals, showDiscount: cfg.showDiscount, style: { fontSize: '0.875rem' } })),
8836
+ cfg.showVariants !== false && options.length > 0 && (React.createElement(VariantSwatches, { options: options, visibleOptions: cfg.variantOptionsToShow, maxValues: cfg.maxVariantValues ?? 3, selectedValues: selectedVariants, variants: variants, onSwatchHover: onVariantHover, onSwatchClick: onVariantClick })),
8837
+ actionButtons && actionButtons.length > 0 && actionButtonsPosition === 'inline' && (React.createElement(ActionButtons, { buttons: actionButtons, position: "inline", showLabels: showActionLabels, size: "small", layout: "horizontal" })))));
8838
+ }
8839
+
8840
+ /**
8841
+ * ProductCard – one product tile (primitive)
8842
+ *
8843
+ * Without displayConfig: renders the original minimal layout (image, title, price).
8844
+ * With displayConfig: renders layout variants (minimal, standard, detailed, compact, horizontal).
8845
+ */
8846
+ const cardStyle = {
8847
+ display: 'flex',
8848
+ flexDirection: 'column',
8849
+ gap: 8,
8850
+ padding: 8,
8851
+ cursor: 'pointer',
8852
+ border: 'none',
8853
+ borderRadius: 'var(--seekora-border-radius, 6px)',
8854
+ backgroundColor: 'transparent',
8855
+ textAlign: 'left',
8856
+ transition: 'background-color 120ms ease',
8857
+ };
8858
+ const imgStyle = {
8859
+ width: '100%',
8860
+ aspectRatio: '1',
8861
+ objectFit: 'cover',
8862
+ borderRadius: 4,
8863
+ backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
8864
+ };
8865
+ function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', displayConfig, onVariantHover, onVariantClick, selectedVariants, asLink, actionButtons, actionButtonsPosition = 'overlay-top-right', showActionLabels = false, enableImageZoom = false, imageZoomMode = 'both', imageZoomLevel = 2.5, }) {
8866
+ // Find selected variant if selections are provided
8867
+ const selectedVariant = useMemo(() => {
8868
+ if (!selectedVariants || !product.options || !product.variants)
8869
+ return null;
8870
+ return findVariantBySelections(product.options, product.variants, selectedVariants);
8871
+ }, [selectedVariants, product.options, product.variants]);
8872
+ // Compute effective display data (use selected variant if available, otherwise product defaults)
8873
+ const effectiveImages = useMemo(() => {
8874
+ // Priority: selected variant image > product images > product image
8875
+ if (selectedVariant?.image)
8876
+ return [selectedVariant.image];
8877
+ if (product.images?.length)
8878
+ return product.images;
8879
+ if (product.image ?? product.imageUrl)
8880
+ return [String(product.image ?? product.imageUrl)];
8881
+ return [];
8882
+ }, [selectedVariant, product]);
8883
+ const effectiveTitle = useMemo(() => {
8884
+ // Show variant title if available (e.g., "T-Shirt - Black / M")
8885
+ if (selectedVariant?.title && selectedVariant.title !== product.title) {
8886
+ return `${product.title ?? product.name ?? ''} - ${selectedVariant.title}`;
8887
+ }
8888
+ return product.title ?? product.name ?? '';
8889
+ }, [selectedVariant, product]);
8890
+ const effectivePrice = useMemo(() => {
8891
+ const variantPrice = selectedVariant?.price;
8892
+ if (variantPrice != null)
8893
+ return typeof variantPrice === 'number' ? variantPrice : Number(variantPrice);
8894
+ const productPrice = product.price;
8895
+ return productPrice != null ? (typeof productPrice === 'number' ? productPrice : Number(productPrice)) : null;
8896
+ }, [selectedVariant, product.price]);
8897
+ const effectiveComparePrice = useMemo(() => {
8898
+ const variantCompare = selectedVariant?.comparePrice;
8899
+ if (variantCompare != null)
8900
+ return typeof variantCompare === 'number' ? variantCompare : Number(variantCompare);
8901
+ const productCompare = product.original_price ?? product.compare_at_price;
8902
+ return productCompare != null ? (typeof productCompare === 'number' ? productCompare : Number(productCompare)) : null;
8903
+ }, [selectedVariant, product]);
8904
+ // Legacy vars for backwards compat
8905
+ const images = effectiveImages;
8906
+ const title = effectiveTitle;
8907
+ const price = effectivePrice;
8908
+ // If no displayConfig, render original minimal layout (backwards compat)
8909
+ if (!displayConfig) {
8910
+ return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
8911
+ e.preventDefault();
8912
+ onSelect();
8913
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8914
+ images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image", enableZoom: enableImageZoom, zoomMode: imageZoomMode, zoomLevel: imageZoomLevel })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
8915
+ React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
8916
+ price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
8917
+ product.currency ?? '$',
8918
+ price.toFixed(2))) : null));
8919
+ }
8920
+ // Enhanced layout with displayConfig
8921
+ const layoutStyle = displayConfig.style ?? 'minimal';
8922
+ const comparePrice = effectiveComparePrice;
8923
+ const brand = product.brand ?? null;
8924
+ const options = product.options ?? [];
8925
+ const variants = product.variants ?? [];
8926
+ const badges = useMemo(() => {
8927
+ if (displayConfig.showBadges === false)
8928
+ return [];
8929
+ if (displayConfig.badgeExtractor)
8930
+ return displayConfig.badgeExtractor(product.tags, product);
8931
+ return extractBadges(product.tags, product);
8932
+ }, [displayConfig, product]);
8933
+ const priceRange = useMemo(() => {
8934
+ if (!displayConfig.showPriceRange)
8935
+ return null;
8936
+ return getPriceRange(product.variants);
8937
+ }, [displayConfig.showPriceRange, product.variants]);
8938
+ const layoutProps = {
8939
+ product,
8940
+ images,
8941
+ title,
8942
+ price,
8943
+ comparePrice,
8944
+ brand,
8945
+ badges,
8946
+ priceRange,
8947
+ options,
8948
+ variants,
8949
+ displayConfig,
8950
+ imageVariant,
8951
+ onVariantHover,
8952
+ onVariantClick,
8953
+ selectedVariants,
8954
+ actionButtons,
8955
+ actionButtonsPosition,
8956
+ showActionLabels,
8957
+ enableImageZoom,
8958
+ imageZoomMode,
8959
+ imageZoomLevel,
8960
+ };
8961
+ const layoutMap = {
8962
+ minimal: MinimalLayout,
8963
+ standard: StandardLayout,
8964
+ detailed: DetailedLayout,
8965
+ compact: CompactLayout,
8966
+ horizontal: HorizontalLayout,
8967
+ };
8968
+ const LayoutComponent = layoutMap[layoutStyle] ?? MinimalLayout;
8969
+ const rootClassName = clsx('seekora-product-card', `seekora-product-card--${layoutStyle}`, 'seekora-suggestions-product-card', className);
8970
+ const rootStyle = {
8971
+ ...cardStyle,
8972
+ ...(layoutStyle === 'horizontal' ? { flexDirection: 'row' } : {}),
8973
+ ...style,
8974
+ };
8975
+ if (asLink && product.url) {
8976
+ return (React.createElement("a", { href: product.url, className: rootClassName, style: { ...rootStyle, textDecoration: 'none', color: 'inherit' }, onClick: (e) => {
8977
+ e.preventDefault();
8978
+ onSelect();
8979
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8980
+ React.createElement(LayoutComponent, { ...layoutProps })));
8981
+ }
8982
+ return (React.createElement("button", { type: "button", className: rootClassName, style: rootStyle, onMouseDown: (e) => {
8983
+ e.preventDefault();
8984
+ onSelect();
8985
+ }, "data-position": position, "data-section": section, "data-tab-id": tabId },
8986
+ React.createElement(LayoutComponent, { ...layoutProps })));
8987
+ }
8988
+
8989
+ /**
8990
+ * ProductGrid – grid of product cards from context (primitive)
8991
+ *
8992
+ * Uses trendingProducts or active tab products; each click calls context selectProduct.
8993
+ */
8994
+ function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, displayConfig, onVariantHover, }) {
8995
+ const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
8996
+ const products = useMemo(() => {
8997
+ if (source === 'trending')
8998
+ return trendingProducts;
8999
+ const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
9000
+ return tab?.products ?? [];
9001
+ }, [source, activeTabId, trendingProducts, filteredTabs]);
9002
+ const items = products.slice(0, maxItems);
9003
+ const navigableItems = getAllNavigableItems();
9004
+ const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
9005
+ if (items.length === 0)
9006
+ return null;
9007
+ const gridStyle = {
9008
+ display: 'grid',
9009
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
9010
+ gap: 12,
9011
+ padding: 12,
9012
+ };
9013
+ return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
9014
+ React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
9015
+ const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
9016
+ const section = source === 'trending' ? 'products' : 'filtered_tab';
9017
+ const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
9018
+ return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId), displayConfig: displayConfig, onVariantHover: onVariantHover }));
9019
+ }))));
9020
+ }
9021
+
9022
+ /**
9023
+ * CategoriesTabs – horizontal tabs (e.g. filtered tabs) (primitive)
9024
+ *
9025
+ * Active tab from context; on select updates context and tracks analytics.
9026
+ */
9027
+ function CategoriesTabs({ className, style, tabClassName }) {
9028
+ const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
9029
+ if (filteredTabs.length === 0)
9030
+ return null;
9031
+ return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
9032
+ display: 'flex',
9033
+ gap: 4,
9034
+ padding: '8px 12px',
9035
+ borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
9036
+ overflowX: 'auto',
9037
+ ...style,
9038
+ }, role: "tablist" }, filteredTabs.map((tab) => {
9039
+ const isActive = activeTabId === tab.id;
9040
+ return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
9041
+ padding: '8px 12px',
9042
+ border: 'none',
9043
+ borderRadius: 'var(--seekora-border-radius, 6px)',
9044
+ backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
9045
+ color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
9046
+ cursor: 'pointer',
9047
+ fontSize: '0.875rem',
9048
+ fontWeight: isActive ? 600 : 400,
9049
+ whiteSpace: 'nowrap',
9050
+ transition: 'background-color 120ms ease',
9051
+ }, onClick: () => setActiveTab(tab) }, tab.label));
9052
+ })));
9053
+ }
9054
+
9055
+ /**
9056
+ * RecentSearchesList – list of recent queries (primitive)
9057
+ *
9058
+ * Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
9059
+ */
9060
+ const itemStyle$1 = {
9061
+ padding: '10px 12px',
9062
+ cursor: 'pointer',
9063
+ border: 'none',
9064
+ width: '100%',
9065
+ textAlign: 'left',
9066
+ fontSize: 'inherit',
9067
+ fontFamily: 'inherit',
9068
+ backgroundColor: 'transparent',
9069
+ color: 'var(--seekora-text-primary, #111827)',
9070
+ transition: 'background-color 120ms ease',
9071
+ };
9072
+ function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
9073
+ const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
9074
+ const items = recentSearches.slice(0, maxItems);
9075
+ if (items.length === 0)
9076
+ return null;
9077
+ return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
9078
+ title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
9079
+ React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
9080
+ const onSelect = () => selectRecentSearch(search);
9081
+ if (renderItem) {
9082
+ return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
9083
+ }
9084
+ return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
9085
+ React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
9086
+ e.preventDefault();
9087
+ onSelect();
9088
+ } }, search.query)));
9089
+ }))));
9090
+ }
9091
+
9092
+ /**
9093
+ * TrendingList – list of trending searches (primitive)
9094
+ *
9095
+ * Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
9096
+ */
9097
+ const itemStyle = {
9098
+ padding: '10px 12px',
9099
+ cursor: 'pointer',
9100
+ border: 'none',
9101
+ width: '100%',
9102
+ textAlign: 'left',
9103
+ fontSize: 'inherit',
9104
+ fontFamily: 'inherit',
9105
+ backgroundColor: 'transparent',
9106
+ color: 'var(--seekora-text-primary, #111827)',
9107
+ transition: 'background-color 120ms ease',
9108
+ };
9109
+ function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
9110
+ const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
9111
+ const items = trendingSearches.slice(0, maxItems);
9112
+ if (items.length === 0)
9113
+ return null;
9114
+ return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
9115
+ title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
9116
+ React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
9117
+ const onSelect = () => selectTrendingSearch(trending, i);
9118
+ if (renderItem) {
9119
+ return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
9120
+ }
9121
+ return (React.createElement("li", { key: `${trending.query}-${i}` },
9122
+ React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
9123
+ e.preventDefault();
9124
+ onSelect();
9125
+ } },
9126
+ trending.query,
9127
+ trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
9128
+ }))));
9129
+ }
9130
+
9131
+ /**
9132
+ * SuggestionsError – error message (primitive)
9133
+ */
9134
+ function SuggestionsError({ className, style, render }) {
9135
+ const { error } = useSuggestionsContext();
9136
+ if (!error)
9137
+ return null;
9138
+ if (render)
9139
+ return React.createElement(React.Fragment, null, render(error));
9140
+ return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
9141
+ padding: 16,
9142
+ color: 'var(--seekora-error, #dc2626)',
9143
+ fontSize: '0.875rem',
9144
+ ...style,
9145
+ } }, error.message));
9146
+ }
9147
+
9148
+ /**
9149
+ * SuggestionsDropdownComposition – reference composition
9150
+ *
9151
+ * Example layout built from primitives: SearchInput + DropdownPanel containing
9152
+ * RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
9153
+ * Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
9154
+ * with your own arrangement of the same primitives.
9155
+ */
9156
+ function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
9157
+ return (React.createElement(SuggestionsProvider, { ...providerProps },
9158
+ React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
9159
+ React.createElement(SearchInput, { placeholder: placeholder }),
9160
+ React.createElement(DropdownPanel, null,
9161
+ React.createElement(SuggestionsError, null),
9162
+ React.createElement(SuggestionsLoading, null),
9163
+ showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
9164
+ React.createElement(SuggestionList, null),
9165
+ showTabs ? React.createElement(CategoriesTabs, null) : null,
9166
+ showProducts ? React.createElement(ProductGrid, null) : null,
9167
+ showTrending ? React.createElement(TrendingList, null) : null))));
9168
+ }
9169
+
9170
+ /**
9171
+ * VariantSelector – full variant selector for product detail pages
9172
+ *
9173
+ * Three render modes per option:
9174
+ * - swatch: color circles (auto-detected for "Color" option or when colorMap provided)
9175
+ * - button: rectangular buttons (default for most options)
9176
+ * - dropdown: <select> for options with many values
9177
+ */
9178
+ const COLOR_NAMES = {
9179
+ black: '#000', white: '#fff', red: '#ef4444', blue: '#3b82f6',
9180
+ green: '#22c55e', yellow: '#eab308', orange: '#f97316', purple: '#a855f7',
9181
+ pink: '#ec4899', brown: '#92400e', grey: '#6b7280', gray: '#6b7280',
9182
+ navy: '#1e3a5f', beige: '#d4c5a9', cream: '#fffdd0', ivory: '#fffff0',
9183
+ teal: '#0d9488', coral: '#ff7f50', maroon: '#800000', charcoal: '#36454f',
9184
+ sage: '#9caf88', lavender: '#e6e6fa', mint: '#98fb98', rust: '#b7410e',
9185
+ plum: '#8e4585', slate: '#708090', indigo: '#4b0082', gold: '#ffd700',
9186
+ silver: '#c0c0c0', rose: '#ff007f', raven: '#0a0a0a', natural: '#f5f0e1',
9187
+ bone: '#e3dac9', sand: '#c2b280', olive: '#808000', khaki: '#c3b091',
9188
+ burgundy: '#800020', wine: '#722f37', mauve: '#e0b0ff', tan: '#d2b48c',
9189
+ };
9190
+ const isColorOption = (name) => {
9191
+ const lower = name.toLowerCase();
9192
+ return lower === 'color' || lower === 'colour' || lower === 'colors' || lower === 'colours';
9193
+ };
9194
+ const resolveColor = (value, colorMap) => {
9195
+ if (colorMap?.[value])
9196
+ return colorMap[value];
9197
+ const lower = value.toLowerCase();
9198
+ if (colorMap?.[lower])
9199
+ return colorMap[lower];
9200
+ return COLOR_NAMES[lower] ?? null;
9201
+ };
9202
+ const getAvailability = (optionName, value, options, variants, selections) => {
9203
+ const optionIndex = options.findIndex((o) => o.name === optionName);
9204
+ if (optionIndex === -1)
9205
+ return true;
9206
+ const optionKey = `option${optionIndex + 1}`;
9207
+ return variants.some((variant) => {
9208
+ if (variant[optionKey] !== value)
9209
+ return false;
9210
+ if (variant.available === false)
9211
+ return false;
9212
+ for (const [selName, selValue] of Object.entries(selections)) {
9213
+ if (selName === optionName)
9214
+ continue;
9215
+ const selIdx = options.findIndex((o) => o.name === selName);
9216
+ if (selIdx === -1)
9217
+ continue;
9218
+ const selKey = `option${selIdx + 1}`;
9219
+ if (variant[selKey] !== selValue)
9220
+ return false;
9221
+ }
9222
+ return true;
9223
+ });
9224
+ };
9225
+ function VariantSelector({ options, variants, selections, onSelectionChange, optionRenderModes, dropdownThreshold = 8, colorMap, showAvailability = true, selectedVariant: _selectedVariant, className, style, }) {
9226
+ if (!options || options.length === 0)
9227
+ return null;
9228
+ const getRenderMode = (option) => {
9229
+ if (optionRenderModes?.[option.name])
9230
+ return optionRenderModes[option.name];
9231
+ if (isColorOption(option.name))
9232
+ return 'swatch';
9233
+ if (option.values.length > dropdownThreshold)
9234
+ return 'dropdown';
9235
+ return 'button';
9236
+ };
9237
+ return (React.createElement("div", { className: clsx('seekora-variant-selector', className), style: { display: 'flex', flexDirection: 'column', gap: 16, ...style } }, options.map((option) => {
9238
+ const mode = getRenderMode(option);
9239
+ const selected = selections[option.name];
9240
+ return (React.createElement("div", { key: option.name, className: "seekora-variant-option-group" },
9241
+ React.createElement("label", { className: "seekora-variant-option-label", style: {
9242
+ display: 'block',
9243
+ fontSize: '0.875rem',
9244
+ fontWeight: 600,
9245
+ marginBottom: 8,
9246
+ color: 'var(--seekora-text-primary, #111827)',
9247
+ } },
9248
+ option.name,
9249
+ selected && (React.createElement("span", { style: { fontWeight: 400, marginLeft: 6, color: 'var(--seekora-text-secondary, #6b7280)' } }, selected))),
9250
+ mode === 'dropdown' ? (React.createElement("select", { className: "seekora-variant-dropdown", value: selected ?? '', onChange: (e) => {
9251
+ e.stopPropagation();
9252
+ onSelectionChange(option.name, e.target.value);
9253
+ }, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), style: {
9254
+ padding: '8px 12px',
9255
+ fontSize: '0.875rem',
9256
+ borderRadius: 6,
9257
+ border: '1px solid var(--seekora-border-color, #e5e7eb)',
9258
+ backgroundColor: 'var(--seekora-bg-surface, #fff)',
9259
+ color: 'var(--seekora-text-primary, #111827)',
9260
+ cursor: 'pointer',
9261
+ minWidth: 120,
9262
+ } },
9263
+ React.createElement("option", { value: "" },
9264
+ "Select ",
9265
+ option.name),
9266
+ option.values.map((value) => {
9267
+ const available = showAvailability
9268
+ ? getAvailability(option.name, value, options, variants, selections)
9269
+ : true;
9270
+ return (React.createElement("option", { key: value, value: value, disabled: !available },
9271
+ value,
9272
+ !available ? ' (Unavailable)' : ''));
9273
+ }))) : (React.createElement("div", { className: "seekora-variant-buttons", style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, option.values.map((value) => {
9274
+ const isActive = selected === value;
9275
+ const available = showAvailability
9276
+ ? getAvailability(option.name, value, options, variants, selections)
9277
+ : true;
9278
+ const color = mode === 'swatch' ? resolveColor(value, colorMap) : null;
9279
+ if (mode === 'swatch' && color) {
9280
+ return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-color-swatch', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), title: value, onMouseDown: (e) => {
9281
+ e.stopPropagation();
9282
+ e.preventDefault();
9283
+ }, onClick: (e) => {
9284
+ e.stopPropagation();
9285
+ onSelectionChange(option.name, value);
9286
+ }, disabled: !available, style: {
9287
+ width: 32,
9288
+ height: 32,
9289
+ borderRadius: '50%',
9290
+ backgroundColor: color,
9291
+ border: isActive
9292
+ ? '2px solid var(--seekora-primary, #111827)'
9293
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
9294
+ outline: isActive ? '2px solid var(--seekora-primary, #111827)' : 'none',
9295
+ outlineOffset: 2,
9296
+ cursor: available ? 'pointer' : 'not-allowed',
9297
+ opacity: available ? 1 : 0.4,
9298
+ position: 'relative',
9299
+ padding: 0,
9300
+ }, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }));
9301
+ }
9302
+ return (React.createElement("button", { key: value, type: "button", className: clsx('seekora-variant-button', isActive && 'seekora-variant-button--active', !available && 'seekora-variant-button--unavailable'), onMouseDown: (e) => {
9303
+ e.stopPropagation();
9304
+ e.preventDefault();
9305
+ }, onClick: (e) => {
9306
+ e.stopPropagation();
9307
+ onSelectionChange(option.name, value);
9308
+ }, disabled: !available, style: {
9309
+ padding: '6px 16px',
9310
+ fontSize: '0.875rem',
9311
+ borderRadius: 6,
9312
+ border: isActive
9313
+ ? '2px solid var(--seekora-primary, #111827)'
9314
+ : '1px solid var(--seekora-border-color, #e5e7eb)',
9315
+ backgroundColor: isActive
9316
+ ? 'var(--seekora-primary, #111827)'
9317
+ : 'var(--seekora-bg-surface, #fff)',
9318
+ color: isActive
9319
+ ? '#fff'
9320
+ : 'var(--seekora-text-primary, #111827)',
9321
+ cursor: available ? 'pointer' : 'not-allowed',
9322
+ opacity: available ? 1 : 0.5,
9323
+ textDecoration: !available ? 'line-through' : 'none',
9324
+ fontWeight: isActive ? 600 : 400,
9325
+ transition: 'all 120ms ease',
9326
+ }, "aria-label": `${option.name}: ${value}`, "aria-pressed": isActive }, value));
9327
+ })))));
9328
+ })));
9329
+ }
9330
+
9331
+ /**
9332
+ * withAnalytics – HOC that wraps any React component to inject analytics tracking
9333
+ */
9334
+ function withAnalytics(Component, config = {}) {
9335
+ const { trackClick = true, trackImpression = false, clickEventName = 'product.click', getProductFromProps = () => null, getPositionFromProps = () => 0, } = config;
9336
+ const displayName = Component.displayName || Component.name || 'Component';
9337
+ const Wrapped = React.forwardRef((props, ref) => {
9338
+ const { seekoraClient, seekoraContext, ...rest } = props;
9339
+ const containerRef = useRef(null);
9340
+ const impressionTrackedRef = useRef(false);
9341
+ const product = getProductFromProps(rest);
9342
+ const position = getPositionFromProps(rest);
9343
+ const sendEvent = useCallback(async (eventName, metadata) => {
9344
+ if (!seekoraClient)
9345
+ return;
9346
+ try {
9347
+ await seekoraClient.trackEvent?.({
9348
+ event_name: eventName,
9349
+ metadata: {
9350
+ product_id: product?.id || product?.objectID,
9351
+ product_title: product?.title || product?.name,
9352
+ product_price: product?.price,
9353
+ position,
9354
+ timestamp: Date.now(),
9355
+ source: 'with_analytics_hoc',
9356
+ ...metadata,
9357
+ },
9358
+ }, seekoraContext);
9359
+ }
9360
+ catch (error) {
9361
+ log.warn(`withAnalytics: Failed to track ${eventName}`, { error });
9362
+ }
9363
+ }, [seekoraClient, seekoraContext, product, position]);
9364
+ const handleClick = useCallback(() => {
9365
+ if (!trackClick || !product)
9366
+ return;
9367
+ sendEvent(clickEventName, {});
9368
+ if (seekoraClient?.trackClick && product) {
9369
+ Promise.resolve(seekoraClient.trackClick(product.id || product.objectID || '', position + 1, seekoraContext)).catch(() => { });
9370
+ }
9371
+ }, [trackClick, product, sendEvent, seekoraClient, seekoraContext, position]);
9372
+ // Impression tracking
9373
+ useEffect(() => {
9374
+ if (!trackImpression || !seekoraClient || !containerRef.current || impressionTrackedRef.current)
9375
+ return;
9376
+ if (typeof IntersectionObserver === 'undefined')
9377
+ return;
9378
+ const observer = new IntersectionObserver((entries) => {
9379
+ for (const entry of entries) {
9380
+ if (entry.isIntersecting && !impressionTrackedRef.current) {
9381
+ impressionTrackedRef.current = true;
9382
+ sendEvent('product.impression', {});
9383
+ observer.disconnect();
9384
+ }
9385
+ }
9386
+ }, { threshold: 0.5 });
9387
+ observer.observe(containerRef.current);
9388
+ return () => observer.disconnect();
9389
+ }, [trackImpression, seekoraClient, sendEvent]);
9390
+ return (React.createElement("div", { ref: (node) => {
9391
+ containerRef.current = node;
9392
+ if (typeof ref === 'function')
9393
+ ref(node);
9394
+ else if (ref)
9395
+ ref.current = node;
9396
+ }, onClick: handleClick, style: { display: 'contents' } },
9397
+ React.createElement(Component, { ...rest })));
9398
+ });
9399
+ Wrapped.displayName = `withAnalytics(${displayName})`;
9400
+ return Wrapped;
9401
+ }
9402
+
9403
+ /**
9404
+ * AnalyticsProvider – context + delegated event listener for data-seekora-* attribute-based analytics
9405
+ *
9406
+ * Installs a delegated click listener on a container. Any descendant with
9407
+ * data-seekora-track="click"|"add-to-cart" and data-seekora-product-id="..."
9408
+ * automatically fires analytics events without React wiring.
9409
+ */
9410
+ const AnalyticsContext = createContext(null);
9411
+ const useAnalyticsProvider = () => useContext(AnalyticsContext);
9412
+ function AnalyticsProvider({ client, context, children }) {
9413
+ const containerRef = useRef(null);
9414
+ const sendEvent = useCallback(async (eventName, metadata) => {
9415
+ try {
9416
+ await client.trackEvent?.({
9417
+ event_name: eventName,
9418
+ metadata: {
9419
+ timestamp: Date.now(),
9420
+ source: 'analytics_provider_delegation',
9421
+ ...metadata,
9422
+ },
9423
+ }, context);
9424
+ log.verbose(`AnalyticsProvider: ${eventName}`, metadata);
9425
+ }
9426
+ catch (error) {
9427
+ log.warn(`AnalyticsProvider: Failed to track ${eventName}`, { error });
9428
+ }
9429
+ }, [client, context]);
9430
+ useEffect(() => {
9431
+ const container = containerRef.current;
9432
+ if (!container)
9433
+ return;
9434
+ const handleClick = (e) => {
9435
+ const target = e.target;
9436
+ // Walk up from target to find element with data-seekora-track
9437
+ const tracked = target.closest('[data-seekora-track]');
9438
+ if (!tracked)
9439
+ return;
9440
+ const trackType = tracked.getAttribute('data-seekora-track');
9441
+ const productId = tracked.getAttribute('data-seekora-product-id');
9442
+ const position = tracked.getAttribute('data-seekora-position');
9443
+ const section = tracked.getAttribute('data-seekora-section');
9444
+ const variantSku = tracked.getAttribute('data-seekora-variant-sku');
9445
+ const variantOption = tracked.getAttribute('data-seekora-variant-option');
9446
+ const variantValue = tracked.getAttribute('data-seekora-variant-value');
9447
+ const metadata = {};
9448
+ if (productId)
9449
+ metadata.product_id = productId;
9450
+ if (position)
9451
+ metadata.position = parseInt(position, 10);
9452
+ if (section)
9453
+ metadata.section = section;
9454
+ switch (trackType) {
9455
+ case 'click':
9456
+ sendEvent('product.click', metadata);
9457
+ if (client?.trackClick && productId) {
9458
+ const pos = position ? parseInt(position, 10) + 1 : 1;
9459
+ Promise.resolve(client.trackClick(productId, pos, context)).catch(() => { });
9460
+ }
9461
+ break;
9462
+ case 'add-to-cart':
9463
+ if (variantSku)
9464
+ metadata.variant_sku = variantSku;
9465
+ sendEvent('product.add_to_cart', metadata);
9466
+ break;
9467
+ case 'variant-select':
9468
+ if (variantOption)
9469
+ metadata.option_name = variantOption;
9470
+ if (variantValue)
9471
+ metadata.option_value = variantValue;
9472
+ sendEvent('product.variant_select', metadata);
9473
+ break;
9474
+ default:
9475
+ // Custom track type — use it as event name
9476
+ if (trackType) {
9477
+ sendEvent(trackType, metadata);
9478
+ }
9479
+ break;
9480
+ }
9481
+ };
9482
+ container.addEventListener('click', handleClick);
9483
+ return () => container.removeEventListener('click', handleClick);
9484
+ }, [client, context, sendEvent]);
9485
+ return (React.createElement(AnalyticsContext.Provider, { value: { client, context } },
9486
+ React.createElement("div", { ref: containerRef, style: { display: 'contents' } }, children)));
9487
+ }
9488
+
9489
+ /**
9490
+ * SectionSearchContext – preset query/filter section state
9491
+ *
9492
+ * For menus, sidebar, front-page blocks. Independent of main search state.
9493
+ */
9494
+ const SectionSearchContext = createContext(null);
9495
+ function useSectionSearchContext() {
9496
+ const context = useContext(SectionSearchContext);
9497
+ if (!context) {
9498
+ const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
9499
+ log.error('SectionSearchContext: not available', { error: error.message });
9500
+ throw error;
9501
+ }
9502
+ return context;
9503
+ }
9504
+
9505
+ /**
9506
+ * SectionSearchProvider – preset query + filter section
9507
+ *
9508
+ * Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
9509
+ * query/filters change. Does not use global SearchStateManager. Use for menus,
9510
+ * sidebar, front-page blocks (e.g. "New arrivals", "On sale").
9511
+ */
9512
+ function extractItems(response) {
9513
+ if (!response)
9514
+ return [];
9515
+ if (Array.isArray(response.results))
9516
+ return response.results;
9517
+ if (Array.isArray(response.hits))
9518
+ return response.hits;
9519
+ const data = response.data;
9520
+ if (data && Array.isArray(data.results))
9521
+ return data.results;
9522
+ if (data && Array.isArray(data.data))
9523
+ return data.data;
9524
+ return [];
9525
+ }
9526
+ function extractTotal(response) {
9527
+ if (!response)
9528
+ return 0;
9529
+ const n = response.totalResults ?? response.total ?? response.total_results;
9530
+ if (typeof n === 'number')
9531
+ return n;
9532
+ const data = response.data;
9533
+ if (data?.total_results != null)
9534
+ return Number(data.total_results);
9535
+ if (data?.data?.total_results != null)
9536
+ return Number(data.data.total_results);
9537
+ return 0;
9538
+ }
9539
+ function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
9540
+ const { client } = useSearchContext();
9541
+ const [items, setItems] = useState([]);
9542
+ const [loading, setLoading] = useState(true);
9543
+ const [error, setError] = useState(null);
9544
+ const [totalCount, setTotalCount] = useState(0);
9545
+ useEffect(() => {
9546
+ if (!enabled || !client?.search) {
9547
+ setItems([]);
9548
+ setLoading(false);
9549
+ setError(null);
9550
+ setTotalCount(0);
9551
+ return;
9552
+ }
9553
+ let cancelled = false;
9554
+ setLoading(true);
9555
+ setError(null);
9556
+ const options = {
9557
+ per_page: maxItems,
9558
+ page: 1,
9559
+ };
9560
+ if (sortBy)
9561
+ options.sort_by = sortBy;
9562
+ if (refinements.length > 0) {
9563
+ options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
8125
9564
  }
8126
- this.cache.set(key, {
8127
- data,
8128
- timestamp: Date.now(),
8129
- ttl: ttlMs ?? this.defaultTtl,
9565
+ client
9566
+ .search(query, options)
9567
+ .then((response) => {
9568
+ if (cancelled)
9569
+ return;
9570
+ setItems(extractItems(response));
9571
+ setTotalCount(extractTotal(response));
9572
+ setLoading(false);
9573
+ })
9574
+ .catch((err) => {
9575
+ if (cancelled)
9576
+ return;
9577
+ setError(err instanceof Error ? err : new Error(String(err)));
9578
+ setItems([]);
9579
+ setLoading(false);
8130
9580
  });
8131
- }
8132
- /**
8133
- * Check if key exists and is valid
8134
- */
8135
- has(key) {
8136
- return this.get(key) !== null;
8137
- }
8138
- /**
8139
- * Clear all cached entries
8140
- */
8141
- clear() {
8142
- this.cache.clear();
8143
- }
8144
- /**
8145
- * Clear expired entries
8146
- */
8147
- cleanup() {
8148
- const now = Date.now();
8149
- for (const [key, entry] of this.cache.entries()) {
8150
- if (now - entry.timestamp > entry.ttl) {
8151
- this.cache.delete(key);
8152
- }
8153
- }
8154
- }
8155
- /**
8156
- * Get cache statistics
8157
- */
8158
- getStats() {
8159
- return {
8160
- size: this.cache.size,
8161
- maxSize: this.maxSize,
9581
+ return () => {
9582
+ cancelled = true;
8162
9583
  };
8163
- }
9584
+ }, [client, enabled, query, maxItems, sortBy, refinements]);
9585
+ const trackClick = useCallback((item, position) => {
9586
+ if (!client?.trackEvent)
9587
+ return;
9588
+ const id = item?.id ?? item?.objectID;
9589
+ client.trackEvent({
9590
+ event_name: 'section_result_click',
9591
+ clicked_item_id: id,
9592
+ position,
9593
+ section: sectionId,
9594
+ metadata: { section_id: sectionId },
9595
+ }, undefined);
9596
+ }, [client, sectionId]);
9597
+ const value = useMemo(() => ({
9598
+ items,
9599
+ loading,
9600
+ error,
9601
+ totalCount,
9602
+ sectionId,
9603
+ trackClick,
9604
+ }), [items, loading, error, totalCount, sectionId, trackClick]);
9605
+ return React.createElement(SectionSearchContext.Provider, { value: value }, children);
8164
9606
  }
8165
- // Global cache instance for suggestions (shared across components)
8166
- let globalSuggestionsCache = null;
9607
+
8167
9608
  /**
8168
- * Get the global suggestions cache instance
9609
+ * SectionLoading loading state for section (primitive)
8169
9610
  */
8170
- const getSuggestionsCache = (options) => {
8171
- if (!globalSuggestionsCache) {
8172
- globalSuggestionsCache = new SuggestionsCache(options);
8173
- }
8174
- return globalSuggestionsCache;
8175
- };
9611
+ function SectionLoading({ className, style, text = 'Loading...' }) {
9612
+ const { loading } = useSectionSearchContext();
9613
+ if (!loading)
9614
+ return null;
9615
+ return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
9616
+ }
9617
+
8176
9618
  /**
8177
- * Create a new cache instance (for isolated caching per component)
9619
+ * SectionError error state for section (primitive)
8178
9620
  */
8179
- const createSuggestionsCache = (options) => {
8180
- return new SuggestionsCache(options);
8181
- };
9621
+ function SectionError({ className, style, render }) {
9622
+ const { error } = useSectionSearchContext();
9623
+ if (!error)
9624
+ return null;
9625
+ if (render)
9626
+ return React.createElement(React.Fragment, null, render(error));
9627
+ return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
9628
+ }
9629
+
8182
9630
  /**
8183
- * Clear the global cache
9631
+ * SectionItemGrid generic grid of items from SectionSearchProvider (primitive)
8184
9632
  */
8185
- const clearSuggestionsCache = () => {
8186
- globalSuggestionsCache?.clear();
8187
- };
9633
+ function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
9634
+ const { items, loading, error, trackClick } = useSectionSearchContext();
9635
+ if (loading)
9636
+ return React.createElement(SectionLoading, { className: className, style: style });
9637
+ if (error)
9638
+ return React.createElement(SectionError, { className: className, style: style });
9639
+ return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
9640
+ }
9641
+
9642
+ /**
9643
+ * ProductGallery – product detail image gallery (primitive)
9644
+ *
9645
+ * Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
9646
+ * For use on individual product page.
9647
+ */
9648
+ function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
9649
+ return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
9650
+ React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
9651
+ }
9652
+
9653
+ /**
9654
+ * ProductInfo – product detail block (primitive)
9655
+ *
9656
+ * Title, description, price, optional variant selector and CTA. Minimal layout;
9657
+ * override with className/style. For use on individual product page.
9658
+ *
9659
+ * When options/variants/selections/onSelectionChange are provided and
9660
+ * renderVariantSelector is NOT provided, renders the built-in VariantSelector.
9661
+ * renderVariantSelector still takes priority as an override.
9662
+ */
9663
+ function ProductInfo({ title, description, price, currency = '$', comparePrice, brand, available, badges, options, variants, selectedVariant: _selectedVariant, selections, onSelectionChange, renderVariantSelector, renderCTA, className, style, }) {
9664
+ const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
9665
+ const comparePriceNum = comparePrice != null ? (typeof comparePrice === 'number' ? comparePrice : parseFloat(String(comparePrice))) : null;
9666
+ const showBuiltInVariantSelector = !renderVariantSelector && options && options.length > 0 && variants && selections && onSelectionChange;
9667
+ return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
9668
+ badges && badges.length > 0 && (React.createElement(BadgeList, { badges: badges, position: "inline" })),
9669
+ brand && (React.createElement("span", { className: "seekora-product-info-brand", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary)', textTransform: 'uppercase', letterSpacing: '0.02em' } }, brand)),
9670
+ React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
9671
+ (priceNum != null && !Number.isNaN(priceNum)) && (React.createElement("div", { className: "seekora-product-info-price" },
9672
+ React.createElement(PriceDisplay, { price: priceNum, comparePrice: comparePriceNum ?? undefined, currency: currency, style: { fontSize: '1.125rem' } }))),
9673
+ available != null && (React.createElement("span", { className: "seekora-product-info-availability", style: {
9674
+ fontSize: '0.875rem',
9675
+ color: available ? 'var(--seekora-success, #22c55e)' : 'var(--seekora-error, #ef4444)',
9676
+ } }, available ? 'In Stock' : 'Out of Stock')),
9677
+ description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
9678
+ renderVariantSelector ? renderVariantSelector() : null,
9679
+ showBuiltInVariantSelector && (React.createElement(VariantSelector, { options: options, variants: variants, selections: selections, onSelectionChange: onSelectionChange, showAvailability: true })),
9680
+ renderCTA?.()));
9681
+ }
9682
+
9683
+ /**
9684
+ * ProductRecommendations – related / frequently bought (primitive)
9685
+ *
9686
+ * Renders a section of recommended items (generic ItemGrid or product list).
9687
+ * Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
9688
+ * For use on individual product page.
9689
+ */
9690
+ function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
9691
+ if (!items?.length)
9692
+ return null;
9693
+ return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
9694
+ React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
9695
+ React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
9696
+ }
8188
9697
 
8189
9698
  /**
8190
9699
  * Responsive Utilities and Breakpoints
@@ -13875,6 +15384,164 @@ function formatParsedFilters(filters) {
13875
15384
  .join(', ');
13876
15385
  }
13877
15386
 
15387
+ /**
15388
+ * useVariantSelection – manages variant selection state
15389
+ *
15390
+ * Single source of truth for variant interactions on product pages.
15391
+ * Cards display variants passively; product pages use this hook.
15392
+ */
15393
+ function useVariantSelection({ options = [], variants = [], initialSelections = {}, onVariantChange, } = {}) {
15394
+ const [selections, setSelections] = useState(initialSelections);
15395
+ const selectedVariant = useMemo(() => findVariantBySelections(options, variants, selections), [options, variants, selections]);
15396
+ const isComplete = useMemo(() => options.length > 0 && options.every((opt) => !!selections[opt.name]), [options, selections]);
15397
+ const availableValues = useMemo(() => {
15398
+ const result = {};
15399
+ for (const option of options) {
15400
+ result[option.name] = getAvailableValuesForOption(option.name, options, variants, selections);
15401
+ }
15402
+ return result;
15403
+ }, [options, variants, selections]);
15404
+ const effectivePrice = selectedVariant?.price ?? null;
15405
+ const effectiveComparePrice = selectedVariant?.comparePrice ?? null;
15406
+ const setSelection = useCallback((optionName, value) => {
15407
+ setSelections((prev) => {
15408
+ const next = { ...prev, [optionName]: value };
15409
+ const variant = findVariantBySelections(options, variants, next);
15410
+ onVariantChange?.(variant, next);
15411
+ return next;
15412
+ });
15413
+ }, [options, variants, onVariantChange]);
15414
+ const resetSelections = useCallback(() => {
15415
+ setSelections({});
15416
+ onVariantChange?.(null, {});
15417
+ }, [onVariantChange]);
15418
+ return {
15419
+ selections,
15420
+ setSelection,
15421
+ resetSelections,
15422
+ selectedVariant,
15423
+ availableValues,
15424
+ isComplete,
15425
+ effectivePrice,
15426
+ effectiveComparePrice,
15427
+ };
15428
+ }
15429
+
15430
+ /**
15431
+ * useProductAnalytics – analytics event binding hook for custom components
15432
+ *
15433
+ * Returns event handler props ready to spread onto any element, plus imperative
15434
+ * tracking methods. Builds on existing useAnalytics infrastructure.
15435
+ */
15436
+ function useProductAnalytics({ client, product, position = 0, section, tabId, query, context, enabled = true, }) {
15437
+ const impressionTrackedRef = useRef(false);
15438
+ const observerRef = useRef(null);
15439
+ const elementRef = useRef(null);
15440
+ const productId = product.id || product.objectID || '';
15441
+ const sendEvent = useCallback(async (eventName, metadata) => {
15442
+ if (!enabled || !client)
15443
+ return;
15444
+ try {
15445
+ await client.trackEvent?.({
15446
+ event_name: eventName,
15447
+ metadata: {
15448
+ product_id: productId,
15449
+ product_title: product.title || product.name,
15450
+ product_price: product.price,
15451
+ position,
15452
+ section,
15453
+ tab_id: tabId,
15454
+ original_query: query,
15455
+ timestamp: Date.now(),
15456
+ source: 'product_analytics',
15457
+ ...metadata,
15458
+ },
15459
+ }, context);
15460
+ log.verbose(`ProductAnalytics: ${eventName}`, metadata);
15461
+ }
15462
+ catch (error) {
15463
+ log.warn(`Failed to track ${eventName}`, { error });
15464
+ }
15465
+ }, [client, enabled, productId, product, position, section, tabId, query, context]);
15466
+ const trackClick = useCallback(() => {
15467
+ sendEvent('product.click', {});
15468
+ // Also fire via client.trackClick for V3 compat
15469
+ if (client?.trackClick) {
15470
+ Promise.resolve(client.trackClick(productId, position + 1, context)).catch(() => { });
15471
+ }
15472
+ }, [sendEvent, client, productId, position, context]);
15473
+ const trackVariantSelect = useCallback((optionName, value) => {
15474
+ sendEvent('product.variant_select', {
15475
+ option_name: optionName,
15476
+ option_value: value,
15477
+ });
15478
+ }, [sendEvent]);
15479
+ const trackAddToCart = useCallback((variant) => {
15480
+ sendEvent('product.add_to_cart', {
15481
+ variant_id: variant?.id,
15482
+ variant_sku: variant?.sku,
15483
+ variant_title: variant?.title,
15484
+ variant_price: variant?.price,
15485
+ });
15486
+ }, [sendEvent]);
15487
+ const trackCustomEvent = useCallback((eventName, metadata) => {
15488
+ sendEvent(eventName, metadata ?? {});
15489
+ }, [sendEvent]);
15490
+ // Impression tracking via IntersectionObserver
15491
+ const impressionRef = useCallback((node) => {
15492
+ // Cleanup previous observer
15493
+ if (observerRef.current) {
15494
+ observerRef.current.disconnect();
15495
+ observerRef.current = null;
15496
+ }
15497
+ elementRef.current = node;
15498
+ if (!node || !enabled || impressionTrackedRef.current)
15499
+ return;
15500
+ if (typeof IntersectionObserver === 'undefined')
15501
+ return;
15502
+ observerRef.current = new IntersectionObserver((entries) => {
15503
+ for (const entry of entries) {
15504
+ if (entry.isIntersecting && !impressionTrackedRef.current) {
15505
+ impressionTrackedRef.current = true;
15506
+ sendEvent('product.impression', {});
15507
+ observerRef.current?.disconnect();
15508
+ }
15509
+ }
15510
+ }, { threshold: 0.5 });
15511
+ observerRef.current.observe(node);
15512
+ }, [enabled, sendEvent]);
15513
+ // Cleanup on unmount
15514
+ useEffect(() => {
15515
+ return () => {
15516
+ observerRef.current?.disconnect();
15517
+ };
15518
+ }, []);
15519
+ const clickProps = {
15520
+ onClick: () => trackClick(),
15521
+ 'data-seekora-product-id': productId,
15522
+ 'data-seekora-position': position,
15523
+ };
15524
+ const variantSelectProps = (optionName, value) => ({
15525
+ onClick: () => trackVariantSelect(optionName, value),
15526
+ 'data-seekora-variant-option': optionName,
15527
+ 'data-seekora-variant-value': value,
15528
+ });
15529
+ const addToCartProps = (variant) => ({
15530
+ onClick: () => trackAddToCart(variant),
15531
+ 'data-seekora-action': 'add-to-cart',
15532
+ });
15533
+ return {
15534
+ clickProps,
15535
+ variantSelectProps,
15536
+ addToCartProps,
15537
+ impressionRef,
15538
+ trackClick,
15539
+ trackVariantSelect,
15540
+ trackAddToCart,
15541
+ trackCustomEvent,
15542
+ };
15543
+ }
15544
+
13878
15545
  /**
13879
15546
  * Dark Theme
13880
15547
  */
@@ -14414,5 +16081,5 @@ function updateSuggestionsStyles(theme) {
14414
16081
  injectSuggestionsStyles(theme, true);
14415
16082
  }
14416
16083
 
14417
- export { AmazonDropdown, Breadcrumb, CategoriesTabs, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, DropdownPanel, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, ImageDisplay, InfiniteHits, ItemCard, ItemGrid, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, ProductCard, ProductGallery, ProductGrid, ProductInfo, ProductRecommendations, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RecentSearchesList, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchInput, SearchLayout, SearchProvider, SearchResults, SectionError, SectionItemGrid, SectionLoading, SectionSearchProvider, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionItem, SuggestionList, SuggestionSearchBar, SuggestionsDropdownComposition, SuggestionsError, SuggestionsLoading, SuggestionsProvider, TrendingItems, TrendingList, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBrand, extractCategory, extractProduct, extractSuggestion, formatParsedFilters, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getFingerprint, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, parseHighlightMarkup, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSectionSearchContext, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics, useSuggestionsContext };
16084
+ export { ActionButtons, AmazonDropdown, AnalyticsProvider, BadgeList, Breadcrumb, CategoriesTabs, ClearRefinements, CurrentRefinements, DocSearch, DocSearchButton, DropdownPanel, Facets, FederatedDropdown, Fingerprint, FrequentlyBoughtTogether, GoogleDropdown, HierarchicalMenu, Highlight$1 as Highlight, HitsPerPage, ImageDisplay, ImageZoom, InfiniteHits, ItemCard, ItemGrid, MinimalDropdown, MobileFilters, MobileFiltersButton, MobileSheetDropdown, Pagination, PinterestDropdown, PriceDisplay, ProductCard, ProductGallery, ProductGrid, ProductInfo, ProductRecommendations, QuerySuggestions, QuerySuggestionsDropdown, RangeInput, RangeSlider, RatingDisplay, RecentSearchesList, RecentlyViewed, RelatedProducts, RichQuerySuggestions, SearchBar, SearchBarWithSuggestions, SearchInput, SearchLayout, SearchProvider, SearchResults, SectionError, SectionItemGrid, SectionLoading, SectionSearchProvider, ShopifyDropdown, Snippet, SortBy, SpotlightDropdown, Stats, SuggestionDropdownVariants, SuggestionItem, SuggestionList, SuggestionSearchBar, SuggestionsDropdownComposition, SuggestionsError, SuggestionsLoading, SuggestionsProvider, TrendingItems, TrendingList, VariantSelector, VariantSwatches, addRecentSearch, addToRecentlyViewed, brandPresets, breakpoints, clearRecentSearches, clearSuggestionsCache, createSuggestionsCache, createSuggestionsTheme, createTheme, darkTheme, darkThemeVariables, defaultTheme, extractBadges, extractBrand, extractCategory, extractProduct, extractSuggestion, findVariantBySelections, formatParsedFilters, formatPriceRange, formatPrice as formatSuggestionPrice, generateSuggestionsStylesheet, getAvailableValuesForOption, getFingerprint, getPriceRange, getRecentSearches, getShortcutText, getSuggestionsCache, highlightText, injectGlobalResponsiveStyles, injectSuggestionsStyles, lightThemeVariables, mediaQueries, mergeThemes, minimalTheme, minimalThemeVariables, parseHighlightMarkup, removeRecentSearch, touchTargets, updateSuggestionsStyles, useAnalytics, useAnalyticsProvider, useDocSearch, useSeekoraSearch$1 as useDocSearchSeekoraSearch, useInjectResponsiveStyles, useKeyboard, useNaturalLanguageFilters, useProductAnalytics, useQuerySuggestions, useQuerySuggestionsEnhanced, useResponsive, useSearchContext, useSearchState, useSectionSearchContext, useSeekoraSearch, useSmartSuggestions, useSuggestionsAnalytics, useSuggestionsContext, useVariantSelection, withAnalytics };
14418
16085
  //# sourceMappingURL=index.esm.js.map