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