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

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 (102) hide show
  1. package/dist/components/CurrentRefinements.d.ts +22 -2
  2. package/dist/components/CurrentRefinements.d.ts.map +1 -1
  3. package/dist/components/CurrentRefinements.js +199 -47
  4. package/dist/components/Facets.d.ts +30 -1
  5. package/dist/components/Facets.d.ts.map +1 -1
  6. package/dist/components/Facets.js +418 -46
  7. package/dist/components/HierarchicalMenu.d.ts.map +1 -1
  8. package/dist/components/HierarchicalMenu.js +112 -4
  9. package/dist/components/InfiniteHits.d.ts +2 -0
  10. package/dist/components/InfiniteHits.d.ts.map +1 -1
  11. package/dist/components/InfiniteHits.js +6 -3
  12. package/dist/components/Pagination.d.ts +47 -1
  13. package/dist/components/Pagination.d.ts.map +1 -1
  14. package/dist/components/Pagination.js +166 -28
  15. package/dist/components/QuerySuggestions.d.ts +2 -0
  16. package/dist/components/QuerySuggestions.d.ts.map +1 -1
  17. package/dist/components/QuerySuggestions.js +4 -3
  18. package/dist/components/QuerySuggestionsDropdown.d.ts +1 -1
  19. package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
  20. package/dist/components/QuerySuggestionsDropdown.js +4 -4
  21. package/dist/components/RangeSlider.d.ts.map +1 -1
  22. package/dist/components/RangeSlider.js +49 -2
  23. package/dist/components/Recommendations.d.ts +6 -0
  24. package/dist/components/Recommendations.d.ts.map +1 -1
  25. package/dist/components/Recommendations.js +12 -6
  26. package/dist/components/RichQuerySuggestions.d.ts +11 -0
  27. package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
  28. package/dist/components/RichQuerySuggestions.js +2 -3
  29. package/dist/components/SearchBar.d.ts +18 -0
  30. package/dist/components/SearchBar.d.ts.map +1 -1
  31. package/dist/components/SearchBar.js +134 -24
  32. package/dist/components/SearchProvider.d.ts +8 -1
  33. package/dist/components/SearchProvider.d.ts.map +1 -1
  34. package/dist/components/SearchProvider.js +16 -4
  35. package/dist/components/SearchResults.d.ts +12 -0
  36. package/dist/components/SearchResults.d.ts.map +1 -1
  37. package/dist/components/SearchResults.js +11 -5
  38. package/dist/components/SortBy.d.ts +44 -4
  39. package/dist/components/SortBy.d.ts.map +1 -1
  40. package/dist/components/SortBy.js +154 -29
  41. package/dist/components/Stats.d.ts +14 -0
  42. package/dist/components/Stats.d.ts.map +1 -1
  43. package/dist/components/Stats.js +172 -23
  44. package/dist/components/section-primitives/SectionItemGrid.d.ts +3 -1
  45. package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
  46. package/dist/components/section-primitives/SectionItemGrid.js +3 -2
  47. package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
  48. package/dist/components/suggestions/AmazonDropdown.js +4 -6
  49. package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
  50. package/dist/components/suggestions/GoogleDropdown.js +4 -8
  51. package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
  52. package/dist/components/suggestions/MinimalDropdown.js +4 -6
  53. package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
  54. package/dist/components/suggestions/MobileSheetDropdown.js +4 -6
  55. package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
  56. package/dist/components/suggestions/PinterestDropdown.js +4 -8
  57. package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
  58. package/dist/components/suggestions/ShopifyDropdown.js +4 -6
  59. package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
  60. package/dist/components/suggestions/SpotlightDropdown.js +4 -6
  61. package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
  62. package/dist/components/suggestions/SuggestionSearchBar.js +1 -0
  63. package/dist/components/suggestions/types.d.ts +2 -0
  64. package/dist/components/suggestions/types.d.ts.map +1 -1
  65. package/dist/components/suggestions/utils.d.ts +10 -1
  66. package/dist/components/suggestions/utils.d.ts.map +1 -1
  67. package/dist/components/suggestions/utils.js +36 -0
  68. package/dist/components/suggestions-primitives/SuggestionList.d.ts +8 -1
  69. package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
  70. package/dist/components/suggestions-primitives/SuggestionList.js +7 -4
  71. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -1
  72. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +0 -2
  73. package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
  74. package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
  75. package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
  76. package/dist/docsearch/components/Results.d.ts +3 -1
  77. package/dist/docsearch/components/Results.d.ts.map +1 -1
  78. package/dist/docsearch/components/Results.js +6 -2
  79. package/dist/hooks/useClickTracking.d.ts +36 -0
  80. package/dist/hooks/useClickTracking.d.ts.map +1 -0
  81. package/dist/hooks/useClickTracking.js +96 -0
  82. package/dist/hooks/useExperiment.d.ts +25 -0
  83. package/dist/hooks/useExperiment.d.ts.map +1 -0
  84. package/dist/hooks/useExperiment.js +146 -0
  85. package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
  86. package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
  87. package/dist/hooks/useKeyboardNavigation.js +113 -0
  88. package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
  89. package/dist/hooks/useQuerySuggestions.js +19 -3
  90. package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
  91. package/dist/hooks/useQuerySuggestionsEnhanced.js +25 -7
  92. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  93. package/dist/hooks/useSuggestionsAnalytics.js +6 -1
  94. package/dist/index.d.ts +4 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.umd.js +1 -1
  97. package/dist/src/index.d.ts +249 -19
  98. package/dist/src/index.esm.js +1659 -305
  99. package/dist/src/index.esm.js.map +1 -1
  100. package/dist/src/index.js +1658 -304
  101. package/dist/src/index.js.map +1 -1
  102. package/package.json +3 -3
package/dist/src/index.js CHANGED
@@ -307,6 +307,9 @@ class SearchStateManager {
307
307
  this.autoSearch = config.autoSearch !== false;
308
308
  this.debounceMs = config.debounceMs || 300;
309
309
  this.defaultSearchOptions = config.defaultSearchOptions || { widget_mode: true };
310
+ this.keepResultsOnClear = config.keepResultsOnClear !== false;
311
+ this.abTestId = config.abTestId;
312
+ this.abVariant = config.abVariant;
310
313
  this.state = {
311
314
  query: config.initialQuery || '',
312
315
  refinements: [],
@@ -355,6 +358,15 @@ class SearchStateManager {
355
358
  }
356
359
  return;
357
360
  }
361
+ // When query is cleared to empty and keepResultsOnClear is true,
362
+ // preserve previous results and skip triggering a search
363
+ if (query === '' && this.keepResultsOnClear) {
364
+ log.verbose('SearchStateManager: Query cleared, keeping previous results');
365
+ this.state.query = '';
366
+ this.state.currentPage = 1;
367
+ this.notifyListeners();
368
+ return;
369
+ }
358
370
  this.state.query = query;
359
371
  this.state.currentPage = 1; // Reset to first page on new query
360
372
  log.verbose('SearchStateManager: Query updated', { query, triggerSearch, autoSearch: this.autoSearch });
@@ -553,6 +565,23 @@ class SearchStateManager {
553
565
  }
554
566
  });
555
567
  }
568
+ /** Explicitly clear results (bypasses keepResultsOnClear) */
569
+ clearResults() {
570
+ this.setState({ results: null, loading: false, error: null });
571
+ log.verbose('SearchStateManager: Results explicitly cleared');
572
+ }
573
+ // A/B test getters
574
+ getAbTestId() {
575
+ return this.abTestId;
576
+ }
577
+ getAbVariant() {
578
+ return this.abVariant;
579
+ }
580
+ setAbTest(abTestId, abVariant) {
581
+ this.abTestId = abTestId;
582
+ this.abVariant = abVariant;
583
+ log.verbose('SearchStateManager: A/B test updated', { abTestId, abVariant });
584
+ }
556
585
  // Clear all state
557
586
  clear() {
558
587
  this.state = {
@@ -1335,10 +1364,11 @@ const createTheme = (config) => {
1335
1364
  /**
1336
1365
  * SearchProvider Component
1337
1366
  *
1338
- * Provides Seekora client and context to child components
1367
+ * Provides Seekora client and context to child components.
1368
+ * Supports A/B testing via abTestId/abVariant props.
1339
1369
  */
1340
1370
  const SearchContext = React.createContext(null);
1341
- const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, }) => {
1371
+ const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, abTestId, abVariant, experiments: _experiments, }) => {
1342
1372
  const theme = React.useMemo(() => {
1343
1373
  return themeConfig ? createTheme(themeConfig) : defaultTheme;
1344
1374
  }, [themeConfig]);
@@ -1351,8 +1381,19 @@ const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, au
1351
1381
  itemsPerPage: 10,
1352
1382
  defaultSearchOptions: { widget_mode: true },
1353
1383
  ...stateManagerConfig,
1384
+ abTestId,
1385
+ abVariant,
1354
1386
  });
1355
- }, [client, stateManagerConfig]);
1387
+ }, [client, stateManagerConfig, abTestId, abVariant]);
1388
+ // Update A/B test fields on state manager and SDK client when props change
1389
+ React.useEffect(() => {
1390
+ if (abTestId !== undefined || abVariant !== undefined) {
1391
+ stateManager.setAbTest(abTestId, abVariant);
1392
+ if (typeof client.setAbTest === 'function') {
1393
+ client.setAbTest(abTestId, abVariant);
1394
+ }
1395
+ }
1396
+ }, [stateManager, client, abTestId, abVariant]);
1356
1397
  const value = React.useMemo(() => ({
1357
1398
  client,
1358
1399
  theme,
@@ -1485,9 +1526,25 @@ const useQuerySuggestions = ({ client, query, enabled = true, debounceMs = 300,
1485
1526
  }
1486
1527
  catch (err) {
1487
1528
  const error = err instanceof Error ? err : new Error(String(err));
1488
- log.error('Error in useQuerySuggestions:', error);
1489
- setError(error);
1490
- setSuggestions([]);
1529
+ // Check if it's a 404 error (suggestions not enabled for this store).
1530
+ // The search SDK wraps axios errors into plain Error objects with the status
1531
+ // embedded in the message, e.g. "[getSuggestions] ... (404)".
1532
+ const errMsg = error.message || '';
1533
+ const is404 = err?.response?.status === 404 ||
1534
+ err?.status === 404 ||
1535
+ /\(404\)/.test(errMsg);
1536
+ if (is404) {
1537
+ // Silently handle 404 - suggestions feature not enabled
1538
+ log.verbose('Query suggestions not enabled for this store (404)');
1539
+ setSuggestions([]);
1540
+ setError(null);
1541
+ }
1542
+ else {
1543
+ // For other errors, log and set error state
1544
+ log.error('Error in useQuerySuggestions:', error);
1545
+ setError(error);
1546
+ setSuggestions([]);
1547
+ }
1491
1548
  }
1492
1549
  finally {
1493
1550
  setLoading(false);
@@ -1513,7 +1570,42 @@ function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else i
1513
1570
  *
1514
1571
  * Interactive search input component with query suggestions support
1515
1572
  */
1516
- const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, renderSuggestion, renderLoading, }) => {
1573
+ const SIZE_CONFIG = {
1574
+ small: {
1575
+ padding: '0.375rem 0.5rem',
1576
+ fontSize: '0.875rem',
1577
+ iconSize: 14,
1578
+ iconPaddingLeft: '1.75rem',
1579
+ iconPaddingRight: '1.75rem',
1580
+ iconLeft: '0.5rem',
1581
+ iconRight: '0.5rem',
1582
+ },
1583
+ medium: {
1584
+ padding: '0.625rem 1rem',
1585
+ fontSize: '1rem',
1586
+ iconSize: 18,
1587
+ iconPaddingLeft: '2.25rem',
1588
+ iconPaddingRight: '2.25rem',
1589
+ iconLeft: '0.625rem',
1590
+ iconRight: '0.625rem',
1591
+ },
1592
+ large: {
1593
+ padding: '0.875rem 1.25rem',
1594
+ fontSize: '1.25rem',
1595
+ iconSize: 22,
1596
+ iconPaddingLeft: '2.75rem',
1597
+ iconPaddingRight: '2.75rem',
1598
+ iconLeft: '0.75rem',
1599
+ iconRight: '0.75rem',
1600
+ },
1601
+ };
1602
+ const DefaultSearchIcon = ({ size = 18 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
1603
+ React.createElement("circle", { cx: "11", cy: "11", r: "8" }),
1604
+ React.createElement("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })));
1605
+ const DefaultClearIcon = ({ size = 14 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
1606
+ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1607
+ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })));
1608
+ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, renderSearchIcon, showClearButton = true, renderClearIcon, showSubmitButton = false, renderSubmitButton, size = 'medium', }) => {
1517
1609
  const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
1518
1610
  const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
1519
1611
  const [isFocused, setIsFocused] = React.useState(false);
@@ -1646,40 +1738,112 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
1646
1738
  const defaultRenderLoading = () => (React.createElement("div", { style: { padding: theme.spacing.medium, textAlign: 'center' } }, "Loading suggestions..."));
1647
1739
  const searchBarTheme = customTheme || {};
1648
1740
  const isLoading = suggestionsLoading || searchLoading;
1649
- // Only show suggestions list if:
1650
- // 1. Input is focused
1651
- // 2. Suggestions are enabled
1652
- // 3. Query is long enough
1653
- // 4. AND (we have suggestions to show OR we're currently loading suggestions)
1741
+ // Show list when we have suggestions (including previous while loading) or when loading and showLoadingState
1654
1742
  const hasSuggestions = displayedSuggestions.length > 0;
1655
- const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || isLoading);
1743
+ const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || (isLoading && showLoadingState));
1656
1744
  // Get processing time from results
1657
1745
  const res = results;
1658
1746
  const processingTime = res?.processingTimeMS
1659
1747
  || res?.data?.processingTimeMS
1660
1748
  || res?.data?.data?.processingTimeMS;
1749
+ // Size-based configuration
1750
+ const sizeConfig = SIZE_CONFIG[size];
1751
+ const hasClearBtn = showClearButton && query.length > 0;
1752
+ // Compute input padding accounting for icons
1753
+ const inputPaddingLeft = sizeConfig.iconPaddingLeft ;
1754
+ const inputPaddingRight = hasClearBtn ? sizeConfig.iconPaddingRight : sizeConfig.padding.split(' ')[1] || sizeConfig.padding;
1755
+ const borderRadius = typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium;
1756
+ const focusBorderColor = isFocused ? theme.colors.focus : theme.colors.border;
1757
+ const focusRingShadow = isFocused
1758
+ ? `0 0 0 3px ${theme.colors.focus}33`
1759
+ : undefined;
1760
+ const handleClear = React.useCallback(() => {
1761
+ setQuery('', false);
1762
+ setSelectedIndex(-1);
1763
+ inputRef.current?.focus();
1764
+ }, [setQuery]);
1765
+ const handleSubmit = React.useCallback(() => {
1766
+ const currentValue = inputRef.current?.value || query;
1767
+ handleSearch(currentValue);
1768
+ }, [query, handleSearch]);
1661
1769
  return (React.createElement("div", { ref: containerRef, className: clsx(searchBarTheme.container, className), style: {
1662
1770
  position: 'relative',
1663
1771
  display: 'flex',
1664
1772
  alignItems: 'center',
1773
+ // CSS custom properties for external styling
1774
+ '--seekora-searchbar-bg': theme.colors.background,
1775
+ '--seekora-searchbar-border': theme.colors.border,
1776
+ '--seekora-searchbar-focus-border': theme.colors.focus,
1777
+ '--seekora-searchbar-radius': borderRadius,
1778
+ '--seekora-searchbar-icon-color': theme.colors.textSecondary,
1665
1779
  ...style,
1666
1780
  } },
1667
- React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
1668
- flex: 1,
1669
- padding: theme.spacing.medium,
1670
- fontSize: theme.typography.fontSize.medium,
1781
+ React.createElement("div", { style: { position: 'relative', flex: 1, display: 'flex', alignItems: 'center' } },
1782
+ (React.createElement("span", { className: searchBarTheme.searchIcon, "aria-hidden": "true", style: {
1783
+ position: 'absolute',
1784
+ left: sizeConfig.iconLeft,
1785
+ top: '50%',
1786
+ transform: 'translateY(-50%)',
1787
+ display: 'flex',
1788
+ alignItems: 'center',
1789
+ justifyContent: 'center',
1790
+ pointerEvents: 'none',
1791
+ color: 'var(--seekora-searchbar-icon-color)',
1792
+ zIndex: 1,
1793
+ } }, renderSearchIcon ? renderSearchIcon() : React.createElement(DefaultSearchIcon, { size: sizeConfig.iconSize }))),
1794
+ React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
1795
+ width: '100%',
1796
+ paddingTop: sizeConfig.padding.split(' ')[0],
1797
+ paddingBottom: sizeConfig.padding.split(' ')[0],
1798
+ paddingLeft: inputPaddingLeft,
1799
+ paddingRight: inputPaddingRight,
1800
+ fontSize: sizeConfig.fontSize,
1801
+ fontFamily: theme.typography.fontFamily,
1802
+ backgroundColor: 'var(--seekora-searchbar-bg)',
1803
+ color: theme.colors.text,
1804
+ borderWidth: '1px',
1805
+ borderStyle: 'solid',
1806
+ borderColor: focusBorderColor,
1807
+ borderRadius: 'var(--seekora-searchbar-radius)',
1808
+ outline: 'none',
1809
+ boxShadow: focusRingShadow,
1810
+ transition: theme.transitions?.fast || '150ms ease-in-out',
1811
+ boxSizing: 'border-box',
1812
+ } }),
1813
+ hasClearBtn && (React.createElement("button", { type: "button", onClick: handleClear, className: searchBarTheme.clearButton, "aria-label": "Clear search", style: {
1814
+ position: 'absolute',
1815
+ right: sizeConfig.iconRight,
1816
+ top: '50%',
1817
+ transform: 'translateY(-50%)',
1818
+ display: 'flex',
1819
+ alignItems: 'center',
1820
+ justifyContent: 'center',
1821
+ background: 'none',
1822
+ border: 'none',
1823
+ cursor: 'pointer',
1824
+ padding: '2px',
1825
+ borderRadius: '50%',
1826
+ color: 'var(--seekora-searchbar-icon-color)',
1827
+ transition: theme.transitions?.fast || '150ms ease-in-out',
1828
+ zIndex: 1,
1829
+ }, onMouseDown: (e) => {
1830
+ // Prevent input blur so the clear action doesn't race with blur handler
1831
+ e.preventDefault();
1832
+ } }, renderClearIcon ? renderClearIcon() : React.createElement(DefaultClearIcon, { size: sizeConfig.iconSize - 4 })))),
1833
+ showSubmitButton && (renderSubmitButton ? (React.createElement("div", { onClick: handleSubmit, style: { marginLeft: theme.spacing.small, cursor: 'pointer' } }, renderSubmitButton())) : (React.createElement("button", { type: "button", onClick: handleSubmit, className: searchBarTheme.submitButton, style: {
1834
+ marginLeft: theme.spacing.small,
1835
+ padding: sizeConfig.padding,
1836
+ fontSize: sizeConfig.fontSize,
1671
1837
  fontFamily: theme.typography.fontFamily,
1672
- backgroundColor: theme.colors.background,
1673
- color: theme.colors.text,
1674
- borderWidth: '1px',
1675
- borderStyle: 'solid',
1676
- borderColor: isFocused ? theme.colors.focus : theme.colors.border,
1677
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
1678
- outline: 'none',
1679
- ...(isFocused && {
1680
- boxShadow: theme.shadows.small,
1681
- }),
1682
- } }),
1838
+ fontWeight: theme.typography.fontWeight?.medium ?? 500,
1839
+ backgroundColor: theme.colors.primary,
1840
+ color: '#ffffff',
1841
+ border: 'none',
1842
+ borderRadius: 'var(--seekora-searchbar-radius)',
1843
+ cursor: 'pointer',
1844
+ whiteSpace: 'nowrap',
1845
+ transition: theme.transitions?.fast || '150ms ease-in-out',
1846
+ } }, "Search"))),
1683
1847
  processingTime !== undefined && (React.createElement("span", { style: {
1684
1848
  marginLeft: theme.spacing.small,
1685
1849
  fontSize: theme.typography.fontSize.small,
@@ -1703,8 +1867,8 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
1703
1867
  overflowY: 'auto',
1704
1868
  zIndex: 1000,
1705
1869
  } },
1706
- isLoading && (renderLoading ? renderLoading() : defaultRenderLoading()),
1707
- !isLoading && displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
1870
+ isLoading && displayedSuggestions.length === 0 && showLoadingState && (renderLoading ? renderLoading() : defaultRenderLoading()),
1871
+ displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
1708
1872
  const isSelected = index === selectedIndex;
1709
1873
  const renderFn = renderSuggestion || defaultRenderSuggestion;
1710
1874
  return (React.createElement("div", { key: index, className: clsx(searchBarTheme.suggestionItem, isSelected && searchBarTheme.suggestionItemActive), onClick: () => handleSuggestionSelect(suggestion.query), onMouseEnter: () => setSelectedIndex(index), style: {
@@ -1746,7 +1910,7 @@ const formatPrice$1 = (value, currency = '₹') => {
1746
1910
  }
1747
1911
  return String(value);
1748
1912
  };
1749
- const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, }) => {
1913
+ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, minHeight = '400px', minWidth = '100%', loadingOpacity = 0.7, }) => {
1750
1914
  const { theme, client, enableAnalytics } = useSearchContext();
1751
1915
  const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
1752
1916
  const searchResultsTheme = customTheme || {};
@@ -2090,6 +2254,10 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
2090
2254
  });
2091
2255
  // Determine container style based on view mode
2092
2256
  const containerStyle = {
2257
+ minHeight: `var(--seekora-results-min-height, ${minHeight})`,
2258
+ minWidth: `var(--seekora-results-min-width, ${minWidth})`,
2259
+ transition: 'opacity 200ms ease-in-out',
2260
+ opacity: loading && resultItems.length > 0 ? loadingOpacity : 1,
2093
2261
  ...style,
2094
2262
  };
2095
2263
  // Determine results list style based on view mode
@@ -2113,20 +2281,22 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
2113
2281
  hasError: !!error,
2114
2282
  isLoading: loading,
2115
2283
  });
2116
- if (loading) {
2284
+ // When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
2285
+ if (loading && resultItems.length === 0 && showLoadingState) {
2117
2286
  log.verbose('SearchResults: Rendering loading state');
2118
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
2287
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderLoading ? renderLoading() : defaultRenderLoading()));
2119
2288
  }
2289
+ // When loading with previous results, fall through and render them (with opacity transition)
2120
2290
  if (error) {
2121
2291
  log.error('SearchResults: Rendering error state', {
2122
2292
  error: error.message,
2123
2293
  stack: error.stack,
2124
2294
  });
2125
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderError ? renderError(error) : defaultRenderError(error)));
2295
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderError ? renderError(error) : defaultRenderError(error)));
2126
2296
  }
2127
2297
  if (!results || resultItems.length === 0) {
2128
2298
  log.verbose('SearchResults: No results to display');
2129
- return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: style }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
2299
+ return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
2130
2300
  }
2131
2301
  const renderFn = renderResult || defaultRenderResult;
2132
2302
  return (React.createElement("div", { ref: containerRef, className: clsx(searchResultsTheme.container, className), style: containerStyle, tabIndex: enableKeyboardNavigation ? 0 : undefined, onKeyDown: handleKeyDown, role: "listbox", "aria-label": "Search results", "aria-activedescendant": activeIndex >= 0 ? `result-${activeIndex}` : undefined },
@@ -2171,10 +2341,44 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
2171
2341
  * Stats Component
2172
2342
  *
2173
2343
  * Displays search statistics (total results, processing time, etc.)
2344
+ * Supports inline, badge, and detailed display variants.
2174
2345
  */
2175
- const Stats = ({ results, renderStats, className, style, theme: customTheme, showProcessingTime = false, showQuery = false, separator = ' • ', }) => {
2346
+ /** CSS class name used for the animated count fade transition */
2347
+ const ANIMATE_CLASS = 'seekora-stats-animate';
2348
+ /** Inline <style> for the count-change animation and CSS custom properties */
2349
+ const statsStyleId = 'seekora-stats-styles';
2350
+ function ensureStatsStyles() {
2351
+ if (typeof document === 'undefined')
2352
+ return;
2353
+ if (document.getElementById(statsStyleId))
2354
+ return;
2355
+ const style = document.createElement('style');
2356
+ style.id = statsStyleId;
2357
+ style.textContent = `
2358
+ .seekora-stats-root {
2359
+ --seekora-stats-color: inherit;
2360
+ --seekora-stats-bg: transparent;
2361
+ --seekora-stats-font-size: inherit;
2362
+ --seekora-stats-badge-bg: inherit;
2363
+ --seekora-stats-badge-color: inherit;
2364
+ }
2365
+ .${ANIMATE_CLASS} {
2366
+ opacity: 0;
2367
+ transition: opacity 250ms ease-in-out;
2368
+ }
2369
+ `;
2370
+ document.head.appendChild(style);
2371
+ }
2372
+ const Stats = ({ results: resultsProp, renderStats, className, style, theme: customTheme, showProcessingTime = false, showQuery = false, separator = ' \u2022 ', variant = 'inline', showResultCount = true, formatNumber, }) => {
2176
2373
  const { theme } = useSearchContext();
2374
+ const { results: stateResults } = useSearchState();
2177
2375
  const statsTheme = customTheme || {};
2376
+ // Use results from prop if provided, otherwise from state manager
2377
+ const results = resultsProp || stateResults;
2378
+ // Inject keyframe / transition styles once
2379
+ React.useEffect(() => {
2380
+ ensureStatsStyles();
2381
+ }, []);
2178
2382
  // Extract stats from results
2179
2383
  const res = results;
2180
2384
  const totalResults = res?.totalResults
@@ -2185,44 +2389,173 @@ const Stats = ({ results, renderStats, className, style, theme: customTheme, sho
2185
2389
  || res?.data?.processingTimeMS
2186
2390
  || res?.data?.data?.processingTimeMS;
2187
2391
  const query = (res?.query ?? '');
2188
- const defaultRenderStats = () => {
2392
+ // Number formatter
2393
+ const fmt = formatNumber || ((n) => n.toLocaleString());
2394
+ // Animated count change — toggle a CSS class briefly on totalResults change
2395
+ const countRef = React.useRef(null);
2396
+ const prevTotalRef = React.useRef(totalResults);
2397
+ const [animating, setAnimating] = React.useState(false);
2398
+ React.useEffect(() => {
2399
+ if (prevTotalRef.current !== totalResults) {
2400
+ prevTotalRef.current = totalResults;
2401
+ setAnimating(true);
2402
+ // After a frame, remove the class so the transition plays (opacity 0 -> 1)
2403
+ const raf = requestAnimationFrame(() => {
2404
+ setAnimating(false);
2405
+ });
2406
+ return () => cancelAnimationFrame(raf);
2407
+ }
2408
+ }, [totalResults]);
2409
+ // Helper: border radius value from theme
2410
+ const borderRadiusSmall = typeof theme.borderRadius === 'string'
2411
+ ? theme.borderRadius
2412
+ : theme.borderRadius.small;
2413
+ // ─── Custom render ────────────────────────────────────────
2414
+ if (renderStats) {
2415
+ return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: style }, renderStats({
2416
+ totalResults,
2417
+ processingTime,
2418
+ query,
2419
+ })));
2420
+ }
2421
+ // ─── Inline variant (original behaviour) ──────────────────
2422
+ if (variant === 'inline') {
2189
2423
  const parts = [];
2190
- if (totalResults > 0) {
2191
- parts.push(`${totalResults.toLocaleString()} result${totalResults !== 1 ? 's' : ''}`);
2192
- }
2193
- else {
2194
- parts.push('No results');
2424
+ if (showResultCount) {
2425
+ if (totalResults > 0) {
2426
+ parts.push(React.createElement("span", { key: "count", className: statsTheme.text },
2427
+ React.createElement("span", { ref: countRef, className: clsx(animating && ANIMATE_CLASS), style: { transition: 'opacity 250ms ease-in-out' } }, fmt(totalResults)),
2428
+ ` result${totalResults !== 1 ? 's' : ''}`));
2429
+ }
2430
+ else {
2431
+ parts.push(React.createElement("span", { key: "count", className: statsTheme.text }, "No results"));
2432
+ }
2195
2433
  }
2196
2434
  if (showProcessingTime && processingTime !== undefined) {
2197
- parts.push(`${processingTime}ms`);
2435
+ parts.push(React.createElement("span", { key: "time", className: statsTheme.text },
2436
+ fmt(processingTime),
2437
+ "ms"));
2198
2438
  }
2199
2439
  if (showQuery && query) {
2200
- parts.push(`for "${query}"`);
2440
+ parts.push(React.createElement("span", { key: "query", className: statsTheme.text },
2441
+ "for \"",
2442
+ query,
2443
+ "\""));
2201
2444
  }
2202
- return (React.createElement("div", { className: clsx(statsTheme.container, className), style: {
2203
- fontSize: theme.typography.fontSize.medium,
2204
- color: theme.colors.text,
2445
+ return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: {
2446
+ fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.medium + ')',
2447
+ color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
2448
+ backgroundColor: 'var(--seekora-stats-bg, transparent)',
2205
2449
  ...style,
2206
2450
  } }, parts.map((part, index) => (React.createElement("span", { key: index },
2207
- index > 0 && React.createElement("span", { className: statsTheme.separator, style: { margin: `0 ${theme.spacing.small}` } }, separator),
2208
- React.createElement("span", { className: statsTheme.text }, part))))));
2209
- };
2210
- if (renderStats) {
2211
- return (React.createElement("div", { className: clsx(statsTheme.container, className), style: style }, renderStats({
2212
- totalResults,
2213
- processingTime,
2214
- query,
2215
- })));
2451
+ index > 0 && (React.createElement("span", { className: statsTheme.separator, style: { margin: `0 ${theme.spacing.small}` } }, separator)),
2452
+ part)))));
2453
+ }
2454
+ // ─── Badge variant ────────────────────────────────────────
2455
+ if (variant === 'badge') {
2456
+ const badges = [];
2457
+ const badgeStyle = {
2458
+ display: 'inline-flex',
2459
+ alignItems: 'center',
2460
+ gap: theme.spacing.small,
2461
+ padding: `${theme.spacing.small} ${theme.spacing.medium}`,
2462
+ borderRadius: borderRadiusSmall,
2463
+ backgroundColor: `var(--seekora-stats-badge-bg, ${theme.colors.primary}1A)`, // 10 % opacity hex
2464
+ color: `var(--seekora-stats-badge-color, ${theme.colors.primary})`,
2465
+ fontSize: theme.typography.fontSize.small,
2466
+ fontWeight: 500,
2467
+ };
2468
+ if (showResultCount) {
2469
+ badges.push(React.createElement("span", { key: "count", className: clsx(statsTheme.badge), style: badgeStyle },
2470
+ React.createElement("span", { className: statsTheme.badgeLabel }, "Results"),
2471
+ React.createElement("span", { className: clsx(statsTheme.badgeValue, animating && ANIMATE_CLASS), style: { fontWeight: 600, transition: 'opacity 250ms ease-in-out' } }, totalResults > 0 ? fmt(totalResults) : '0')));
2472
+ }
2473
+ if (showQuery && query) {
2474
+ badges.push(React.createElement("span", { key: "query", className: clsx(statsTheme.badge), style: badgeStyle },
2475
+ React.createElement("span", { className: statsTheme.badgeLabel }, "Query"),
2476
+ React.createElement("span", { className: statsTheme.badgeValue, style: { fontWeight: 600 } }, query)));
2477
+ }
2478
+ if (showProcessingTime && processingTime !== undefined) {
2479
+ badges.push(React.createElement("span", { key: "time", className: clsx(statsTheme.badge), style: badgeStyle },
2480
+ React.createElement("span", { className: statsTheme.badgeLabel }, "Time"),
2481
+ React.createElement("span", { className: statsTheme.badgeValue, style: { fontWeight: 600 } },
2482
+ fmt(processingTime),
2483
+ "ms")));
2484
+ }
2485
+ return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: {
2486
+ display: 'flex',
2487
+ flexWrap: 'wrap',
2488
+ gap: theme.spacing.small,
2489
+ fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.small + ')',
2490
+ color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
2491
+ backgroundColor: 'var(--seekora-stats-bg, transparent)',
2492
+ ...style,
2493
+ } }, badges));
2216
2494
  }
2217
- return defaultRenderStats();
2495
+ // ─── Detailed variant ─────────────────────────────────────
2496
+ if (variant === 'detailed') {
2497
+ const rows = [];
2498
+ if (showResultCount) {
2499
+ rows.push({
2500
+ label: 'Total Results',
2501
+ value: (React.createElement("span", { className: clsx(animating && ANIMATE_CLASS), style: { transition: 'opacity 250ms ease-in-out' } }, fmt(totalResults))),
2502
+ });
2503
+ }
2504
+ if (showQuery && query) {
2505
+ rows.push({ label: 'Query', value: query });
2506
+ }
2507
+ if (showProcessingTime && processingTime !== undefined) {
2508
+ rows.push({ label: 'Time', value: `${fmt(processingTime)}ms` });
2509
+ }
2510
+ return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.detailed, statsTheme.container, className), style: {
2511
+ display: 'flex',
2512
+ flexDirection: 'column',
2513
+ gap: theme.spacing.small,
2514
+ fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.medium + ')',
2515
+ color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
2516
+ backgroundColor: 'var(--seekora-stats-bg, transparent)',
2517
+ ...style,
2518
+ } }, rows.map((row, index) => (React.createElement("div", { key: index, className: statsTheme.detailedRow, style: {
2519
+ display: 'flex',
2520
+ alignItems: 'center',
2521
+ gap: theme.spacing.small,
2522
+ } },
2523
+ React.createElement("span", { className: statsTheme.detailedLabel, style: {
2524
+ color: theme.colors.textSecondary,
2525
+ fontWeight: 500,
2526
+ } },
2527
+ row.label,
2528
+ ":"),
2529
+ React.createElement("span", { className: statsTheme.detailedValue, style: {
2530
+ color: theme.colors.text,
2531
+ fontWeight: 600,
2532
+ } }, row.value))))));
2533
+ }
2534
+ // Fallback (should not be reached)
2535
+ return null;
2218
2536
  };
2219
2537
 
2220
2538
  /**
2221
2539
  * Pagination Component
2222
2540
  *
2223
- * Displays pagination controls for search results
2541
+ * Displays pagination controls for search results.
2542
+ * Supports three display variants: numbered, load-more, and simple.
2543
+ *
2544
+ * CSS Variables (applied to the container element):
2545
+ * --seekora-pagination-bg
2546
+ * --seekora-pagination-color
2547
+ * --seekora-pagination-active-bg
2548
+ * --seekora-pagination-active-color
2549
+ * --seekora-pagination-border
2550
+ * --seekora-pagination-radius
2224
2551
  */
2225
- const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsPerPage: itemsPerPageProp, totalPages: totalPagesProp, onPageChange, maxPages = 7, showFirstLast = true, showPrevNext = true, renderPageButton, className, style, theme: customTheme, }) => {
2552
+ /** Size-specific style tokens */
2553
+ const SIZE_TOKENS = {
2554
+ small: { paddingKey: 'small', fontSizeKey: 'small', minWidth: '32px' },
2555
+ medium: { paddingKey: 'small', fontSizeKey: 'medium', minWidth: '40px' },
2556
+ large: { paddingKey: 'medium', fontSizeKey: 'large', minWidth: '48px' },
2557
+ };
2558
+ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsPerPage: itemsPerPageProp, totalPages: totalPagesProp, onPageChange, maxPages = 7, showFirstLast = true, showPrevNext = true, renderPageButton, className, style, theme: customTheme, variant = 'numbered', loadMoreText = 'Load More', size = 'medium', showPageInfo, previousLabel = 'Previous', nextLabel = 'Next', }) => {
2226
2559
  const { theme } = useSearchContext();
2227
2560
  const { results: stateResults, currentPage: stateCurrentPage, setPage } = useSearchState();
2228
2561
  const paginationTheme = customTheme || {};
@@ -2245,6 +2578,19 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
2245
2578
  || res?.data?.total_pages
2246
2579
  || res?.data?.data?.total_pages
2247
2580
  || Math.ceil(totalResults / itemsPerPage);
2581
+ // Resolve whether to show page info text
2582
+ const resolvedShowPageInfo = showPageInfo !== undefined
2583
+ ? showPageInfo
2584
+ : variant === 'simple';
2585
+ // Size tokens
2586
+ const sizeTokens = SIZE_TOKENS[size];
2587
+ // CSS variable aware helpers — allow overrides via custom properties
2588
+ const cssVarBg = 'var(--seekora-pagination-bg, ' + theme.colors.background + ')';
2589
+ const cssVarColor = 'var(--seekora-pagination-color, ' + theme.colors.text + ')';
2590
+ const cssVarActiveBg = 'var(--seekora-pagination-active-bg, ' + theme.colors.primary + ')';
2591
+ const cssVarActiveColor = 'var(--seekora-pagination-active-color, #fff)';
2592
+ const cssVarBorder = 'var(--seekora-pagination-border, ' + theme.colors.border + ')';
2593
+ const cssVarRadius = 'var(--seekora-pagination-radius, ' + (typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium) + ')';
2248
2594
  const handlePageChange = (page) => {
2249
2595
  if (page < 1 || page > totalPages || page === currentPage)
2250
2596
  return;
@@ -2255,22 +2601,26 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
2255
2601
  onPageChange(page);
2256
2602
  }
2257
2603
  };
2258
- const defaultRenderPageButton = (page, isActive, isDisabled) => (React.createElement("button", { type: "button", disabled: isDisabled, onClick: () => handlePageChange(page), className: clsx(paginationTheme.item, isActive && paginationTheme.itemActive, isDisabled), style: {
2259
- padding: theme.spacing.small,
2604
+ const defaultRenderPageButton = (page, isActive, isDisabled) => (React.createElement("button", { type: "button", disabled: isDisabled, onClick: () => handlePageChange(page), "aria-current": isActive ? 'page' : undefined, "aria-label": `Page ${page}`, className: clsx(paginationTheme.item, isActive && paginationTheme.itemActive, isDisabled), style: {
2605
+ padding: theme.spacing[sizeTokens.paddingKey],
2260
2606
  margin: `0 ${theme.spacing.small}`,
2261
- border: `1px solid ${theme.colors.border}`,
2262
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2263
- backgroundColor: isActive ? theme.colors.primary : theme.colors.background,
2264
- color: isActive ? '#fff' : theme.colors.text,
2607
+ border: `1px solid ${cssVarBorder}`,
2608
+ borderRadius: cssVarRadius,
2609
+ backgroundColor: isActive ? cssVarActiveBg : cssVarBg,
2610
+ color: isActive ? cssVarActiveColor : cssVarColor,
2265
2611
  cursor: 'pointer',
2266
2612
  opacity: 1,
2267
- fontSize: theme.typography.fontSize.medium,
2268
- minWidth: '40px',
2613
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2614
+ minWidth: sizeTokens.minWidth,
2269
2615
  ...(isActive && {
2270
2616
  fontWeight: 'bold',
2271
2617
  }),
2272
2618
  } }, page));
2273
- if (totalPages <= 1) {
2619
+ if (totalPages <= 1 && variant !== 'load-more') {
2620
+ return null;
2621
+ }
2622
+ // For load-more, hide when there are no more pages to load
2623
+ if (variant === 'load-more' && currentPage >= totalPages) {
2274
2624
  return null;
2275
2625
  }
2276
2626
  // Calculate page range to display
@@ -2303,6 +2653,94 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
2303
2653
  }
2304
2654
  return pages;
2305
2655
  };
2656
+ // ── Page info element ──────────────────────────────────────────────
2657
+ const pageInfoElement = resolvedShowPageInfo ? (React.createElement("span", { className: clsx(paginationTheme.pageInfo), style: {
2658
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2659
+ color: cssVarColor,
2660
+ padding: `0 ${theme.spacing.small}`,
2661
+ whiteSpace: 'nowrap',
2662
+ }, "aria-live": "polite" },
2663
+ "Page ",
2664
+ currentPage,
2665
+ " of ",
2666
+ totalPages)) : null;
2667
+ // ── Load More variant ──────────────────────────────────────────────
2668
+ if (variant === 'load-more') {
2669
+ const remaining = Math.max(0, totalResults - currentPage * itemsPerPage);
2670
+ return (React.createElement("nav", { className: clsx(paginationTheme.container, className), style: {
2671
+ display: 'flex',
2672
+ flexDirection: 'column',
2673
+ alignItems: 'center',
2674
+ gap: theme.spacing.small,
2675
+ ...style,
2676
+ }, "aria-label": "Pagination" },
2677
+ React.createElement("button", { type: "button", onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.loadMoreButton), style: {
2678
+ padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.large}`,
2679
+ border: 'none',
2680
+ borderRadius: cssVarRadius,
2681
+ backgroundColor: cssVarActiveBg,
2682
+ color: cssVarActiveColor,
2683
+ cursor: 'pointer',
2684
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2685
+ fontWeight: theme.typography.fontWeight?.medium ?? 500,
2686
+ transition: theme.transitions?.fast ?? '150ms ease-in-out',
2687
+ minWidth: sizeTokens.minWidth,
2688
+ }, "aria-label": remaining > 0 ? `${loadMoreText} (${remaining} remaining)` : loadMoreText },
2689
+ loadMoreText,
2690
+ remaining > 0 && (React.createElement("span", { className: clsx(paginationTheme.loadMoreText), style: {
2691
+ marginLeft: theme.spacing.small,
2692
+ opacity: 0.85,
2693
+ fontSize: theme.typography.fontSize.small,
2694
+ } },
2695
+ "(",
2696
+ remaining,
2697
+ " remaining)"))),
2698
+ pageInfoElement));
2699
+ }
2700
+ // ── Simple variant ─────────────────────────────────────────────────
2701
+ if (variant === 'simple') {
2702
+ return (React.createElement("nav", { className: clsx(paginationTheme.container, paginationTheme.simpleContainer, className), style: {
2703
+ display: 'flex',
2704
+ alignItems: 'center',
2705
+ justifyContent: 'center',
2706
+ gap: theme.spacing.medium,
2707
+ ...style,
2708
+ }, "aria-label": "Pagination" },
2709
+ React.createElement("button", { type: "button", disabled: currentPage === 1, onClick: () => handlePageChange(currentPage - 1), className: clsx(paginationTheme.simpleButton, currentPage === 1 && paginationTheme.itemDisabled), style: {
2710
+ padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.medium}`,
2711
+ border: `1px solid ${cssVarBorder}`,
2712
+ borderRadius: cssVarRadius,
2713
+ backgroundColor: cssVarBg,
2714
+ color: cssVarColor,
2715
+ cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
2716
+ opacity: currentPage === 1 ? 0.5 : 1,
2717
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2718
+ minWidth: sizeTokens.minWidth,
2719
+ transition: theme.transitions?.fast ?? '150ms ease-in-out',
2720
+ }, "aria-label": "Previous page" }, previousLabel),
2721
+ React.createElement("span", { className: clsx(paginationTheme.simpleText, paginationTheme.pageInfo), style: {
2722
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2723
+ color: cssVarColor,
2724
+ whiteSpace: 'nowrap',
2725
+ }, "aria-live": "polite" },
2726
+ "Page ",
2727
+ currentPage,
2728
+ " of ",
2729
+ totalPages),
2730
+ React.createElement("button", { type: "button", disabled: currentPage === totalPages, onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.simpleButton, currentPage === totalPages && paginationTheme.itemDisabled), style: {
2731
+ padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.medium}`,
2732
+ border: `1px solid ${cssVarBorder}`,
2733
+ borderRadius: cssVarRadius,
2734
+ backgroundColor: cssVarBg,
2735
+ color: cssVarColor,
2736
+ cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
2737
+ opacity: currentPage === totalPages ? 0.5 : 1,
2738
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2739
+ minWidth: sizeTokens.minWidth,
2740
+ transition: theme.transitions?.fast ?? '150ms ease-in-out',
2741
+ }, "aria-label": "Next page" }, nextLabel)));
2742
+ }
2743
+ // ── Numbered variant (default — original behavior) ─────────────────
2306
2744
  const pageNumbers = getPageNumbers();
2307
2745
  return (React.createElement("nav", { className: clsx(paginationTheme.container, className), style: style, "aria-label": "Pagination" },
2308
2746
  React.createElement("ul", { className: paginationTheme.list, style: {
@@ -2313,27 +2751,44 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
2313
2751
  padding: 0,
2314
2752
  margin: 0,
2315
2753
  flexWrap: 'wrap',
2754
+ }, tabIndex: 0, onKeyDown: (e) => {
2755
+ if (e.key === 'ArrowLeft') {
2756
+ e.preventDefault();
2757
+ handlePageChange(currentPage - 1);
2758
+ }
2759
+ else if (e.key === 'ArrowRight') {
2760
+ e.preventDefault();
2761
+ handlePageChange(currentPage + 1);
2762
+ }
2763
+ else if (e.key === 'Home') {
2764
+ e.preventDefault();
2765
+ handlePageChange(1);
2766
+ }
2767
+ else if (e.key === 'End') {
2768
+ e.preventDefault();
2769
+ handlePageChange(totalPages);
2770
+ }
2316
2771
  } },
2317
2772
  showPrevNext && (React.createElement("li", null,
2318
2773
  React.createElement("button", { type: "button", disabled: currentPage === 1, onClick: () => handlePageChange(currentPage - 1), className: clsx(paginationTheme.item, currentPage === 1 && paginationTheme.itemDisabled), style: {
2319
- padding: theme.spacing.small,
2774
+ padding: theme.spacing[sizeTokens.paddingKey],
2320
2775
  margin: `0 ${theme.spacing.small}`,
2321
- border: `1px solid ${theme.colors.border}`,
2322
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2323
- backgroundColor: theme.colors.background,
2324
- color: theme.colors.text,
2776
+ border: `1px solid ${cssVarBorder}`,
2777
+ borderRadius: cssVarRadius,
2778
+ backgroundColor: cssVarBg,
2779
+ color: cssVarColor,
2325
2780
  cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
2326
2781
  opacity: currentPage === 1 ? 0.5 : 1,
2327
- fontSize: theme.typography.fontSize.medium,
2328
- }, "aria-label": "Previous page" }, "Previous"))),
2782
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2783
+ }, "aria-label": "Previous page" }, previousLabel))),
2329
2784
  pageNumbers.map((page, index) => {
2330
2785
  if (page === 'ellipsis') {
2331
2786
  return (React.createElement("li", { key: `ellipsis-${index}` },
2332
2787
  React.createElement("span", { className: paginationTheme.ellipsis, style: {
2333
- padding: theme.spacing.small,
2788
+ padding: theme.spacing[sizeTokens.paddingKey],
2334
2789
  margin: `0 ${theme.spacing.small}`,
2335
- color: theme.colors.text,
2336
- fontSize: theme.typography.fontSize.medium,
2790
+ color: cssVarColor,
2791
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2337
2792
  } }, "...")));
2338
2793
  }
2339
2794
  const isActive = page === currentPage;
@@ -2344,89 +2799,274 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
2344
2799
  }),
2345
2800
  showPrevNext && (React.createElement("li", null,
2346
2801
  React.createElement("button", { type: "button", disabled: currentPage === totalPages, onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.item, currentPage === totalPages && paginationTheme.itemDisabled), style: {
2347
- padding: theme.spacing.small,
2802
+ padding: theme.spacing[sizeTokens.paddingKey],
2348
2803
  margin: `0 ${theme.spacing.small}`,
2349
- border: `1px solid ${theme.colors.border}`,
2350
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2351
- backgroundColor: theme.colors.background,
2352
- color: theme.colors.text,
2804
+ border: `1px solid ${cssVarBorder}`,
2805
+ borderRadius: cssVarRadius,
2806
+ backgroundColor: cssVarBg,
2807
+ color: cssVarColor,
2353
2808
  cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
2354
2809
  opacity: currentPage === totalPages ? 0.5 : 1,
2355
- fontSize: theme.typography.fontSize.medium,
2356
- }, "aria-label": "Next page" }, "Next"))))));
2810
+ fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
2811
+ }, "aria-label": "Next page" }, nextLabel))),
2812
+ resolvedShowPageInfo && (React.createElement("li", { style: { marginLeft: theme.spacing.small } }, pageInfoElement)))));
2357
2813
  };
2358
2814
 
2359
2815
  /**
2360
2816
  * SortBy Component
2361
2817
  *
2362
- * Displays sort options for search results
2363
- * Integrates with SearchStateManager for automatic state sync
2818
+ * Displays sort options for search results in multiple display variants:
2819
+ * - dropdown (default) native select element
2820
+ * - button-group — horizontal row of toggle buttons
2821
+ * - radio-group — vertical list of radio inputs
2822
+ *
2823
+ * Integrates with SearchStateManager for automatic state sync.
2824
+ *
2825
+ * CSS Variables (apply on a parent element to customize):
2826
+ * --seekora-sort-bg — background color
2827
+ * --seekora-sort-color — text color
2828
+ * --seekora-sort-border — border color
2829
+ * --seekora-sort-active-bg — active item background
2830
+ * --seekora-sort-active-color — active item text color
2364
2831
  */
2365
- const SortBy = ({ options, value: valueProp, defaultValue, onSortChange, renderSelect, className, style, theme: customTheme, placeholder = 'Sort by...', syncWithState = true, }) => {
2832
+ // ---------------------------------------------------------------------------
2833
+ // Helpers
2834
+ // ---------------------------------------------------------------------------
2835
+ /** Map a size token to theme fontSize and spacing values. */
2836
+ function sizeStyles(size, theme) {
2837
+ switch (size) {
2838
+ case 'small':
2839
+ return { fontSize: theme.typography.fontSize.small, padding: theme.spacing.small };
2840
+ case 'large':
2841
+ return { fontSize: theme.typography.fontSize.large, padding: theme.spacing.medium };
2842
+ case 'medium':
2843
+ default:
2844
+ return { fontSize: theme.typography.fontSize.medium, padding: theme.spacing.small };
2845
+ }
2846
+ }
2847
+ function resolveBorderRadius(br) {
2848
+ return typeof br === 'string' ? br : br.medium;
2849
+ }
2850
+ // ---------------------------------------------------------------------------
2851
+ // Component
2852
+ // ---------------------------------------------------------------------------
2853
+ const SortBy = ({ options, value: valueProp, defaultValue, onSortChange, renderSelect, className, style, theme: customTheme, placeholder = 'Sort by...', syncWithState = true, variant = 'dropdown', label, showLabel, size = 'medium', }) => {
2366
2854
  const { theme } = useSearchContext();
2367
2855
  const { sortBy: stateManagerSortBy, setSortBy } = useSearchState();
2368
2856
  const sortByTheme = customTheme || {};
2369
- // Initialize with defaultValue or first option
2857
+ const instanceId = React.useId();
2858
+ // Determine whether the label should render.
2859
+ const shouldShowLabel = showLabel !== undefined ? showLabel : !!label;
2860
+ // ------ State ----------------------------------------------------------
2370
2861
  const [internalValue, setInternalValue] = React.useState(defaultValue || options[0]?.value || '');
2371
2862
  // Sync with StateManager on mount if defaultValue is set
2372
2863
  React.useEffect(() => {
2373
2864
  if (syncWithState && defaultValue && !stateManagerSortBy) {
2374
2865
  setSortBy(defaultValue, false); // Don't trigger search on initial sync
2375
2866
  }
2376
- }, []);
2867
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
2377
2868
  // Determine the current value: controlled prop > StateManager > internal
2378
2869
  const value = valueProp !== undefined
2379
2870
  ? valueProp
2380
- : (syncWithState && stateManagerSortBy)
2871
+ : syncWithState && stateManagerSortBy
2381
2872
  ? stateManagerSortBy
2382
2873
  : internalValue;
2874
+ // ------ Handlers -------------------------------------------------------
2383
2875
  const handleChange = (e) => {
2384
- const newValue = e.target.value;
2876
+ applyValue(e.target.value);
2877
+ };
2878
+ const applyValue = React.useCallback((newValue) => {
2385
2879
  setInternalValue(newValue);
2386
- // Update StateManager (automatically triggers search)
2387
2880
  if (syncWithState) {
2388
2881
  setSortBy(newValue);
2389
2882
  }
2390
- // Call callback for backwards compatibility
2391
2883
  if (onSortChange) {
2392
2884
  onSortChange(newValue);
2393
2885
  }
2886
+ }, [syncWithState, setSortBy, onSortChange]);
2887
+ // ------ Derived styles -------------------------------------------------
2888
+ const { fontSize, padding } = sizeStyles(size, theme);
2889
+ const borderRadius = resolveBorderRadius(theme.borderRadius);
2890
+ const cssVarStyle = {
2891
+ '--seekora-sort-bg': theme.colors.background,
2892
+ '--seekora-sort-color': theme.colors.text,
2893
+ '--seekora-sort-border': theme.colors.border,
2894
+ '--seekora-sort-active-bg': theme.colors.primary,
2895
+ '--seekora-sort-active-color': theme.colors.background,
2394
2896
  };
2395
- const defaultRenderSelect = () => (React.createElement("select", { value: value, onChange: handleChange, className: clsx(sortByTheme.select, className), style: {
2396
- padding: theme.spacing.small,
2397
- paddingRight: theme.spacing.medium,
2398
- fontSize: theme.typography.fontSize.medium,
2399
- border: `1px solid ${theme.colors.border}`,
2400
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2401
- backgroundColor: theme.colors.background,
2402
- color: theme.colors.text,
2403
- cursor: 'pointer',
2404
- outline: 'none',
2405
- ...style,
2406
- }, "aria-label": "Sort results" }, options.map((option) => (React.createElement("option", { key: option.value, value: option.value, className: sortByTheme.option }, option.label)))));
2407
- if (renderSelect) {
2408
- return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: style }, renderSelect({
2409
- value,
2410
- onChange: handleChange,
2411
- options,
2412
- })));
2897
+ // ------ Label ----------------------------------------------------------
2898
+ const labelElement = shouldShowLabel && label ? (React.createElement("span", { className: clsx(sortByTheme.label), style: {
2899
+ display: 'block',
2900
+ marginBottom: theme.spacing.small,
2901
+ fontSize,
2902
+ color: 'var(--seekora-sort-color)',
2903
+ fontWeight: 500,
2904
+ } }, label)) : null;
2905
+ // ------ Dropdown variant (original) ------------------------------------
2906
+ if (variant === 'dropdown') {
2907
+ if (renderSelect) {
2908
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
2909
+ labelElement,
2910
+ renderSelect({
2911
+ value,
2912
+ onChange: handleChange,
2913
+ options,
2914
+ })));
2915
+ }
2916
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
2917
+ labelElement,
2918
+ React.createElement("select", { value: value, onChange: handleChange, className: clsx(sortByTheme.select), style: {
2919
+ padding,
2920
+ paddingRight: theme.spacing.medium,
2921
+ fontSize,
2922
+ border: '1px solid var(--seekora-sort-border)',
2923
+ borderRadius,
2924
+ backgroundColor: 'var(--seekora-sort-bg)',
2925
+ color: 'var(--seekora-sort-color)',
2926
+ cursor: 'pointer',
2927
+ outline: 'none',
2928
+ width: '100%',
2929
+ }, "aria-label": label || 'Sort results' }, options.map((option) => (React.createElement("option", { key: option.value, value: option.value, className: sortByTheme.option }, option.label))))));
2413
2930
  }
2414
- return defaultRenderSelect();
2931
+ // ------ Button group variant -------------------------------------------
2932
+ if (variant === 'button-group') {
2933
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
2934
+ labelElement,
2935
+ React.createElement("div", { role: "group", "aria-label": label || 'Sort results', className: clsx(sortByTheme.buttonGroup), style: {
2936
+ display: 'inline-flex',
2937
+ borderRadius,
2938
+ overflow: 'hidden',
2939
+ border: '1px solid var(--seekora-sort-border)',
2940
+ } }, options.map((option, index) => {
2941
+ const isActive = option.value === value;
2942
+ return (React.createElement("button", { key: option.value, type: "button", role: "button", "aria-pressed": isActive, onClick: () => applyValue(option.value), className: clsx(sortByTheme.buttonGroupItem, isActive && sortByTheme.buttonGroupItemActive), style: {
2943
+ padding,
2944
+ fontSize,
2945
+ border: 'none',
2946
+ borderRight: index < options.length - 1
2947
+ ? '1px solid var(--seekora-sort-border)'
2948
+ : 'none',
2949
+ backgroundColor: isActive
2950
+ ? 'var(--seekora-sort-active-bg)'
2951
+ : 'var(--seekora-sort-bg)',
2952
+ color: isActive
2953
+ ? 'var(--seekora-sort-active-color)'
2954
+ : 'var(--seekora-sort-color)',
2955
+ cursor: 'pointer',
2956
+ fontWeight: isActive ? 600 : 400,
2957
+ transition: 'background-color 0.15s ease, color 0.15s ease',
2958
+ outline: 'none',
2959
+ } }, option.label));
2960
+ }))));
2961
+ }
2962
+ // ------ Radio group variant --------------------------------------------
2963
+ if (variant === 'radio-group') {
2964
+ const radioName = `seekora-sort-${instanceId}`;
2965
+ return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
2966
+ labelElement,
2967
+ React.createElement("div", { role: "radiogroup", "aria-label": label || 'Sort results', className: clsx(sortByTheme.radioGroup), style: {
2968
+ display: 'flex',
2969
+ flexDirection: 'column',
2970
+ gap: theme.spacing.small,
2971
+ } }, options.map((option) => {
2972
+ const isActive = option.value === value;
2973
+ const radioId = `${radioName}-${option.value}`;
2974
+ return (React.createElement("label", { key: option.value, htmlFor: radioId, className: clsx(sortByTheme.radioItem, isActive && sortByTheme.radioItemActive), style: {
2975
+ display: 'flex',
2976
+ alignItems: 'center',
2977
+ gap: theme.spacing.small,
2978
+ padding,
2979
+ borderRadius,
2980
+ cursor: 'pointer',
2981
+ backgroundColor: isActive
2982
+ ? 'var(--seekora-sort-active-bg)'
2983
+ : 'transparent',
2984
+ color: isActive
2985
+ ? 'var(--seekora-sort-active-color)'
2986
+ : 'var(--seekora-sort-color)',
2987
+ fontWeight: isActive ? 600 : 400,
2988
+ transition: 'background-color 0.15s ease, color 0.15s ease',
2989
+ } },
2990
+ React.createElement("input", { type: "radio", id: radioId, name: radioName, value: option.value, checked: isActive, onChange: () => applyValue(option.value), style: { margin: 0 } }),
2991
+ React.createElement("span", { className: clsx(sortByTheme.radioLabel), style: { fontSize } }, option.label)));
2992
+ }))));
2993
+ }
2994
+ // Fallback — should never reach here, but satisfies TS exhaustiveness
2995
+ return null;
2415
2996
  };
2416
2997
 
2417
2998
  /**
2418
2999
  * Facets Component
2419
3000
  *
2420
- * Displays facet filters for search results
3001
+ * Displays facet filters for search results with multiple display variants,
3002
+ * client-side search, count badges, and color swatch support.
2421
3003
  */
2422
- const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, }) => {
3004
+ // ---------------------------------------------------------------------------
3005
+ // Helpers
3006
+ // ---------------------------------------------------------------------------
3007
+ /** Generate a deterministic colour from a string (used as fallback for color-swatch). */
3008
+ function stringToColor(str) {
3009
+ let hash = 0;
3010
+ for (let i = 0; i < str.length; i++) {
3011
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
3012
+ hash |= 0; // Convert to 32-bit int
3013
+ }
3014
+ const h = Math.abs(hash) % 360;
3015
+ return `hsl(${h}, 65%, 55%)`;
3016
+ }
3017
+ /** Return swatch pixel size from the size prop. */
3018
+ function swatchSize(size) {
3019
+ switch (size) {
3020
+ case 'small':
3021
+ return 24;
3022
+ case 'large':
3023
+ return 40;
3024
+ case 'medium':
3025
+ default:
3026
+ return 32;
3027
+ }
3028
+ }
3029
+ // ---------------------------------------------------------------------------
3030
+ // Chevron SVG icon for collapsible variant
3031
+ // ---------------------------------------------------------------------------
3032
+ const ChevronIcon$1 = ({ expanded, color = 'currentColor', size = 16, }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
3033
+ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
3034
+ transition: 'transform 200ms ease',
3035
+ flexShrink: 0,
3036
+ }, "aria-hidden": "true" },
3037
+ React.createElement("path", { d: "M4 6L8 10L12 6", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })));
3038
+ // ---------------------------------------------------------------------------
3039
+ // Checkmark SVG for selected color swatches
3040
+ // ---------------------------------------------------------------------------
3041
+ const CheckmarkIcon = ({ size = 16 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
3042
+ React.createElement("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })));
3043
+ // ---------------------------------------------------------------------------
3044
+ // CSS variable defaults
3045
+ // ---------------------------------------------------------------------------
3046
+ const CSS_VAR_DEFAULTS = {
3047
+ '--seekora-facet-bg': '#ffffff',
3048
+ '--seekora-facet-border': '#dee2e6',
3049
+ '--seekora-facet-active-bg': '#f0f7ff',
3050
+ '--seekora-facet-swatch-size': '32px',
3051
+ '--seekora-facet-count-bg': '#e9ecef',
3052
+ '--seekora-facet-count-color': '#495057',
3053
+ };
3054
+ // ---------------------------------------------------------------------------
3055
+ // Component
3056
+ // ---------------------------------------------------------------------------
3057
+ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, variant = 'checkbox', searchable = false, showCounts = true, colorMap, defaultCollapsed = false, size = 'medium', }) => {
2423
3058
  const { theme } = useSearchContext();
2424
3059
  const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
2425
3060
  const facetsTheme = customTheme || {};
3061
+ // expandedFacets is used for "Show more/less" in checkbox/color-swatch variants
3062
+ // AND for collapse/expand in collapsible variant.
2426
3063
  const [expandedFacets, setExpandedFacets] = React.useState({});
3064
+ const [searchTerms, setSearchTerms] = React.useState({});
2427
3065
  // Use results from prop if provided, otherwise from state manager
2428
3066
  const results = resultsProp || stateResults;
3067
+ // -------------------------------------------------------------------
2429
3068
  // Extract facets from results
3069
+ // -------------------------------------------------------------------
2430
3070
  const extractFacets = () => {
2431
3071
  if (facetsProp)
2432
3072
  return facetsProp;
@@ -2473,6 +3113,9 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
2473
3113
  return extracted;
2474
3114
  };
2475
3115
  const facets = extractFacets();
3116
+ // -------------------------------------------------------------------
3117
+ // Handlers
3118
+ // -------------------------------------------------------------------
2476
3119
  const handleFacetToggle = (field, value, selected) => {
2477
3120
  const newSelected = !selected;
2478
3121
  log.verbose('Facets: Facet toggled', { field, value, selected: newSelected });
@@ -2505,49 +3148,258 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
2505
3148
  [field]: !prev[field],
2506
3149
  }));
2507
3150
  };
3151
+ /** For collapsible variant — determine if a facet group is open. */
3152
+ const isFacetGroupOpen = (field) => {
3153
+ if (field in expandedFacets) {
3154
+ return expandedFacets[field];
3155
+ }
3156
+ // Default based on defaultCollapsed prop
3157
+ return !defaultCollapsed;
3158
+ };
3159
+ const toggleCollapsible = (field) => {
3160
+ setExpandedFacets((prev) => ({
3161
+ ...prev,
3162
+ [field]: !(prev[field] ?? !defaultCollapsed),
3163
+ }));
3164
+ };
3165
+ const getSearchTerm = (field) => searchTerms[field] || '';
3166
+ const setSearchTerm = (field, term) => {
3167
+ setSearchTerms((prev) => ({ ...prev, [field]: term }));
3168
+ };
3169
+ /** Filter facet items by search term. */
3170
+ const filterItems = (items, field) => {
3171
+ if (!searchable)
3172
+ return items;
3173
+ const term = getSearchTerm(field).toLowerCase();
3174
+ if (!term)
3175
+ return items;
3176
+ return items.filter((item) => item.value.toLowerCase().includes(term));
3177
+ };
3178
+ // -------------------------------------------------------------------
3179
+ // Size helpers
3180
+ // -------------------------------------------------------------------
3181
+ const sizeScale = React.useMemo(() => {
3182
+ switch (size) {
3183
+ case 'small':
3184
+ return { font: theme.typography.fontSize.small, padding: '0.25rem', gap: '0.25rem' };
3185
+ case 'large':
3186
+ return { font: theme.typography.fontSize.large, padding: '0.75rem', gap: '0.75rem' };
3187
+ case 'medium':
3188
+ default:
3189
+ return { font: theme.typography.fontSize.medium, padding: theme.spacing.small, gap: theme.spacing.small };
3190
+ }
3191
+ }, [size, theme]);
3192
+ // -------------------------------------------------------------------
3193
+ // Count badge renderer
3194
+ // -------------------------------------------------------------------
3195
+ const renderCountBadge = (count) => {
3196
+ if (!showCounts)
3197
+ return null;
3198
+ return (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
3199
+ display: 'inline-flex',
3200
+ alignItems: 'center',
3201
+ justifyContent: 'center',
3202
+ minWidth: '1.5em',
3203
+ padding: '0.1em 0.5em',
3204
+ marginLeft: sizeScale.gap,
3205
+ fontSize: theme.typography.fontSize.small,
3206
+ fontWeight: theme.typography.fontWeight?.medium ?? 500,
3207
+ lineHeight: 1,
3208
+ color: 'var(--seekora-facet-count-color, #495057)',
3209
+ backgroundColor: 'var(--seekora-facet-count-bg, #e9ecef)',
3210
+ borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.full,
3211
+ flexShrink: 0,
3212
+ } }, count));
3213
+ };
3214
+ // -------------------------------------------------------------------
3215
+ // Search input renderer
3216
+ // -------------------------------------------------------------------
3217
+ const renderSearchInput = (facet) => {
3218
+ if (!searchable)
3219
+ return null;
3220
+ return (React.createElement("input", { type: "text", value: getSearchTerm(facet.field), onChange: (e) => setSearchTerm(facet.field, e.target.value), placeholder: `Search ${facet.label || facet.field}...`, className: clsx(facetsTheme.searchInput), "aria-label": `Search within ${facet.label || facet.field}`, style: {
3221
+ width: '100%',
3222
+ boxSizing: 'border-box',
3223
+ padding: sizeScale.padding,
3224
+ marginBottom: sizeScale.gap,
3225
+ fontSize: theme.typography.fontSize.small,
3226
+ border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
3227
+ borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.small,
3228
+ outline: 'none',
3229
+ color: theme.colors.text,
3230
+ backgroundColor: 'var(--seekora-facet-bg, transparent)',
3231
+ } }));
3232
+ };
3233
+ // -------------------------------------------------------------------
3234
+ // Checkbox variant item renderer (original behaviour, preserved)
3235
+ // -------------------------------------------------------------------
2508
3236
  const defaultRenderFacetItem = (item, facet, index) => {
2509
3237
  const isExpanded = expandedFacets[facet.field] || index < maxItems;
2510
3238
  if (!isExpanded && index >= maxItems) {
2511
3239
  return null;
2512
3240
  }
2513
- return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) && facetsTheme.facetItemActive), onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), style: {
3241
+ const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
3242
+ return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, isChecked && facetsTheme.facetItemActive), role: "option", "aria-selected": isChecked, "aria-checked": isChecked, tabIndex: -1, onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), style: {
2514
3243
  display: 'flex',
2515
3244
  alignItems: 'center',
2516
- padding: theme.spacing.small,
3245
+ padding: sizeScale.padding,
2517
3246
  cursor: 'pointer',
2518
3247
  borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2519
- marginBottom: theme.spacing.small,
2520
- backgroundColor: (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) ? theme.colors.hover : 'transparent',
3248
+ marginBottom: sizeScale.gap,
3249
+ backgroundColor: isChecked
3250
+ ? 'var(--seekora-facet-active-bg, ' + theme.colors.hover + ')'
3251
+ : 'transparent',
2521
3252
  transition: 'background-color 0.2s ease',
2522
3253
  } },
2523
3254
  React.createElement("input", { type: "checkbox", checked: refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false, onChange: () => handleFacetToggle(facet.field, item.value, item.selected || false), className: facetsTheme.checkbox, style: {
2524
- marginRight: theme.spacing.small,
3255
+ marginRight: sizeScale.gap,
2525
3256
  cursor: 'pointer',
2526
3257
  }, "aria-label": `Filter by ${item.value}` }),
2527
3258
  React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
2528
3259
  flex: 1,
2529
- fontSize: theme.typography.fontSize.medium,
3260
+ fontSize: sizeScale.font,
3261
+ color: theme.colors.text,
3262
+ } }, item.value),
3263
+ renderCountBadge(item.count)));
3264
+ };
3265
+ // -------------------------------------------------------------------
3266
+ // Color swatch variant item renderer
3267
+ // -------------------------------------------------------------------
3268
+ const renderColorSwatchItem = (item, facet, index) => {
3269
+ const isExpanded = expandedFacets[facet.field] || index < maxItems;
3270
+ if (!isExpanded && index >= maxItems) {
3271
+ return null;
3272
+ }
3273
+ const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
3274
+ const color = colorMap?.[item.value] ?? stringToColor(item.value);
3275
+ const pxSize = swatchSize(size);
3276
+ return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, isChecked && facetsTheme.facetItemActive), role: "option", "aria-selected": isChecked, "aria-checked": isChecked, tabIndex: -1, onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), title: `${item.value}${showCounts ? ` (${item.count})` : ''}`, style: {
3277
+ display: 'inline-flex',
3278
+ flexDirection: 'column',
3279
+ alignItems: 'center',
3280
+ cursor: 'pointer',
3281
+ margin: sizeScale.gap,
3282
+ } },
3283
+ React.createElement("div", { className: clsx(facetsTheme.colorSwatch, isChecked && facetsTheme.colorSwatchSelected), style: {
3284
+ width: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
3285
+ height: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
3286
+ borderRadius: '50%',
3287
+ backgroundColor: color,
3288
+ border: isChecked
3289
+ ? `3px solid ${theme.colors.primary}`
3290
+ : `2px solid var(--seekora-facet-border, ${theme.colors.border})`,
3291
+ display: 'flex',
3292
+ alignItems: 'center',
3293
+ justifyContent: 'center',
3294
+ transition: 'border 0.2s ease, box-shadow 0.2s ease',
3295
+ boxShadow: isChecked ? `0 0 0 2px ${theme.colors.primary}33` : 'none',
3296
+ position: 'relative',
3297
+ } }, isChecked && (React.createElement("span", { className: clsx(facetsTheme.colorSwatchInner), style: {
3298
+ display: 'flex',
3299
+ alignItems: 'center',
3300
+ justifyContent: 'center',
3301
+ } },
3302
+ React.createElement(CheckmarkIcon, { size: Math.round(pxSize * 0.5) })))),
3303
+ React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
3304
+ fontSize: theme.typography.fontSize.small,
2530
3305
  color: theme.colors.text,
3306
+ marginTop: '0.25rem',
3307
+ textAlign: 'center',
3308
+ maxWidth: `${pxSize + 16}px`,
3309
+ overflow: 'hidden',
3310
+ textOverflow: 'ellipsis',
3311
+ whiteSpace: 'nowrap',
2531
3312
  } }, item.value),
2532
- React.createElement("span", { className: facetsTheme.facetItemCount, style: {
3313
+ showCounts && (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
2533
3314
  fontSize: theme.typography.fontSize.small,
2534
- color: theme.colors.textSecondary || theme.colors.text,
2535
- opacity: 0.7,
2536
- marginLeft: theme.spacing.small,
3315
+ color: 'var(--seekora-facet-count-color, ' + (theme.colors.textSecondary || theme.colors.text) + ')',
3316
+ lineHeight: 1,
3317
+ marginTop: '0.125rem',
3318
+ } }, item.count))));
3319
+ };
3320
+ // -------------------------------------------------------------------
3321
+ // Item renderer dispatcher
3322
+ // -------------------------------------------------------------------
3323
+ const renderItem = (item, facet, index) => {
3324
+ if (renderFacetItem) {
3325
+ return renderFacetItem(item, facet, index);
3326
+ }
3327
+ switch (variant) {
3328
+ case 'color-swatch':
3329
+ return renderColorSwatchItem(item, facet, index);
3330
+ case 'collapsible':
3331
+ case 'checkbox':
3332
+ default:
3333
+ return defaultRenderFacetItem(item, facet, index);
3334
+ }
3335
+ };
3336
+ // -------------------------------------------------------------------
3337
+ // Keyboard handler (shared across variants)
3338
+ // -------------------------------------------------------------------
3339
+ const handleListKeyDown = (e, visibleItems, facet) => {
3340
+ const currentEl = e.currentTarget.querySelector('[aria-selected="true"]');
3341
+ const allItems = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
3342
+ const currentIdx = currentEl ? allItems.indexOf(currentEl) : -1;
3343
+ if (e.key === 'ArrowDown') {
3344
+ e.preventDefault();
3345
+ const next = Math.min(currentIdx + 1, allItems.length - 1);
3346
+ allItems[next]?.focus();
3347
+ }
3348
+ else if (e.key === 'ArrowUp') {
3349
+ e.preventDefault();
3350
+ const prev = Math.max(currentIdx - 1, 0);
3351
+ allItems[prev]?.focus();
3352
+ }
3353
+ else if (e.key === 'Enter' || e.key === ' ') {
3354
+ e.preventDefault();
3355
+ if (currentIdx >= 0 && currentIdx < visibleItems.length) {
3356
+ handleFacetToggle(facet.field, visibleItems[currentIdx].value, visibleItems[currentIdx].selected || false);
3357
+ }
3358
+ }
3359
+ };
3360
+ // -------------------------------------------------------------------
3361
+ // Show more / less buttons (shared)
3362
+ // -------------------------------------------------------------------
3363
+ const renderShowMoreLess = (facet, filteredItems) => {
3364
+ const isExpanded = expandedFacets[facet.field] || false;
3365
+ const hasMore = filteredItems.length > maxItems;
3366
+ return (React.createElement(React.Fragment, null,
3367
+ showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
3368
+ marginTop: sizeScale.gap,
3369
+ padding: sizeScale.padding,
3370
+ border: 'none',
3371
+ backgroundColor: 'transparent',
3372
+ color: theme.colors.primary,
3373
+ cursor: 'pointer',
3374
+ fontSize: theme.typography.fontSize.small,
3375
+ textDecoration: 'underline',
2537
3376
  } },
2538
- "(",
2539
- item.count,
2540
- ")")));
3377
+ "Show more (",
3378
+ filteredItems.length - maxItems,
3379
+ " more)")),
3380
+ isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
3381
+ marginTop: sizeScale.gap,
3382
+ padding: sizeScale.padding,
3383
+ border: 'none',
3384
+ backgroundColor: 'transparent',
3385
+ color: theme.colors.primary,
3386
+ cursor: 'pointer',
3387
+ fontSize: theme.typography.fontSize.small,
3388
+ textDecoration: 'underline',
3389
+ } }, "Show less"))));
2541
3390
  };
2542
- const defaultRenderFacet = (facet, index) => {
3391
+ // -------------------------------------------------------------------
3392
+ // Default facet group renderer — Checkbox variant
3393
+ // -------------------------------------------------------------------
3394
+ const renderCheckboxFacet = (facet, _index) => {
3395
+ const filteredItems = filterItems(facet.items, facet.field);
2543
3396
  const isExpanded = expandedFacets[facet.field] || false;
2544
- const visibleItems = isExpanded ? facet.items : facet.items.slice(0, maxItems);
2545
- const hasMore = facet.items.length > maxItems;
3397
+ const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
2546
3398
  return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
2547
3399
  marginBottom: theme.spacing.large,
2548
3400
  padding: theme.spacing.medium,
2549
- backgroundColor: theme.colors.background,
2550
- border: `1px solid ${theme.colors.border}`,
3401
+ backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
3402
+ border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
2551
3403
  borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2552
3404
  } },
2553
3405
  React.createElement("h3", { className: facetsTheme.facetTitle, style: {
@@ -2557,36 +3409,131 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
2557
3409
  marginBottom: theme.spacing.medium,
2558
3410
  color: theme.colors.text,
2559
3411
  } }, facet.label || facet.field),
2560
- React.createElement("div", { className: facetsTheme.facetList }, visibleItems.map((item, itemIndex) => {
2561
- const actualIndex = isExpanded ? itemIndex : itemIndex;
2562
- return renderFacetItem
2563
- ? renderFacetItem(item, facet, actualIndex)
2564
- : defaultRenderFacetItem(item, facet, actualIndex);
2565
- })),
2566
- showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
2567
- marginTop: theme.spacing.small,
2568
- padding: theme.spacing.small,
3412
+ renderSearchInput(facet),
3413
+ React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet) }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
3414
+ renderShowMoreLess(facet, filteredItems)));
3415
+ };
3416
+ // -------------------------------------------------------------------
3417
+ // Color-swatch facet group renderer
3418
+ // -------------------------------------------------------------------
3419
+ const renderColorSwatchFacet = (facet, _index) => {
3420
+ const filteredItems = filterItems(facet.items, facet.field);
3421
+ const isExpanded = expandedFacets[facet.field] || false;
3422
+ const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
3423
+ return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
3424
+ marginBottom: theme.spacing.large,
3425
+ padding: theme.spacing.medium,
3426
+ backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
3427
+ border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
3428
+ borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
3429
+ } },
3430
+ React.createElement("h3", { className: facetsTheme.facetTitle, style: {
3431
+ fontSize: theme.typography.fontSize.large,
3432
+ fontWeight: 'bold',
3433
+ margin: 0,
3434
+ marginBottom: theme.spacing.medium,
3435
+ color: theme.colors.text,
3436
+ } }, facet.label || facet.field),
3437
+ renderSearchInput(facet),
3438
+ React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet), style: {
3439
+ display: 'flex',
3440
+ flexWrap: 'wrap',
3441
+ gap: sizeScale.gap,
3442
+ } }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
3443
+ renderShowMoreLess(facet, filteredItems)));
3444
+ };
3445
+ // -------------------------------------------------------------------
3446
+ // Collapsible facet group renderer
3447
+ // -------------------------------------------------------------------
3448
+ const renderCollapsibleFacet = (facet, _index) => {
3449
+ const isOpen = isFacetGroupOpen(facet.field);
3450
+ const filteredItems = filterItems(facet.items, facet.field);
3451
+ expandedFacets[facet.field] || false;
3452
+ // Note: For collapsible, the expandedFacets state controls the collapse/expand
3453
+ // of the group itself. We use a separate concept for Show more/less within items.
3454
+ // To avoid collision, Show more/less for collapsible uses the same expandedFacets
3455
+ // key prefixed with `_items_`.
3456
+ const isShowMoreExpanded = expandedFacets[`_items_${facet.field}`] || false;
3457
+ const visibleItems = isShowMoreExpanded ? filteredItems : filteredItems.slice(0, maxItems);
3458
+ const hasMore = filteredItems.length > maxItems;
3459
+ return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
3460
+ marginBottom: theme.spacing.large,
3461
+ backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
3462
+ border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
3463
+ borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
3464
+ overflow: 'hidden',
3465
+ } },
3466
+ React.createElement("button", { type: "button", className: clsx(facetsTheme.collapsibleHeader), onClick: () => toggleCollapsible(facet.field), "aria-expanded": isOpen, "aria-controls": `facet-group-${facet.field}`, style: {
3467
+ display: 'flex',
3468
+ alignItems: 'center',
3469
+ justifyContent: 'space-between',
3470
+ width: '100%',
3471
+ padding: theme.spacing.medium,
2569
3472
  border: 'none',
2570
3473
  backgroundColor: 'transparent',
2571
- color: theme.colors.primary,
2572
3474
  cursor: 'pointer',
2573
- fontSize: theme.typography.fontSize.small,
2574
- textDecoration: 'underline',
3475
+ textAlign: 'left',
2575
3476
  } },
2576
- "Show more (",
2577
- facet.items.length - maxItems,
2578
- " more)")),
2579
- isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
2580
- marginTop: theme.spacing.small,
2581
- padding: theme.spacing.small,
2582
- border: 'none',
2583
- backgroundColor: 'transparent',
2584
- color: theme.colors.primary,
2585
- cursor: 'pointer',
2586
- fontSize: theme.typography.fontSize.small,
2587
- textDecoration: 'underline',
2588
- } }, "Show less"))));
3477
+ React.createElement("span", { className: facetsTheme.facetTitle, style: {
3478
+ fontSize: theme.typography.fontSize.large,
3479
+ fontWeight: 'bold',
3480
+ color: theme.colors.text,
3481
+ flex: 1,
3482
+ } }, facet.label || facet.field),
3483
+ React.createElement("span", { className: clsx(facetsTheme.collapsibleIcon) },
3484
+ React.createElement(ChevronIcon$1, { expanded: isOpen, color: theme.colors.textSecondary || theme.colors.text }))),
3485
+ isOpen && (React.createElement("div", { id: `facet-group-${facet.field}`, style: {
3486
+ padding: `0 ${theme.spacing.medium} ${theme.spacing.medium}`,
3487
+ } },
3488
+ renderSearchInput(facet),
3489
+ React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet) }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
3490
+ showMore && hasMore && !isShowMoreExpanded && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
3491
+ ...prev,
3492
+ [`_items_${facet.field}`]: true,
3493
+ })), style: {
3494
+ marginTop: sizeScale.gap,
3495
+ padding: sizeScale.padding,
3496
+ border: 'none',
3497
+ backgroundColor: 'transparent',
3498
+ color: theme.colors.primary,
3499
+ cursor: 'pointer',
3500
+ fontSize: theme.typography.fontSize.small,
3501
+ textDecoration: 'underline',
3502
+ } },
3503
+ "Show more (",
3504
+ filteredItems.length - maxItems,
3505
+ " more)")),
3506
+ isShowMoreExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
3507
+ ...prev,
3508
+ [`_items_${facet.field}`]: false,
3509
+ })), style: {
3510
+ marginTop: sizeScale.gap,
3511
+ padding: sizeScale.padding,
3512
+ border: 'none',
3513
+ backgroundColor: 'transparent',
3514
+ color: theme.colors.primary,
3515
+ cursor: 'pointer',
3516
+ fontSize: theme.typography.fontSize.small,
3517
+ textDecoration: 'underline',
3518
+ } }, "Show less"))))));
3519
+ };
3520
+ // -------------------------------------------------------------------
3521
+ // Default facet renderer dispatcher
3522
+ // -------------------------------------------------------------------
3523
+ const defaultRenderFacet = (facet, index) => {
3524
+ switch (variant) {
3525
+ case 'color-swatch':
3526
+ return renderColorSwatchFacet(facet);
3527
+ case 'collapsible':
3528
+ return renderCollapsibleFacet(facet);
3529
+ case 'checkbox':
3530
+ default:
3531
+ return renderCheckboxFacet(facet);
3532
+ }
2589
3533
  };
3534
+ // -------------------------------------------------------------------
3535
+ // Empty state
3536
+ // -------------------------------------------------------------------
2590
3537
  if (facets.length === 0) {
2591
3538
  log.verbose('Facets: No facets to display', {
2592
3539
  hasResults: !!results,
@@ -2594,7 +3541,13 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
2594
3541
  });
2595
3542
  return null;
2596
3543
  }
2597
- return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: style }, facets.map((facet, index) => {
3544
+ // -------------------------------------------------------------------
3545
+ // Render
3546
+ // -------------------------------------------------------------------
3547
+ return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: {
3548
+ ...CSS_VAR_DEFAULTS,
3549
+ ...style,
3550
+ } }, facets.map((facet, index) => {
2598
3551
  return renderFacet
2599
3552
  ? renderFacet(facet, index)
2600
3553
  : defaultRenderFacet(facet);
@@ -2604,72 +3557,222 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
2604
3557
  /**
2605
3558
  * CurrentRefinements Component
2606
3559
  *
2607
- * Displays currently active filters/refinements with ability to clear them
3560
+ * Displays currently active filters/refinements with ability to clear them.
3561
+ * Supports StateManager auto-sync, display variants, layout modes, and animations.
2608
3562
  */
2609
- const CurrentRefinements = ({ refinements = [], onRefinementClear, onClearAll, renderRefinement, showClearAll = true, className, style, theme: customTheme, }) => {
3563
+ /** Get variant-specific styles */
3564
+ const getVariantStyles = (variant, themeColors, themeSpacing, themeBorderRadius, fieldColor) => {
3565
+ const baseBg = fieldColor || `var(--seekora-refinement-bg, ${themeColors.hover})`;
3566
+ const baseColor = `var(--seekora-refinement-color, ${themeColors.text})`;
3567
+ const baseBorder = `var(--seekora-refinement-border, ${themeColors.border})`;
3568
+ switch (variant) {
3569
+ case 'tags':
3570
+ return {
3571
+ display: 'inline-flex',
3572
+ alignItems: 'center',
3573
+ padding: `2px ${themeSpacing.medium}`,
3574
+ backgroundColor: baseBg,
3575
+ border: `1px solid ${baseBorder}`,
3576
+ borderRadius: `var(--seekora-refinement-radius, 4px)`,
3577
+ fontSize: '12px',
3578
+ color: baseColor,
3579
+ fontWeight: 500,
3580
+ };
3581
+ case 'pills':
3582
+ return {
3583
+ display: 'inline-flex',
3584
+ alignItems: 'center',
3585
+ padding: `${themeSpacing.small} ${themeSpacing.medium}`,
3586
+ backgroundColor: baseBg,
3587
+ border: 'none',
3588
+ borderRadius: `var(--seekora-refinement-radius, 9999px)`,
3589
+ fontSize: '13px',
3590
+ color: baseColor,
3591
+ };
3592
+ case 'badges':
3593
+ return {
3594
+ display: 'inline-flex',
3595
+ alignItems: 'center',
3596
+ padding: `3px ${themeSpacing.small}`,
3597
+ backgroundColor: themeColors.primary,
3598
+ border: 'none',
3599
+ borderRadius: `var(--seekora-refinement-radius, 4px)`,
3600
+ fontSize: '11px',
3601
+ color: '#fff',
3602
+ fontWeight: 600,
3603
+ textTransform: 'uppercase',
3604
+ letterSpacing: '0.5px',
3605
+ };
3606
+ case 'text-list':
3607
+ return {
3608
+ display: 'flex',
3609
+ alignItems: 'center',
3610
+ padding: `${themeSpacing.small} 0`,
3611
+ backgroundColor: 'transparent',
3612
+ border: 'none',
3613
+ borderBottom: `1px solid ${baseBorder}`,
3614
+ borderRadius: '0',
3615
+ fontSize: '14px',
3616
+ color: baseColor,
3617
+ };
3618
+ case 'chips':
3619
+ default:
3620
+ return {
3621
+ display: 'inline-flex',
3622
+ alignItems: 'center',
3623
+ padding: `${themeSpacing.small} ${themeSpacing.medium}`,
3624
+ backgroundColor: baseBg,
3625
+ border: `1px solid ${baseBorder}`,
3626
+ borderRadius: `var(--seekora-refinement-radius, ${typeof themeBorderRadius === 'string' ? themeBorderRadius : themeBorderRadius.medium})`,
3627
+ fontSize: '13px',
3628
+ color: baseColor,
3629
+ };
3630
+ }
3631
+ };
3632
+ /** Get variant-specific class name from theme */
3633
+ const getVariantClass = (variant, refinementsTheme) => {
3634
+ switch (variant) {
3635
+ case 'tags': return refinementsTheme.tag;
3636
+ case 'pills': return refinementsTheme.pill;
3637
+ case 'badges': return refinementsTheme.badge;
3638
+ case 'chips': return refinementsTheme.chip;
3639
+ default: return refinementsTheme.item;
3640
+ }
3641
+ };
3642
+ const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, onClearAll, renderRefinement, showClearAll = true, variant = 'chips', layout = 'horizontal', fieldColors, renderCloseIcon, className, style, theme: customTheme, }) => {
2610
3643
  const { theme } = useSearchContext();
3644
+ const { refinements: stateRefinements, removeRefinement, clearRefinements } = useSearchState();
2611
3645
  const refinementsTheme = customTheme || {};
3646
+ // Use props if provided, otherwise auto-read from StateManager
3647
+ const refinements = refinementsProp !== undefined
3648
+ ? refinementsProp
3649
+ : stateRefinements.map(r => ({ field: r.field, value: r.value }));
3650
+ // Track items for entry/exit animations
3651
+ const [visibleItems, setVisibleItems] = React.useState(new Set());
3652
+ const [exitingItems, setExitingItems] = React.useState(new Set());
3653
+ const prevRefinementsRef = React.useRef([]);
3654
+ React.useEffect(() => {
3655
+ const currentKeys = new Set(refinements.map(r => `${r.field}:${r.value}`));
3656
+ const prevKeys = new Set(prevRefinementsRef.current.map(r => `${r.field}:${r.value}`));
3657
+ // Detect removed items for exit animation
3658
+ const removed = new Set();
3659
+ prevKeys.forEach(key => {
3660
+ if (!currentKeys.has(key))
3661
+ removed.add(key);
3662
+ });
3663
+ if (removed.size > 0) {
3664
+ setExitingItems(removed);
3665
+ // Remove after animation completes
3666
+ setTimeout(() => setExitingItems(new Set()), 200);
3667
+ }
3668
+ // Mark new items for entry animation
3669
+ setVisibleItems(currentKeys);
3670
+ prevRefinementsRef.current = [...refinements];
3671
+ }, [refinements]);
2612
3672
  const handleClear = (field, value) => {
3673
+ // If synced with StateManager and no prop provided, auto-clear via StateManager
3674
+ if (refinementsProp === undefined) {
3675
+ removeRefinement(field, value);
3676
+ }
2613
3677
  if (onRefinementClear) {
2614
3678
  onRefinementClear(field, value);
2615
3679
  }
2616
3680
  };
2617
3681
  const handleClearAll = () => {
3682
+ // If synced with StateManager, auto-clear all via StateManager
3683
+ if (refinementsProp === undefined) {
3684
+ clearRefinements();
3685
+ }
2618
3686
  if (onClearAll) {
2619
3687
  onClearAll();
2620
3688
  }
2621
3689
  };
2622
- const defaultRenderRefinement = (refinement, index) => (React.createElement("div", { key: `${refinement.field}-${refinement.value}-${index}`, className: refinementsTheme.item, style: {
2623
- display: 'inline-flex',
2624
- alignItems: 'center',
2625
- padding: `${theme.spacing.small} ${theme.spacing.medium}`,
2626
- margin: `0 ${theme.spacing.small} ${theme.spacing.small} 0`,
2627
- backgroundColor: theme.colors.hover,
2628
- border: `1px solid ${theme.colors.border}`,
2629
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
2630
- fontSize: theme.typography.fontSize.small,
2631
- } },
2632
- React.createElement("span", { className: refinementsTheme.label, style: {
2633
- marginRight: theme.spacing.small,
2634
- color: theme.colors.text,
2635
- fontWeight: '500',
3690
+ const defaultCloseIcon = () => (React.createElement("span", { "aria-hidden": "true", style: { lineHeight: 1 } }, "\u00D7"));
3691
+ const defaultRenderRefinement = (refinement, index) => {
3692
+ const key = `${refinement.field}:${refinement.value}`;
3693
+ const fieldColor = fieldColors?.[refinement.field];
3694
+ const isEntering = visibleItems.has(key) && !prevRefinementsRef.current.some(r => `${r.field}:${r.value}` === key);
3695
+ const variantStyles = getVariantStyles(variant, theme.colors, theme.spacing, theme.borderRadius, fieldColor);
3696
+ const variantClass = getVariantClass(variant, refinementsTheme);
3697
+ return (React.createElement("div", { key: `${key}-${index}`, className: clsx(refinementsTheme.item, variantClass), role: "listitem", style: {
3698
+ ...variantStyles,
3699
+ margin: layout === 'vertical'
3700
+ ? `0 0 ${theme.spacing.small} 0`
3701
+ : `0 ${theme.spacing.small} ${theme.spacing.small} 0`,
3702
+ transition: 'all 200ms ease-in-out',
3703
+ opacity: exitingItems.has(key) ? 0 : 1,
3704
+ transform: exitingItems.has(key) ? 'scale(0.8)' : 'scale(1)',
3705
+ animation: isEntering ? 'seekoraChipIn 200ms ease-out' : undefined,
2636
3706
  } },
2637
- refinement.label || refinement.field,
2638
- ":"),
2639
- React.createElement("span", { className: refinementsTheme.value, style: {
2640
- marginRight: theme.spacing.small,
2641
- color: theme.colors.text,
2642
- } }, refinement.displayValue || refinement.value),
2643
- React.createElement("button", { type: "button", onClick: () => handleClear(refinement.field, refinement.value), className: refinementsTheme.clearButton, style: {
2644
- border: 'none',
2645
- backgroundColor: 'transparent',
2646
- color: theme.colors.text,
2647
- cursor: 'pointer',
2648
- fontSize: theme.typography.fontSize.medium,
2649
- padding: 0,
2650
- marginLeft: theme.spacing.small,
2651
- width: '20px',
2652
- height: '20px',
2653
- display: 'flex',
2654
- alignItems: 'center',
2655
- justifyContent: 'center',
2656
- borderRadius: '50%',
2657
- transition: theme.transitions?.fast || '150ms ease-in-out',
2658
- }, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}` }, "\u00D7")));
3707
+ variant !== 'badges' && (React.createElement("span", { className: refinementsTheme.label, style: {
3708
+ marginRight: theme.spacing.small,
3709
+ fontWeight: '500',
3710
+ opacity: 0.7,
3711
+ } },
3712
+ refinement.label || refinement.field,
3713
+ ":")),
3714
+ React.createElement("span", { className: refinementsTheme.value }, refinement.displayValue || refinement.value),
3715
+ React.createElement("button", { type: "button", onClick: () => handleClear(refinement.field, refinement.value), className: refinementsTheme.clearButton, style: {
3716
+ border: 'none',
3717
+ backgroundColor: 'transparent',
3718
+ color: 'inherit',
3719
+ cursor: 'pointer',
3720
+ fontSize: '14px',
3721
+ padding: '0 0 0 6px',
3722
+ display: 'flex',
3723
+ alignItems: 'center',
3724
+ justifyContent: 'center',
3725
+ borderRadius: '50%',
3726
+ transition: 'opacity 150ms ease-in-out',
3727
+ opacity: 0.6,
3728
+ }, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}`, onMouseEnter: e => (e.currentTarget.style.opacity = '1'), onMouseLeave: e => (e.currentTarget.style.opacity = '0.6') }, renderCloseIcon ? renderCloseIcon() : defaultCloseIcon())));
3729
+ };
2659
3730
  if (refinements.length === 0) {
2660
3731
  return null;
2661
3732
  }
2662
- return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: style },
2663
- React.createElement("div", { className: refinementsTheme.list, style: {
2664
- display: 'flex',
2665
- flexWrap: 'wrap',
2666
- alignItems: 'center',
2667
- marginBottom: showClearAll ? theme.spacing.medium : 0,
2668
- } }, refinements.map((refinement, index) => {
3733
+ // Group refinements by field for grouped layout
3734
+ const groupedRefinements = layout === 'grouped'
3735
+ ? refinements.reduce((acc, r) => {
3736
+ if (!acc[r.field])
3737
+ acc[r.field] = [];
3738
+ acc[r.field].push(r);
3739
+ return acc;
3740
+ }, {})
3741
+ : null;
3742
+ const containerStyles = {
3743
+ ...style,
3744
+ };
3745
+ const listStyles = {
3746
+ display: 'flex',
3747
+ flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
3748
+ flexDirection: layout === 'vertical' ? 'column' : 'row',
3749
+ alignItems: layout === 'vertical' ? 'flex-start' : 'center',
3750
+ marginBottom: showClearAll ? theme.spacing.medium : 0,
3751
+ };
3752
+ return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
3753
+ React.createElement("style", null, `
3754
+ @keyframes seekoraChipIn {
3755
+ from { opacity: 0; transform: scale(0.8); }
3756
+ to { opacity: 1; transform: scale(1); }
3757
+ }
3758
+ `),
3759
+ layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
3760
+ React.createElement("div", { className: refinementsTheme.groupLabel, style: {
3761
+ fontSize: theme.typography.fontSize.small,
3762
+ fontWeight: 600,
3763
+ color: theme.colors.text,
3764
+ marginBottom: theme.spacing.small,
3765
+ textTransform: 'capitalize',
3766
+ } }, items[0]?.label || field),
3767
+ React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
3768
+ return renderRefinement
3769
+ ? renderRefinement(refinement, index)
3770
+ : defaultRenderRefinement(refinement, index);
3771
+ })))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
2669
3772
  return renderRefinement
2670
3773
  ? renderRefinement(refinement, index)
2671
3774
  : defaultRenderRefinement(refinement, index);
2672
- })),
3775
+ }))),
2673
3776
  showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
2674
3777
  padding: `${theme.spacing.small} ${theme.spacing.medium}`,
2675
3778
  border: `1px solid ${theme.colors.border}`,
@@ -2679,6 +3782,7 @@ const CurrentRefinements = ({ refinements = [], onRefinementClear, onClearAll, r
2679
3782
  cursor: 'pointer',
2680
3783
  fontSize: theme.typography.fontSize.small,
2681
3784
  textDecoration: 'underline',
3785
+ transition: 'background-color 150ms ease-in-out',
2682
3786
  } }, "Clear all filters"))));
2683
3787
  };
2684
3788
 
@@ -3088,7 +4192,7 @@ const HitsPerPage = ({ items, onHitsPerPageChange, renderSelect, className, styl
3088
4192
  * Displays search results with infinite scroll or "Show More" button
3089
4193
  * Accumulates results as user loads more pages
3090
4194
  */
3091
- const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
4195
+ const InfiniteHits = ({ renderHit, renderEmpty, showInitialLoading = false, renderLoading, renderShowMore, showMoreButton = true, useInfiniteScroll = false, scrollThreshold = 0.1, fieldMapping, showMoreLabel = 'Show more', loadingLabel = 'Loading...', onHitClick, className, style, theme: customTheme, syncWithState = true, }) => {
3092
4196
  const { theme, stateManager } = useSearchContext();
3093
4197
  const { results, loading, currentPage, setPage } = useSearchState();
3094
4198
  const infiniteHitsTheme = customTheme || {};
@@ -3243,10 +4347,13 @@ const InfiniteHits = ({ renderHit, renderEmpty, renderLoading, renderShowMore, s
3243
4347
  cursor: isLastPage || isLoadingMore ? 'not-allowed' : 'pointer',
3244
4348
  transition: theme.transitions?.fast || '150ms ease-in-out',
3245
4349
  } }, isLoadingMore ? loadingLabel : isLastPage ? 'No more results' : showMoreLabel));
3246
- // Initial loading state
3247
- if (loading && accumulatedHits.length === 0) {
4350
+ // Initial loading state (only when showInitialLoading: default no loading screen)
4351
+ if (loading && accumulatedHits.length === 0 && showInitialLoading) {
3248
4352
  return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderLoading ? renderLoading() : defaultRenderLoading()));
3249
4353
  }
4354
+ if (loading && accumulatedHits.length === 0) {
4355
+ return React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style });
4356
+ }
3250
4357
  // Empty state
3251
4358
  if (!loading && accumulatedHits.length === 0) {
3252
4359
  return (React.createElement("div", { className: clsx(infiniteHitsTheme.root, className), style: style }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
@@ -3524,6 +4631,110 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
3524
4631
  const toggleShowMore = (level) => {
3525
4632
  setExpanded(prev => ({ ...prev, [level]: !prev[level] }));
3526
4633
  };
4634
+ const containerRef = React.useRef(null);
4635
+ // Collect all visible treeitem elements within the container
4636
+ const getVisibleTreeItems = React.useCallback(() => {
4637
+ if (!containerRef.current)
4638
+ return [];
4639
+ return Array.from(containerRef.current.querySelectorAll('[role="treeitem"]'));
4640
+ }, []);
4641
+ // Keyboard navigation handler for the tree
4642
+ const handleKeyDown = React.useCallback((e) => {
4643
+ const items = getVisibleTreeItems();
4644
+ if (items.length === 0)
4645
+ return;
4646
+ const activeElement = document.activeElement;
4647
+ // Find the treeitem that is or contains the active element
4648
+ const currentItem = items.find(item => item === activeElement || item.contains(activeElement));
4649
+ const currentIndex = currentItem ? items.indexOf(currentItem) : -1;
4650
+ switch (e.key) {
4651
+ case 'ArrowDown': {
4652
+ e.preventDefault();
4653
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
4654
+ const button = items[nextIndex].querySelector('button');
4655
+ if (button)
4656
+ button.focus();
4657
+ else
4658
+ items[nextIndex].focus();
4659
+ break;
4660
+ }
4661
+ case 'ArrowUp': {
4662
+ e.preventDefault();
4663
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
4664
+ const button = items[prevIndex].querySelector('button');
4665
+ if (button)
4666
+ button.focus();
4667
+ else
4668
+ items[prevIndex].focus();
4669
+ break;
4670
+ }
4671
+ case 'ArrowRight': {
4672
+ if (!currentItem)
4673
+ break;
4674
+ const isExpanded = currentItem.getAttribute('aria-expanded');
4675
+ if (isExpanded === 'false') {
4676
+ // Expand (select) this item
4677
+ e.preventDefault();
4678
+ const button = currentItem.querySelector('button');
4679
+ if (button)
4680
+ button.click();
4681
+ }
4682
+ else if (isExpanded === 'true') {
4683
+ // Move to first child
4684
+ e.preventDefault();
4685
+ const childList = currentItem.querySelector('[role="group"]');
4686
+ if (childList) {
4687
+ const firstChild = childList.querySelector('[role="treeitem"]');
4688
+ if (firstChild) {
4689
+ const button = firstChild.querySelector('button');
4690
+ if (button)
4691
+ button.focus();
4692
+ else
4693
+ firstChild.focus();
4694
+ }
4695
+ }
4696
+ }
4697
+ break;
4698
+ }
4699
+ case 'ArrowLeft': {
4700
+ if (!currentItem)
4701
+ break;
4702
+ const isExpanded = currentItem.getAttribute('aria-expanded');
4703
+ if (isExpanded === 'true') {
4704
+ // Collapse this item
4705
+ e.preventDefault();
4706
+ const button = currentItem.querySelector('button');
4707
+ if (button)
4708
+ button.click();
4709
+ }
4710
+ else {
4711
+ // Move focus to parent treeitem
4712
+ e.preventDefault();
4713
+ const parentGroup = currentItem.closest('[role="group"]');
4714
+ if (parentGroup) {
4715
+ const parentItem = parentGroup.closest('[role="treeitem"]');
4716
+ if (parentItem) {
4717
+ const button = parentItem.querySelector(':scope > button');
4718
+ if (button)
4719
+ button.focus();
4720
+ else
4721
+ parentItem.focus();
4722
+ }
4723
+ }
4724
+ }
4725
+ break;
4726
+ }
4727
+ case 'Enter': {
4728
+ if (!currentItem)
4729
+ break;
4730
+ e.preventDefault();
4731
+ const button = currentItem.querySelector('button');
4732
+ if (button)
4733
+ button.click();
4734
+ break;
4735
+ }
4736
+ }
4737
+ }, [getVisibleTreeItems]);
3527
4738
  // Render a level of the hierarchy
3528
4739
  const renderLevel = (items, level) => {
3529
4740
  if (!items || items.length === 0)
@@ -3532,12 +4743,14 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
3532
4743
  const displayLimit = isExpanded ? showMoreLimit : limit;
3533
4744
  const displayItems = items.slice(0, displayLimit);
3534
4745
  const hasMore = items.length > displayLimit;
3535
- return (React.createElement("ul", { className: hierarchicalTheme.list, style: {
4746
+ return (React.createElement("ul", { role: level === 0 ? 'tree' : 'group', className: hierarchicalTheme.list, style: {
3536
4747
  listStyle: 'none',
3537
4748
  margin: 0,
3538
4749
  padding: level > 0 ? `0 0 0 ${theme.spacing.medium}` : 0,
3539
4750
  } },
3540
- displayItems.map((item, index) => (React.createElement("li", { key: item.value, className: clsx(hierarchicalTheme.item, item.isRefined && hierarchicalTheme.itemSelected, item.data && item.data.length > 0 && hierarchicalTheme.itemParent), style: {
4751
+ displayItems.map((item, index) => (React.createElement("li", { key: item.value, role: "treeitem", ...(item.data && item.data.length > 0
4752
+ ? { 'aria-expanded': !!item.isRefined }
4753
+ : {}), className: clsx(hierarchicalTheme.item, item.isRefined && hierarchicalTheme.itemSelected, item.data && item.data.length > 0 && hierarchicalTheme.itemParent), style: {
3541
4754
  padding: `${theme.spacing.small} 0`,
3542
4755
  } }, renderItem ? (renderItem(item, level)) : (React.createElement(React.Fragment, null,
3543
4756
  React.createElement("button", { type: "button", onClick: () => handleItemClick(item, level), className: hierarchicalTheme.link, style: {
@@ -3575,7 +4788,7 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
3575
4788
  if (processedItems.length === 0) {
3576
4789
  return null;
3577
4790
  }
3578
- return (React.createElement("div", { className: clsx(hierarchicalTheme.root, className), style: style }, renderLevel(processedItems, 0)));
4791
+ return (React.createElement("div", { ref: containerRef, className: clsx(hierarchicalTheme.root, className), style: style, tabIndex: 0, onKeyDown: handleKeyDown }, renderLevel(processedItems, 0)));
3579
4792
  };
3580
4793
 
3581
4794
  /**
@@ -3675,6 +4888,53 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
3675
4888
  const handleDragEnd = () => {
3676
4889
  setIsDragging(false);
3677
4890
  };
4891
+ // Handle keyboard navigation for enhanced control (Shift+Arrow for 10x step, Home/End)
4892
+ const handleMinKeyDown = (e) => {
4893
+ let newValue = null;
4894
+ if (e.key === 'Home') {
4895
+ e.preventDefault();
4896
+ newValue = min;
4897
+ }
4898
+ else if (e.key === 'End') {
4899
+ e.preventDefault();
4900
+ newValue = internalMax - step;
4901
+ }
4902
+ else if (e.shiftKey && (e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
4903
+ e.preventDefault();
4904
+ newValue = Math.max(min, internalMin - step * 10);
4905
+ }
4906
+ else if (e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowUp')) {
4907
+ e.preventDefault();
4908
+ newValue = Math.min(internalMax - step, internalMin + step * 10);
4909
+ }
4910
+ if (newValue !== null) {
4911
+ setInternalMin(newValue);
4912
+ debouncedUpdate(newValue, internalMax);
4913
+ }
4914
+ };
4915
+ const handleMaxKeyDown = (e) => {
4916
+ let newValue = null;
4917
+ if (e.key === 'Home') {
4918
+ e.preventDefault();
4919
+ newValue = internalMin + step;
4920
+ }
4921
+ else if (e.key === 'End') {
4922
+ e.preventDefault();
4923
+ newValue = max;
4924
+ }
4925
+ else if (e.shiftKey && (e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
4926
+ e.preventDefault();
4927
+ newValue = Math.max(internalMin + step, internalMax - step * 10);
4928
+ }
4929
+ else if (e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowUp')) {
4930
+ e.preventDefault();
4931
+ newValue = Math.min(max, internalMax + step * 10);
4932
+ }
4933
+ if (newValue !== null) {
4934
+ setInternalMax(newValue);
4935
+ debouncedUpdate(internalMin, newValue);
4936
+ }
4937
+ };
3678
4938
  // Calculate filled track position
3679
4939
  const minPercent = ((internalMin - min) / (max - min)) * 100;
3680
4940
  const maxPercent = ((internalMax - min) / (max - min)) * 100;
@@ -3710,7 +4970,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
3710
4970
  backgroundColor: theme.colors.primary,
3711
4971
  borderRadius: '2px',
3712
4972
  } }),
3713
- React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMin, onChange: handleMinChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
4973
+ React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMin, onChange: handleMinChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, onKeyDown: handleMinKeyDown, tabIndex: 0, "aria-valuenow": internalMin, "aria-valuemin": min, "aria-valuemax": max, className: rangeSliderTheme.thumb, style: {
3714
4974
  position: 'absolute',
3715
4975
  width: '100%',
3716
4976
  height: '4px',
@@ -3720,7 +4980,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
3720
4980
  cursor: 'pointer',
3721
4981
  pointerEvents: 'none',
3722
4982
  }, "aria-label": `Minimum ${label || field}` }),
3723
- React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMax, onChange: handleMaxChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
4983
+ React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMax, onChange: handleMaxChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, onKeyDown: handleMaxKeyDown, tabIndex: 0, "aria-valuenow": internalMax, "aria-valuemin": min, "aria-valuemax": max, className: rangeSliderTheme.thumb, style: {
3724
4984
  position: 'absolute',
3725
4985
  width: '100%',
3726
4986
  height: '4px',
@@ -4128,36 +5388,40 @@ const MobileFiltersButton = ({ onClick, text = 'Filters', showCount = true, clas
4128
5388
  * - FrequentlyBoughtTogether: Bundle recommendations
4129
5389
  * - RecentlyViewed: User's recently viewed items
4130
5390
  */
4131
- const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
5391
+ const RelatedProducts = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Related Products', maxItems = 6, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4132
5392
  const { theme } = useSearchContext();
4133
5393
  const recommendationTheme = customTheme || {};
4134
5394
  // If items are provided, use them directly
4135
5395
  const items = itemsProp?.slice(0, maxItems) || [];
4136
5396
  const loading = loadingProp;
4137
- if (loading) {
5397
+ if (loading && items.length === 0 && showLoadingState) {
4138
5398
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4139
5399
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading related products...")));
4140
5400
  }
5401
+ if (loading && items.length === 0)
5402
+ return null;
4141
5403
  if (items.length === 0) {
4142
5404
  return null;
4143
5405
  }
4144
5406
  return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
4145
5407
  };
4146
- const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
5408
+ const TrendingItems = ({ items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Trending Now', maxItems = 8, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', }) => {
4147
5409
  const { theme } = useSearchContext();
4148
5410
  const recommendationTheme = customTheme || {};
4149
5411
  const items = itemsProp?.slice(0, maxItems) || [];
4150
5412
  const loading = loadingProp;
4151
- if (loading) {
5413
+ if (loading && items.length === 0 && showLoadingState) {
4152
5414
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4153
5415
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading trending items...")));
4154
5416
  }
5417
+ if (loading && items.length === 0)
5418
+ return null;
4155
5419
  if (items.length === 0) {
4156
5420
  return null;
4157
5421
  }
4158
5422
  return (React.createElement(RecommendationSection, { title: title, items: items, renderItem: renderItem, onItemClick: onItemClick, className: className, style: style, theme: customTheme, layout: layout, currencySymbol: currencySymbol }));
4159
5423
  };
4160
- const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
5424
+ const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadingProp = false, showLoadingState = false, title = 'Frequently Bought Together', maxItems = 4, renderItem, onItemClick, className, style, theme: customTheme, layout = 'horizontal', currencySymbol = '$', showAddAllButton = true, onAddAll, }) => {
4161
5425
  const { theme } = useSearchContext();
4162
5426
  const recommendationTheme = customTheme || {};
4163
5427
  const items = itemsProp?.slice(0, maxItems) || [];
@@ -4170,10 +5434,12 @@ const FrequentlyBoughtTogether = ({ productId, items: itemsProp, loading: loadin
4170
5434
  return sum + price;
4171
5435
  }, 0);
4172
5436
  }, [items]);
4173
- if (loading) {
5437
+ if (loading && items.length === 0 && showLoadingState) {
4174
5438
  return (React.createElement("div", { className: clsx(recommendationTheme.root, className), style: style },
4175
5439
  React.createElement("div", { className: recommendationTheme.loading, style: getLoadingStyle(theme) }, "Loading recommendations...")));
4176
5440
  }
5441
+ if (loading && items.length === 0)
5442
+ return null;
4177
5443
  if (items.length === 0) {
4178
5444
  return null;
4179
5445
  }
@@ -4392,7 +5658,7 @@ function getLoadingStyle(theme) {
4392
5658
  *
4393
5659
  * Standalone component for displaying query suggestions
4394
5660
  */
4395
- const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
5661
+ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, minQueryLength = 2, onSuggestionClick, renderSuggestion, showLoadingState = false, renderLoading, renderEmpty, showTitle = false, title = 'Suggestions', className, style, theme: customTheme, }) => {
4396
5662
  const { client, theme } = useSearchContext();
4397
5663
  const [selectedIndex, setSelectedIndex] = React.useState(-1);
4398
5664
  const { suggestions, loading, error } = useQuerySuggestions({
@@ -4429,7 +5695,8 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
4429
5695
  if (query.length < minQueryLength) {
4430
5696
  return null;
4431
5697
  }
4432
- if (loading) {
5698
+ // When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
5699
+ if (loading && displayedSuggestions.length === 0 && showLoadingState) {
4433
5700
  return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
4434
5701
  showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
4435
5702
  fontSize: theme.typography.fontSize.large,
@@ -4439,7 +5706,7 @@ const QuerySuggestions = ({ query = '', maxSuggestions = 10, debounceMs = 300, m
4439
5706
  } }, title)),
4440
5707
  renderLoading ? renderLoading() : defaultRenderLoading()));
4441
5708
  }
4442
- if (error || displayedSuggestions.length === 0) {
5709
+ if (error || (!loading && displayedSuggestions.length === 0)) {
4443
5710
  return (React.createElement("div", { className: clsx(suggestionsTheme.container, className), style: style },
4444
5711
  showTitle && (React.createElement("div", { className: suggestionsTheme.title, style: {
4445
5712
  fontSize: theme.typography.fontSize.large,
@@ -4567,7 +5834,8 @@ function transformFilteredTab(raw) {
4567
5834
  // Main Hook
4568
5835
  // ============================================================================
4569
5836
  function useQuerySuggestionsEnhanced(options) {
4570
- const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets = false, maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
5837
+ const { client, query, enabled = true, debounceMs = 200, maxSuggestions = 10, minQueryLength = 1, includeDropdownRecommendations = false, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, includeFacets = true, // CHANGED: Enable facets by default
5838
+ maxCategories = 3, maxFacets = 5, filteredTabs, minPopularity, timeRange, disableTypoTolerance, analyticsTags, enableRecentSearches = true, maxRecentSearches = MAX_RECENT_SEARCHES_DEFAULT, recentSearchesKey = RECENT_SEARCHES_DEFAULT_KEY, onSuggestionsLoaded, onError, } = options;
4571
5839
  // State
4572
5840
  const [suggestions, setSuggestions] = React.useState([]);
4573
5841
  const [loading, setLoading] = React.useState(false);
@@ -4690,12 +5958,29 @@ function useQuerySuggestionsEnhanced(options) {
4690
5958
  const error = err instanceof Error ? err : new Error(String(err));
4691
5959
  if (error.name === 'AbortError')
4692
5960
  return;
4693
- log.error('Failed to fetch query suggestions', { query: searchQuery, error: error.message });
4694
- setError(error);
4695
- setSuggestions([]);
4696
- setDropdownRecommendations(null);
4697
- if (onError) {
4698
- onError(error);
5961
+ // Check if it's a 404 error (suggestions not enabled for this store).
5962
+ // The search SDK wraps axios errors into plain Error objects with the status
5963
+ // embedded in the message, e.g. "[getSuggestions] ... (404)".
5964
+ const errMsg = error.message || '';
5965
+ const is404 = err?.response?.status === 404 ||
5966
+ err?.status === 404 ||
5967
+ /\(404\)/.test(errMsg);
5968
+ if (is404) {
5969
+ // Silently handle 404 - suggestions feature not enabled
5970
+ log.verbose('Query suggestions not enabled for this store (404)');
5971
+ setSuggestions([]);
5972
+ setDropdownRecommendations(null);
5973
+ setError(null);
5974
+ }
5975
+ else {
5976
+ // For other errors, log and set error state
5977
+ log.error('Failed to fetch query suggestions', { query: searchQuery, error: error.message });
5978
+ setError(error);
5979
+ setSuggestions([]);
5980
+ setDropdownRecommendations(null);
5981
+ if (onError) {
5982
+ onError(error);
5983
+ }
4699
5984
  }
4700
5985
  }
4701
5986
  finally {
@@ -5023,7 +6308,7 @@ const LoadingSpinner = ({ style }) => (React.createElement("svg", { style: { ani
5023
6308
  // Component
5024
6309
  // ============================================================================
5025
6310
  const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropdown(props, ref) {
5026
- const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading = true, showEmptyState = true, highlight = { enabled: true, preTag: '<mark>', postTag: '</mark>' }, keyboardNav = { enabled: true }, animation = { enabled: true, duration: 150, entrance: 'fade' }, classNames = {}, style, renderSuggestion, renderRecentSearch, renderLoading, renderEmpty, footer, position = 'absolute', width = '100%', zIndex = 1000, closeOnClickOutside = true, closeOnEscape = true, ariaLabel = 'Search suggestions', onSuggestionSelect, onRecentSearchClick, onRecentSearchRemove, onOpen, onClose, onNavigate, } = props;
6311
+ const { query, isOpen = true, maxSuggestions = 8, minQueryLength = 1, debounceMs = 200, showRecentSearches = true, maxRecentSearches = 5, showCounts = true, showLoading = false, showEmptyState = true, highlight = { enabled: true, preTag: '<mark>', postTag: '</mark>' }, keyboardNav = { enabled: true }, animation = { enabled: true, duration: 150, entrance: 'fade' }, classNames = {}, style, renderSuggestion, renderRecentSearch, renderLoading, renderEmpty, footer, position = 'absolute', width = '100%', zIndex = 1000, closeOnClickOutside = true, closeOnEscape = true, ariaLabel = 'Search suggestions', onSuggestionSelect, onRecentSearchClick, onRecentSearchRemove, onOpen, onClose, onNavigate, } = props;
5027
6312
  const { client, theme } = useSearchContext();
5028
6313
  const containerRef = React.useRef(null);
5029
6314
  const [activeIndex, setActiveIndex] = React.useState(-1);
@@ -5208,7 +6493,7 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
5208
6493
  loading && showLoading && (React.createElement("div", { className: classNames.loadingState, style: defaultStyles$1.loadingState }, renderLoading ? renderLoading() : (React.createElement(React.Fragment, null,
5209
6494
  React.createElement(LoadingSpinner, null),
5210
6495
  React.createElement("span", null, "Searching..."))))),
5211
- !loading && showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
6496
+ showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
5212
6497
  React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Recent Searches"),
5213
6498
  recentSearches.slice(0, maxRecentSearches).map((search, index) => {
5214
6499
  const isActive = activeIndex === index;
@@ -5216,8 +6501,8 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
5216
6501
  onRecentSearchClick?.(search);
5217
6502
  }, onMouseEnter: () => setActiveIndex(index) }, renderRecentSearchItem(search, index, isActive)));
5218
6503
  }))),
5219
- !loading && showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
5220
- !loading && showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
6504
+ showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
6505
+ showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
5221
6506
  query.length > 0 && (React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Suggestions")),
5222
6507
  suggestions.map((suggestion, index) => {
5223
6508
  const itemIndex = showRecent ? recentSearches.length + index : index;
@@ -5479,7 +6764,7 @@ const CloseIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill
5479
6764
  // Component
5480
6765
  // ============================================================================
5481
6766
  const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(props, ref) {
5482
- const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, } = props;
6767
+ const { query, isOpen = true, sections = DEFAULT_SECTIONS, maxSuggestionsPerSection = 8, minQueryLength = 0, debounceMs = 200, includeDropdownRecommendations = true, includeDropdownProductList = true, includeFilteredTabs = true, includeCategories = true, maxCategories = 3, showCounts = true, showCategoryCounts = true, showSectionHeaders = true, classNames = {}, style, renderSuggestion, renderCategory, renderTrendingItem, renderRecentItem, header, footer, width = '100%', maxHeight = '480px', zIndex = 1000, ariaLabel = 'Search suggestions', analyticsTags, onSuggestionSelect, onCategoryClick, onRecentSearchClick, onRecentSearchRemove, onViewAllClick, onOpen, onClose, showLoadingOverlay = false, renderLoading, } = props;
5483
6768
  const { client } = useSearchContext();
5484
6769
  const containerRef = React.useRef(null);
5485
6770
  const [activeIndex, setActiveIndex] = React.useState(-1);
@@ -5698,8 +6983,7 @@ const RichQuerySuggestions = React.forwardRef(function RichQuerySuggestions(prop
5698
6983
  } },
5699
6984
  header && React.createElement("div", { style: styles$2.header }, header),
5700
6985
  React.createElement("div", { style: { ...styles$2.content, maxHeight } },
5701
- loading && (React.createElement("div", { style: styles$2.loadingOverlay },
5702
- React.createElement("span", null, "Loading..."))),
6986
+ loading && showLoadingOverlay && (React.createElement("div", { style: styles$2.loadingOverlay }, renderLoading ? renderLoading() : React.createElement("span", null, "Loading..."))),
5703
6987
  enabledSections.map((section, index) => {
5704
6988
  let content = null;
5705
6989
  switch (section.id) {
@@ -6263,7 +7547,14 @@ const EVENTS = {
6263
7547
  TRENDING_CLICK: 'suggestions.trending_click',
6264
7548
  SEARCH_SUBMIT: 'suggestions.search_submit',
6265
7549
  DROPDOWN_OPEN: 'suggestions.dropdown_open',
6266
- DROPDOWN_CLOSE: 'suggestions.dropdown_close'};
7550
+ DROPDOWN_CLOSE: 'suggestions.dropdown_close',
7551
+ // Variant & product interaction events
7552
+ VARIANT_SELECT: 'product.variant_select',
7553
+ VARIANT_HOVER: 'product.variant_hover',
7554
+ ADD_TO_CART: 'product.add_to_cart',
7555
+ PRODUCT_IMPRESSION: 'product.impression',
7556
+ SWATCH_CLICK: 'product.swatch_click',
7557
+ };
6267
7558
  // ============================================================================
6268
7559
  // Hook Implementation
6269
7560
  // ============================================================================
@@ -6287,11 +7578,16 @@ function useSuggestionsAnalytics(options) {
6287
7578
  return;
6288
7579
  const searchContext = context ?? contextOption;
6289
7580
  try {
7581
+ // Extract query for search-related events (backend requires query at top level for search events)
7582
+ const { query, ...restMetadata } = metadata;
7583
+ const isSearchEvent = eventName.includes('search') || eventName === EVENTS.SEARCH_SUBMIT;
6290
7584
  await client.trackEvent?.({
6291
7585
  event_name: eventName,
6292
7586
  analytics_tags: analyticsTags,
7587
+ // Include query at top level for search events
7588
+ ...(isSearchEvent && query ? { query } : {}),
6293
7589
  metadata: {
6294
- ...metadata,
7590
+ ...restMetadata,
6295
7591
  timestamp: Date.now(),
6296
7592
  source: 'suggestions_dropdown',
6297
7593
  },
@@ -7114,7 +8410,7 @@ function DropdownPanel({ children, position = 'absolute', top = '100%', left = 0
7114
8410
 
7115
8411
  /**
7116
8412
  * Parses suggestion text containing <mark>...</mark> and returns React nodes
7117
- * with the marked segments rendered as <mark> elements. Safe: inner content
8413
+ * with the marked segments rendered as styled elements. Safe: inner content
7118
8414
  * is rendered as text, not HTML.
7119
8415
  */
7120
8416
  const defaultMarkStyle = {
@@ -7123,9 +8419,34 @@ const defaultMarkStyle = {
7123
8419
  borderRadius: '2px',
7124
8420
  padding: '0 2px',
7125
8421
  };
8422
+ /** Compute styles based on highlight options */
8423
+ function computeHighlightStyles(options) {
8424
+ const style = options.highlightStyle || 'background';
8425
+ const base = {};
8426
+ switch (style) {
8427
+ case 'background':
8428
+ base.backgroundColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))';
8429
+ base.borderRadius = '2px';
8430
+ base.padding = '0 2px';
8431
+ break;
8432
+ case 'underline':
8433
+ base.textDecoration = 'underline';
8434
+ base.textDecorationColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.8))';
8435
+ base.textUnderlineOffset = '2px';
8436
+ break;
8437
+ }
8438
+ if (options.highlightTextColor) {
8439
+ base.color = options.highlightTextColor;
8440
+ }
8441
+ else {
8442
+ base.color = 'var(--seekora-highlight-color, inherit)';
8443
+ }
8444
+ base.fontWeight = options.highlightFontWeight || 'var(--seekora-highlight-weight, 500)';
8445
+ return base;
8446
+ }
7126
8447
  /**
7127
8448
  * Converts a string like "lined <mark>blue</mark>" into React nodes with
7128
- * the marked part rendered as a <mark> element. When no <mark> tags are
8449
+ * the marked part rendered as a styled element. When no <mark> tags are
7129
8450
  * present, returns the string as-is.
7130
8451
  */
7131
8452
  function parseHighlightMarkup(text, options = {}) {
@@ -7134,11 +8455,18 @@ function parseHighlightMarkup(text, options = {}) {
7134
8455
  const parts = text.split(/(<mark>[\s\S]*?<\/mark>)/g);
7135
8456
  if (parts.length <= 1)
7136
8457
  return text;
7137
- const { markClassName, markStyle } = options;
8458
+ const { markClassName, markStyle, highlightTag } = options;
8459
+ const Tag = (highlightTag || 'mark');
8460
+ // Compute styles: if no custom options provided, use legacy defaults
8461
+ const hasCustomOptions = options.highlightColor || options.highlightTextColor
8462
+ || options.highlightFontWeight || options.highlightStyle;
8463
+ const computedStyle = hasCustomOptions
8464
+ ? computeHighlightStyles(options)
8465
+ : defaultMarkStyle;
7138
8466
  return (React.createElement(React.Fragment, null, parts.map((part, i) => {
7139
8467
  const m = part.match(/^<mark>([\s\S]*)<\/mark>$/);
7140
8468
  if (m) {
7141
- return (React.createElement("mark", { key: i, className: markClassName, style: { ...defaultMarkStyle, ...markStyle } }, m[1]));
8469
+ return (React.createElement(Tag, { key: i, className: markClassName, style: { ...computedStyle, ...markStyle } }, m[1]));
7142
8470
  }
7143
8471
  return part;
7144
8472
  })));
@@ -7184,21 +8512,6 @@ function SuggestionItem({ suggestion, index, isActive, onSelect, className, styl
7184
8512
  suggestion.count != null ? (React.createElement("span", { className: "seekora-suggestions-item-count", style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, suggestion.count)) : null));
7185
8513
  }
7186
8514
 
7187
- /**
7188
- * SuggestionsLoading – loading indicator (primitive)
7189
- */
7190
- function SuggestionsLoading({ className, style, text = 'Loading...' }) {
7191
- const { loading } = useSuggestionsContext();
7192
- if (!loading)
7193
- return null;
7194
- return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
7195
- padding: 16,
7196
- color: 'var(--seekora-text-secondary, #6b7280)',
7197
- fontSize: '0.875rem',
7198
- ...style,
7199
- } }, text));
7200
- }
7201
-
7202
8515
  /**
7203
8516
  * SuggestionList – container for text suggestions (primitive)
7204
8517
  *
@@ -7210,15 +8523,19 @@ const listStyle = {
7210
8523
  margin: 0,
7211
8524
  padding: '4px 0',
7212
8525
  };
7213
- function SuggestionList({ maxItems = 10, className, style, listClassName, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
8526
+ function SuggestionList({ maxItems = 10, className, style, listClassName, showLoadingState = false, renderLoading, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
7214
8527
  const { suggestions, activeIndex, loading, selectSuggestion, getAllNavigableItems, } = useSuggestionsContext();
7215
8528
  const items = suggestions.slice(0, maxItems);
7216
8529
  const navigableItems = getAllNavigableItems();
7217
8530
  const suggestionStartIndex = navigableItems.findIndex((n) => n.type === 'suggestion');
7218
8531
  const activeIsInSuggestions = suggestionStartIndex >= 0 && activeIndex >= suggestionStartIndex && activeIndex < suggestionStartIndex + items.length;
7219
- if (loading) {
7220
- return React.createElement(SuggestionsLoading, { className: className, style: style });
8532
+ // When loading with no previous results, show loading only if showLoadingState (default: don't show loading screen)
8533
+ if (loading && items.length === 0 && showLoadingState) {
8534
+ if (renderLoading)
8535
+ return React.createElement(React.Fragment, null, renderLoading());
8536
+ return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: { padding: 16, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875rem', ...style } }, "Loading..."));
7221
8537
  }
8538
+ // When loading with previous results, show previous results (no loading UI)
7222
8539
  if (items.length === 0)
7223
8540
  return null;
7224
8541
  return (React.createElement("div", { className: clsx('seekora-suggestions-list', className), style: style },
@@ -8024,6 +9341,41 @@ const highlightText = (text, query, options = {}) => {
8024
9341
  const classAttr = className ? ` class="${className}"` : '';
8025
9342
  return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
8026
9343
  };
9344
+ /**
9345
+ * Safe React-based highlight rendering (no dangerouslySetInnerHTML).
9346
+ * Returns React nodes with matched text wrapped in the specified tag element.
9347
+ */
9348
+ const highlightTextReact = (text, query, options = {}) => {
9349
+ if (!query || !text)
9350
+ return text;
9351
+ const { tag = 'mark', className, style } = options;
9352
+ const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9353
+ const regex = new RegExp(`(${escapedQuery})`, 'gi');
9354
+ const parts = text.split(regex);
9355
+ if (parts.length <= 1)
9356
+ return text;
9357
+ const defaultHighlightStyle = {
9358
+ backgroundColor: 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))',
9359
+ fontWeight: 500,
9360
+ borderRadius: '2px',
9361
+ padding: '0 2px',
9362
+ ...style,
9363
+ };
9364
+ return parts.map((part, i) => {
9365
+ if (regex.test(part)) {
9366
+ // Reset lastIndex after test
9367
+ regex.lastIndex = 0;
9368
+ return React.createElement(tag, {
9369
+ key: i,
9370
+ className,
9371
+ style: defaultHighlightStyle,
9372
+ }, part);
9373
+ }
9374
+ // Reset lastIndex after test
9375
+ regex.lastIndex = 0;
9376
+ return part;
9377
+ });
9378
+ };
8027
9379
  // ============================================================================
8028
9380
  // Variant Utilities
8029
9381
  // ============================================================================
@@ -9130,6 +10482,21 @@ function TrendingList({ title = 'Trending', maxItems = 8, className, style, list
9130
10482
  }))));
9131
10483
  }
9132
10484
 
10485
+ /**
10486
+ * SuggestionsLoading – loading indicator (primitive)
10487
+ */
10488
+ function SuggestionsLoading({ className, style, text = 'Loading...' }) {
10489
+ const { loading } = useSuggestionsContext();
10490
+ if (!loading)
10491
+ return null;
10492
+ return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
10493
+ padding: 16,
10494
+ color: 'var(--seekora-text-secondary, #6b7280)',
10495
+ fontSize: '0.875rem',
10496
+ ...style,
10497
+ } }, text));
10498
+ }
10499
+
9133
10500
  /**
9134
10501
  * SuggestionsError – error message (primitive)
9135
10502
  */
@@ -9161,7 +10528,6 @@ function SuggestionsDropdownComposition({ showRecentSearches = true, showTrendin
9161
10528
  React.createElement(SearchInput, { placeholder: placeholder }),
9162
10529
  React.createElement(DropdownPanel, null,
9163
10530
  React.createElement(SuggestionsError, null),
9164
- React.createElement(SuggestionsLoading, null),
9165
10531
  showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
9166
10532
  React.createElement(SuggestionList, null),
9167
10533
  showTabs ? React.createElement(CategoriesTabs, null) : null,
@@ -9632,12 +10998,13 @@ function SectionError({ className, style, render }) {
9632
10998
  /**
9633
10999
  * SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
9634
11000
  */
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, }) {
11001
+ function SectionItemGrid({ columns = 4, maxItems = 12, className, style, showLoadingState = false, 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
11002
  const { items, loading, error, trackClick } = useSectionSearchContext();
9637
- if (loading)
11003
+ if (loading && items.length === 0 && showLoadingState)
9638
11004
  return React.createElement(SectionLoading, { className: className, style: style });
9639
11005
  if (error)
9640
11006
  return React.createElement(SectionError, { className: className, style: style });
11007
+ // When loading with previous items, show them (no loading screen)
9641
11008
  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
11009
  }
9643
11010
 
@@ -10374,7 +11741,7 @@ const RatingStars = ({ rating, count }) => {
10374
11741
  ")"))));
10375
11742
  };
10376
11743
  const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
10377
- const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
11744
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], recentSearches = [], trendingSearches = [], filteredTabs = [], activeTab = 'all', suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showDepartments = true, currentDepartment, departments = [], showPrime = false, width = '700px', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onRecentClick, onRecentRemove, onRecentClearAll, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
10378
11745
  // Inject global responsive styles
10379
11746
  useInjectResponsiveStyles();
10380
11747
  // Responsive state
@@ -10471,7 +11838,7 @@ const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
10471
11838
  }
10472
11839
  `),
10473
11840
  header,
10474
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11841
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10475
11842
  React.createElement("div", { style: styles.spinner }),
10476
11843
  React.createElement("span", null, "Searching...")))) : (React.createElement(React.Fragment, null,
10477
11844
  filteredTabs.length > 0 && (React.createElement("div", { style: {
@@ -10553,9 +11920,7 @@ const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
10553
11920
  React.createElement("div", { style: styles.suggestionIcon },
10554
11921
  React.createElement(SearchIcon$5, null)),
10555
11922
  React.createElement("div", { style: styles.suggestionContent },
10556
- React.createElement("div", { style: styles.suggestionQuery, dangerouslySetInnerHTML: {
10557
- __html: highlightText(suggestion.query, query, { className: 'highlight' })
10558
- } }),
11923
+ React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { className: 'highlight' })),
10559
11924
  showDepartments && firstCategory && (React.createElement("div", { style: styles.suggestionContext },
10560
11925
  React.createElement("span", { style: styles.suggestionDepartment },
10561
11926
  "in ",
@@ -10805,7 +12170,7 @@ const TrendingIcon$2 = () => (React.createElement("svg", { width: "20", height:
10805
12170
  const ArrowIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", width: "20", height: "20" },
10806
12171
  React.createElement("path", { d: "M9 5l-4 4 4 4", stroke: "currentColor", strokeWidth: "2", fill: "none", transform: "rotate(-90 12 12)" })));
10807
12172
  const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
10808
- const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
12173
+ const { query, isOpen = true, loading = false, suggestions = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, theme = {}, showFeelingLucky = true, feelingLuckyText = "I'm Feeling Lucky", showRemoveRecent = true, showVoiceSearch = false, showTrendingIndicator = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onRecentClick, onRecentRemove, onVoiceSearch, onFeelingLucky, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
10809
12174
  // Inject global responsive styles
10810
12175
  useInjectResponsiveStyles();
10811
12176
  // Responsive state
@@ -10900,11 +12265,7 @@ const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
10900
12265
  item.type === 'trending' ? React.createElement(TrendingIcon$2, null) :
10901
12266
  React.createElement(SearchIcon$4, null)),
10902
12267
  React.createElement("div", { style: styles.itemContent },
10903
- React.createElement("span", { style: styles.itemText, dangerouslySetInnerHTML: {
10904
- __html: query
10905
- ? highlightText(item.query, query, { tag: 'b' })
10906
- : item.query
10907
- } }),
12268
+ React.createElement("span", { style: styles.itemText }, query ? highlightTextReact(item.query, query, { tag: 'b' }) : item.query),
10908
12269
  React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
10909
12270
  item.type === 'trending' && showTrendingIndicator && (React.createElement("span", { style: styles.trending },
10910
12271
  React.createElement(TrendingIcon$2, null),
@@ -10924,7 +12285,7 @@ const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
10924
12285
  }
10925
12286
  `),
10926
12287
  header,
10927
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12288
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
10928
12289
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
10929
12290
  React.createElement("ul", { style: styles.list },
10930
12291
  showRecent && (React.createElement(React.Fragment, null,
@@ -11259,7 +12620,7 @@ const ImageIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill
11259
12620
  React.createElement("polyline", { points: "21 15 16 10 5 21" })));
11260
12621
  const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref) {
11261
12622
  const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showSaveButton = true, activeCategory: initialActiveCategory, activeTab, // From BaseDropdownProps
11262
- showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
12623
+ showPriceOverlay = true, gridColumns = 4, width = '800px', maxHeight = '600px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onSaveProduct, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
11263
12624
  // Inject global responsive styles
11264
12625
  useInjectResponsiveStyles();
11265
12626
  // Responsive state
@@ -11353,17 +12714,13 @@ const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref
11353
12714
  "(",
11354
12715
  cat.count,
11355
12716
  ")"))))))))),
11356
- React.createElement("div", { style: { ...styles.content, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12717
+ React.createElement("div", { style: { ...styles.content, maxHeight } }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11357
12718
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
11358
12719
  processedSuggestions.length > 0 && (React.createElement("div", { style: styles.suggestionsRow }, processedSuggestions.slice(0, 8).map((s, idx) => (React.createElement("button", { key: s.id || idx, "data-index": idx, style: mergeStyles(styles.suggestionPill, activeIndex === idx ? styles.suggestionPillActive : undefined, hoveredSuggestion === idx && activeIndex !== idx ? styles.suggestionPillHover : undefined), onClick: () => onSuggestionSelect?.(s._raw, idx), onMouseEnter: () => {
11359
12720
  setHoveredSuggestion(idx);
11360
12721
  setActiveIndex(idx);
11361
12722
  }, onMouseLeave: () => setHoveredSuggestion(-1) },
11362
- React.createElement("span", { style: styles.suggestionIcon, dangerouslySetInnerHTML: {
11363
- __html: query
11364
- ? highlightText(s.query, query, { tag: 'strong' })
11365
- : s.query
11366
- } })))))),
12723
+ React.createElement("span", { style: styles.suggestionIcon }, query ? highlightTextReact(s.query, query, { tag: 'strong' }) : s.query)))))),
11367
12724
  displayProducts.length > 0 && (React.createElement(React.Fragment, null,
11368
12725
  React.createElement("div", { style: styles.sectionTitle },
11369
12726
  React.createElement(TrendingIcon$1, null),
@@ -11697,7 +13054,7 @@ const ShoppingIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", f
11697
13054
  const ClockIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", width: "16", height: "16" },
11698
13055
  React.createElement("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z", clipRule: "evenodd" })));
11699
13056
  const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref) {
11700
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
13057
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showPreview = true, asOverlay = true, placeholder = 'Search...', showShortcuts = true, actions = [], width = '680px', maxHeight = '500px', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
11701
13058
  // Inject global responsive styles
11702
13059
  useInjectResponsiveStyles();
11703
13060
  // Responsive state
@@ -11851,7 +13208,7 @@ const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref
11851
13208
  React.createElement("span", { style: styles.kbd }, "K")))),
11852
13209
  header,
11853
13210
  React.createElement("div", { style: styles.content },
11854
- React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
13211
+ React.createElement("div", { ref: listRef, style: { ...styles.resultsColumn, maxHeight } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
11855
13212
  React.createElement("div", { style: styles.spinner })))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty },
11856
13213
  "No results for \"",
11857
13214
  query,
@@ -11893,9 +13250,7 @@ const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref
11893
13250
  }, onMouseEnter: () => setActiveIndex(idx) },
11894
13251
  React.createElement("div", { style: mergeStyles(styles.itemIcon, isActive ? styles.itemIconActive : undefined) }, item.icon),
11895
13252
  React.createElement("div", { style: styles.itemContent },
11896
- React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined), dangerouslySetInnerHTML: {
11897
- __html: highlightText(item.title, query, { tag: 'strong' })
11898
- } }))));
13253
+ React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined) }, highlightTextReact(item.title, query, { tag: 'strong' })))));
11899
13254
  }))),
11900
13255
  processedProducts.length > 0 && (React.createElement("div", { style: styles.section },
11901
13256
  React.createElement("div", { style: styles.sectionTitle }, "Products"),
@@ -12250,7 +13605,7 @@ const SearchIcon$1 = () => (React.createElement("svg", { width: "18", height: "1
12250
13605
  const ArrowIcon = () => (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "currentColor" },
12251
13606
  React.createElement("path", { fillRule: "evenodd", d: "M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z", clipRule: "evenodd" })));
12252
13607
  const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
12253
- const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, renderLoading, renderEmpty, } = props;
13608
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], categories = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, categoryFields = { id: 'id', label: 'label' }, productDisplay = {}, theme = {}, showHeroProduct = true, showCollections = true, showAddToCart = true, addToCartText = 'Quick Add', showComparePrice = true, width = '100%', maxHeight = '500px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onCategoryClick, onTabChange, onAddToCart, onViewAll, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
12254
13609
  // Inject global responsive styles
12255
13610
  useInjectResponsiveStyles();
12256
13611
  // Responsive state
@@ -12322,7 +13677,7 @@ const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
12322
13677
  }
12323
13678
  `),
12324
13679
  header,
12325
- loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
13680
+ (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12326
13681
  React.createElement("div", { style: styles.spinner })))) : (React.createElement("div", { style: styles.layout },
12327
13682
  React.createElement("div", { style: styles.leftPanel },
12328
13683
  React.createElement("div", { style: styles.section },
@@ -12335,9 +13690,7 @@ const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
12335
13690
  React.createElement("div", { style: styles.suggestionIcon },
12336
13691
  React.createElement(SearchIcon$1, null)),
12337
13692
  React.createElement("div", { style: styles.suggestionContent },
12338
- React.createElement("div", { style: styles.suggestionQuery, dangerouslySetInnerHTML: {
12339
- __html: highlightText(suggestion.query, query, { tag: 'mark' })
12340
- } }),
13693
+ React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { tag: 'mark' })),
12341
13694
  suggestion.count && (React.createElement("div", { style: styles.suggestionMeta },
12342
13695
  suggestion.count,
12343
13696
  " results"))),
@@ -12657,7 +14010,7 @@ const ChevronIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fi
12657
14010
  const ArrowUpIcon = () => (React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "20", height: "20" },
12658
14011
  React.createElement("path", { d: "M7 17l5-5-5-5M13 17l5-5-5-5" })));
12659
14012
  const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props, ref) {
12660
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, renderLoading, renderEmpty, } = props;
14013
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], trendingSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showSearchInput = true, showCancel = true, cancelText = 'Cancel', showDragHandle = true, swipeToDismiss = true, footerButtonText = 'Search', showFooterButton = true, width = '100%', maxHeight = '85vh', zIndex = 9999, className, style, classNames = {}, inputRef, onSuggestionSelect, onProductClick, onRecentClick, onRecentClearAll, onSearchSubmit, onClose, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
12661
14014
  // Inject global responsive styles
12662
14015
  useInjectResponsiveStyles();
12663
14016
  const styles = React.useMemo(() => createStyles$2(), []);
@@ -12799,7 +14152,7 @@ const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props,
12799
14152
  }
12800
14153
  }, style: styles.searchInput, autoFocus: true })),
12801
14154
  showCancel && (React.createElement("button", { style: styles.cancelBtn, onClick: onClose }, cancelText)))))),
12802
- React.createElement("div", { style: styles.content }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
14155
+ React.createElement("div", { style: styles.content }, (loading && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading },
12803
14156
  React.createElement("div", { style: styles.spinner })))) : (React.createElement(React.Fragment, null,
12804
14157
  showRecent && (React.createElement("div", { style: styles.section },
12805
14158
  React.createElement("div", { style: styles.sectionHeader },
@@ -12840,9 +14193,7 @@ const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props,
12840
14193
  React.createElement("div", { style: styles.itemIcon },
12841
14194
  React.createElement(SearchIcon, null)),
12842
14195
  React.createElement("div", { style: styles.itemContent },
12843
- React.createElement("div", { style: styles.itemTitle, dangerouslySetInnerHTML: {
12844
- __html: highlightText(s.query, inputValue, { tag: 'strong' })
12845
- } }),
14196
+ React.createElement("div", { style: styles.itemTitle }, highlightTextReact(s.query, inputValue, { tag: 'strong' })),
12846
14197
  s.count && (React.createElement("div", { style: styles.itemSubtitle },
12847
14198
  s.count,
12848
14199
  " results"))),
@@ -13035,7 +14386,7 @@ const createStyles$1 = () => ({
13035
14386
  },
13036
14387
  });
13037
14388
  const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
13038
- const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, renderLoading, renderEmpty, } = props;
14389
+ const { query, isOpen = true, loading = false, suggestions = [], products = [], recentSearches = [], suggestionFields = { query: 'query' }, productFields = { id: 'id', title: 'title' }, productDisplay = {}, theme = {}, showIndices = false, showTypeLabels = false, showSectionDividers = true, width = '100%', maxHeight = '400px', zIndex = 1000, className, style, classNames = {}, onSuggestionSelect, onProductClick, onRecentClick, onClose, onSearchSubmit, header, footer, showLoadingState = false, renderLoading, renderEmpty, } = props;
13039
14390
  // Inject global responsive styles
13040
14391
  useInjectResponsiveStyles();
13041
14392
  // Responsive state
@@ -13121,7 +14472,7 @@ const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
13121
14472
  }
13122
14473
  `),
13123
14474
  header,
13124
- React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, loading ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
14475
+ React.createElement("div", { ref: listRef, style: { maxHeight, overflowY: 'auto' } }, (loading && allItems.length === 0 && showLoadingState) ? (renderLoading ? renderLoading() : (React.createElement("div", { style: styles.loading }, "Loading..."))) : allItems.length === 0 ? (renderEmpty ? renderEmpty(query) : (React.createElement("div", { style: styles.empty }, query ? `No results for "${query}"` : 'Start typing to search'))) : (React.createElement(React.Fragment, null,
13125
14476
  showRecent && (React.createElement(React.Fragment, null,
13126
14477
  showSectionDividers && (React.createElement("div", { style: styles.divider }, "Recent")),
13127
14478
  recentQueries.map((q, idx) => {
@@ -13143,9 +14494,7 @@ const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
13143
14494
  return (React.createElement("div", { key: s.id || `suggestion-${idx}`, "data-index": itemIdx, style: mergeStyles(styles.item, isActive ? styles.itemActive : undefined, isLast ? styles.itemLast : undefined), onClick: () => onSuggestionSelect?.(s._raw, idx), onMouseEnter: () => setActiveIndex(itemIdx) },
13144
14495
  showIndices && (React.createElement("span", { style: styles.itemIndex }, itemIdx + 1)),
13145
14496
  React.createElement("div", { style: styles.itemContent },
13146
- React.createElement("div", { style: styles.itemQuery, dangerouslySetInnerHTML: {
13147
- __html: highlightText(s.query, query, { tag: 'mark' })
13148
- } }),
14497
+ React.createElement("div", { style: styles.itemQuery }, highlightTextReact(s.query, query, { tag: 'mark' })),
13149
14498
  s.count && (React.createElement("div", { style: styles.itemMeta },
13150
14499
  s.count.toLocaleString(),
13151
14500
  " results"))),
@@ -13456,6 +14805,7 @@ const SuggestionSearchBar = React.forwardRef(function SuggestionSearchBar(props,
13456
14805
  include_dropdown_product_list: includeDropdownProductList,
13457
14806
  include_filtered_tabs: includeFilteredTabs,
13458
14807
  include_categories: includeCategories,
14808
+ include_facets: true, // Enable facet-based suggestions
13459
14809
  filtered_tabs: filteredTabs,
13460
14810
  analytics_tags: analyticsTags,
13461
14811
  returnFullResponse: true, // Required to get tabs and products
@@ -14137,7 +15487,7 @@ function getHitKey(hit, index) {
14137
15487
  return hit.objectID;
14138
15488
  return `suggestion-${hit.url}-${index}`;
14139
15489
  }
14140
- function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
15490
+ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, showLoadingState = false, error, translations = {}, sources: _sources = [], }) {
14141
15491
  const listRef = React.useRef(null);
14142
15492
  React.useEffect(() => {
14143
15493
  if (!listRef.current || hits.length === 0)
@@ -14171,7 +15521,7 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
14171
15521
  return (React.createElement("div", { className: "seekora-docsearch-empty" },
14172
15522
  React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
14173
15523
  }
14174
- if (isLoading && hits.length === 0) {
15524
+ if (isLoading && hits.length === 0 && showLoadingState) {
14175
15525
  return (React.createElement("div", { className: "seekora-docsearch-loading" },
14176
15526
  React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
14177
15527
  React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
@@ -14179,6 +15529,10 @@ function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSe
14179
15529
  React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
14180
15530
  React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
14181
15531
  }
15532
+ if (isLoading && hits.length === 0) {
15533
+ return React.createElement("div", { className: "seekora-docsearch-empty" });
15534
+ }
15535
+ // When loading with previous hits, fall through and show them (no loading screen)
14182
15536
  if (error) {
14183
15537
  return (React.createElement("div", { className: "seekora-docsearch-error" },
14184
15538
  React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));