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