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