@seekora-ai/ui-sdk-react 0.2.13 → 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/Pagination.d.ts +47 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +166 -28
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +49 -2
- package/dist/components/RichQuerySuggestions.d.ts +7 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/SearchBar.d.ts +16 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +130 -16
- 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 +10 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +9 -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/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +2 -4
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +2 -6
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -4
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +2 -4
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +2 -6
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +2 -4
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -4
- 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/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/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 +23 -6
- 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 +217 -16
- package/dist/src/index.esm.js +1586 -249
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1585 -248
- 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);
|
|
@@ -1654,28 +1746,104 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
|
|
|
1654
1746
|
const processingTime = res?.processingTimeMS
|
|
1655
1747
|
|| res?.data?.processingTimeMS
|
|
1656
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]);
|
|
1657
1769
|
return (React.createElement("div", { ref: containerRef, className: clsx(searchBarTheme.container, className), style: {
|
|
1658
1770
|
position: 'relative',
|
|
1659
1771
|
display: 'flex',
|
|
1660
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,
|
|
1661
1779
|
...style,
|
|
1662
1780
|
} },
|
|
1663
|
-
React.createElement("
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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,
|
|
1667
1837
|
fontFamily: theme.typography.fontFamily,
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}),
|
|
1678
|
-
} }),
|
|
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"))),
|
|
1679
1847
|
processingTime !== undefined && (React.createElement("span", { style: {
|
|
1680
1848
|
marginLeft: theme.spacing.small,
|
|
1681
1849
|
fontSize: theme.typography.fontSize.small,
|
|
@@ -1742,7 +1910,7 @@ const formatPrice$1 = (value, currency = '₹') => {
|
|
|
1742
1910
|
}
|
|
1743
1911
|
return String(value);
|
|
1744
1912
|
};
|
|
1745
|
-
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, }) => {
|
|
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, }) => {
|
|
1746
1914
|
const { theme, client, enableAnalytics } = useSearchContext();
|
|
1747
1915
|
const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
|
|
1748
1916
|
const searchResultsTheme = customTheme || {};
|
|
@@ -2086,6 +2254,10 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2086
2254
|
});
|
|
2087
2255
|
// Determine container style based on view mode
|
|
2088
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,
|
|
2089
2261
|
...style,
|
|
2090
2262
|
};
|
|
2091
2263
|
// Determine results list style based on view mode
|
|
@@ -2112,19 +2284,19 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2112
2284
|
// When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
|
|
2113
2285
|
if (loading && resultItems.length === 0 && showLoadingState) {
|
|
2114
2286
|
log.verbose('SearchResults: Rendering loading state');
|
|
2115
|
-
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()));
|
|
2116
2288
|
}
|
|
2117
|
-
// When loading with previous results, fall through and render them (
|
|
2289
|
+
// When loading with previous results, fall through and render them (with opacity transition)
|
|
2118
2290
|
if (error) {
|
|
2119
2291
|
log.error('SearchResults: Rendering error state', {
|
|
2120
2292
|
error: error.message,
|
|
2121
2293
|
stack: error.stack,
|
|
2122
2294
|
});
|
|
2123
|
-
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)));
|
|
2124
2296
|
}
|
|
2125
2297
|
if (!results || resultItems.length === 0) {
|
|
2126
2298
|
log.verbose('SearchResults: No results to display');
|
|
2127
|
-
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()));
|
|
2128
2300
|
}
|
|
2129
2301
|
const renderFn = renderResult || defaultRenderResult;
|
|
2130
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 },
|
|
@@ -2169,10 +2341,44 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2169
2341
|
* Stats Component
|
|
2170
2342
|
*
|
|
2171
2343
|
* Displays search statistics (total results, processing time, etc.)
|
|
2344
|
+
* Supports inline, badge, and detailed display variants.
|
|
2172
2345
|
*/
|
|
2173
|
-
|
|
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, }) => {
|
|
2174
2373
|
const { theme } = useSearchContext();
|
|
2374
|
+
const { results: stateResults } = useSearchState();
|
|
2175
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
|
+
}, []);
|
|
2176
2382
|
// Extract stats from results
|
|
2177
2383
|
const res = results;
|
|
2178
2384
|
const totalResults = res?.totalResults
|
|
@@ -2183,44 +2389,173 @@ const Stats = ({ results, renderStats, className, style, theme: customTheme, sho
|
|
|
2183
2389
|
|| res?.data?.processingTimeMS
|
|
2184
2390
|
|| res?.data?.data?.processingTimeMS;
|
|
2185
2391
|
const query = (res?.query ?? '');
|
|
2186
|
-
|
|
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') {
|
|
2187
2423
|
const parts = [];
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
+
}
|
|
2193
2433
|
}
|
|
2194
2434
|
if (showProcessingTime && processingTime !== undefined) {
|
|
2195
|
-
parts.push(
|
|
2435
|
+
parts.push(React.createElement("span", { key: "time", className: statsTheme.text },
|
|
2436
|
+
fmt(processingTime),
|
|
2437
|
+
"ms"));
|
|
2196
2438
|
}
|
|
2197
2439
|
if (showQuery && query) {
|
|
2198
|
-
parts.push(
|
|
2440
|
+
parts.push(React.createElement("span", { key: "query", className: statsTheme.text },
|
|
2441
|
+
"for \"",
|
|
2442
|
+
query,
|
|
2443
|
+
"\""));
|
|
2199
2444
|
}
|
|
2200
|
-
return (React.createElement("div", { className: clsx(statsTheme.container, className), style: {
|
|
2201
|
-
fontSize: theme.typography.fontSize.medium,
|
|
2202
|
-
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)',
|
|
2203
2449
|
...style,
|
|
2204
2450
|
} }, parts.map((part, index) => (React.createElement("span", { key: index },
|
|
2205
|
-
index > 0 && React.createElement("span", { className: statsTheme.separator, style: { margin: `0 ${theme.spacing.small}` } }, separator),
|
|
2206
|
-
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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));
|
|
2494
|
+
}
|
|
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))))));
|
|
2214
2533
|
}
|
|
2215
|
-
|
|
2534
|
+
// Fallback (should not be reached)
|
|
2535
|
+
return null;
|
|
2216
2536
|
};
|
|
2217
2537
|
|
|
2218
2538
|
/**
|
|
2219
2539
|
* Pagination Component
|
|
2220
2540
|
*
|
|
2221
|
-
* 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
|
|
2222
2551
|
*/
|
|
2223
|
-
|
|
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', }) => {
|
|
2224
2559
|
const { theme } = useSearchContext();
|
|
2225
2560
|
const { results: stateResults, currentPage: stateCurrentPage, setPage } = useSearchState();
|
|
2226
2561
|
const paginationTheme = customTheme || {};
|
|
@@ -2243,6 +2578,19 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2243
2578
|
|| res?.data?.total_pages
|
|
2244
2579
|
|| res?.data?.data?.total_pages
|
|
2245
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) + ')';
|
|
2246
2594
|
const handlePageChange = (page) => {
|
|
2247
2595
|
if (page < 1 || page > totalPages || page === currentPage)
|
|
2248
2596
|
return;
|
|
@@ -2253,22 +2601,26 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2253
2601
|
onPageChange(page);
|
|
2254
2602
|
}
|
|
2255
2603
|
};
|
|
2256
|
-
const defaultRenderPageButton = (page, isActive, isDisabled) => (React.createElement("button", { type: "button", disabled: isDisabled, onClick: () => handlePageChange(page), className: clsx(paginationTheme.item, isActive && paginationTheme.itemActive, isDisabled), style: {
|
|
2257
|
-
padding: theme.spacing.
|
|
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],
|
|
2258
2606
|
margin: `0 ${theme.spacing.small}`,
|
|
2259
|
-
border: `1px solid ${
|
|
2260
|
-
borderRadius:
|
|
2261
|
-
backgroundColor: isActive ?
|
|
2262
|
-
color: isActive ?
|
|
2607
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2608
|
+
borderRadius: cssVarRadius,
|
|
2609
|
+
backgroundColor: isActive ? cssVarActiveBg : cssVarBg,
|
|
2610
|
+
color: isActive ? cssVarActiveColor : cssVarColor,
|
|
2263
2611
|
cursor: 'pointer',
|
|
2264
2612
|
opacity: 1,
|
|
2265
|
-
fontSize: theme.typography.fontSize.
|
|
2266
|
-
minWidth:
|
|
2613
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2614
|
+
minWidth: sizeTokens.minWidth,
|
|
2267
2615
|
...(isActive && {
|
|
2268
2616
|
fontWeight: 'bold',
|
|
2269
2617
|
}),
|
|
2270
2618
|
} }, page));
|
|
2271
|
-
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) {
|
|
2272
2624
|
return null;
|
|
2273
2625
|
}
|
|
2274
2626
|
// Calculate page range to display
|
|
@@ -2301,6 +2653,94 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2301
2653
|
}
|
|
2302
2654
|
return pages;
|
|
2303
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) ─────────────────
|
|
2304
2744
|
const pageNumbers = getPageNumbers();
|
|
2305
2745
|
return (React.createElement("nav", { className: clsx(paginationTheme.container, className), style: style, "aria-label": "Pagination" },
|
|
2306
2746
|
React.createElement("ul", { className: paginationTheme.list, style: {
|
|
@@ -2311,27 +2751,44 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2311
2751
|
padding: 0,
|
|
2312
2752
|
margin: 0,
|
|
2313
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
|
+
}
|
|
2314
2771
|
} },
|
|
2315
2772
|
showPrevNext && (React.createElement("li", null,
|
|
2316
2773
|
React.createElement("button", { type: "button", disabled: currentPage === 1, onClick: () => handlePageChange(currentPage - 1), className: clsx(paginationTheme.item, currentPage === 1 && paginationTheme.itemDisabled), style: {
|
|
2317
|
-
padding: theme.spacing.
|
|
2774
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2318
2775
|
margin: `0 ${theme.spacing.small}`,
|
|
2319
|
-
border: `1px solid ${
|
|
2320
|
-
borderRadius:
|
|
2321
|
-
backgroundColor:
|
|
2322
|
-
color:
|
|
2776
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2777
|
+
borderRadius: cssVarRadius,
|
|
2778
|
+
backgroundColor: cssVarBg,
|
|
2779
|
+
color: cssVarColor,
|
|
2323
2780
|
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
|
|
2324
2781
|
opacity: currentPage === 1 ? 0.5 : 1,
|
|
2325
|
-
fontSize: theme.typography.fontSize.
|
|
2326
|
-
}, "aria-label": "Previous page" },
|
|
2782
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2783
|
+
}, "aria-label": "Previous page" }, previousLabel))),
|
|
2327
2784
|
pageNumbers.map((page, index) => {
|
|
2328
2785
|
if (page === 'ellipsis') {
|
|
2329
2786
|
return (React.createElement("li", { key: `ellipsis-${index}` },
|
|
2330
2787
|
React.createElement("span", { className: paginationTheme.ellipsis, style: {
|
|
2331
|
-
padding: theme.spacing.
|
|
2788
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2332
2789
|
margin: `0 ${theme.spacing.small}`,
|
|
2333
|
-
color:
|
|
2334
|
-
fontSize: theme.typography.fontSize.
|
|
2790
|
+
color: cssVarColor,
|
|
2791
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2335
2792
|
} }, "...")));
|
|
2336
2793
|
}
|
|
2337
2794
|
const isActive = page === currentPage;
|
|
@@ -2342,89 +2799,274 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2342
2799
|
}),
|
|
2343
2800
|
showPrevNext && (React.createElement("li", null,
|
|
2344
2801
|
React.createElement("button", { type: "button", disabled: currentPage === totalPages, onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.item, currentPage === totalPages && paginationTheme.itemDisabled), style: {
|
|
2345
|
-
padding: theme.spacing.
|
|
2802
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2346
2803
|
margin: `0 ${theme.spacing.small}`,
|
|
2347
|
-
border: `1px solid ${
|
|
2348
|
-
borderRadius:
|
|
2349
|
-
backgroundColor:
|
|
2350
|
-
color:
|
|
2804
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2805
|
+
borderRadius: cssVarRadius,
|
|
2806
|
+
backgroundColor: cssVarBg,
|
|
2807
|
+
color: cssVarColor,
|
|
2351
2808
|
cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
|
|
2352
2809
|
opacity: currentPage === totalPages ? 0.5 : 1,
|
|
2353
|
-
fontSize: theme.typography.fontSize.
|
|
2354
|
-
}, "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)))));
|
|
2355
2813
|
};
|
|
2356
2814
|
|
|
2357
2815
|
/**
|
|
2358
2816
|
* SortBy Component
|
|
2359
2817
|
*
|
|
2360
|
-
* Displays sort options for search results
|
|
2361
|
-
*
|
|
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
|
|
2362
2831
|
*/
|
|
2363
|
-
|
|
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', }) => {
|
|
2364
2854
|
const { theme } = useSearchContext();
|
|
2365
2855
|
const { sortBy: stateManagerSortBy, setSortBy } = useSearchState();
|
|
2366
2856
|
const sortByTheme = customTheme || {};
|
|
2367
|
-
|
|
2857
|
+
const instanceId = React.useId();
|
|
2858
|
+
// Determine whether the label should render.
|
|
2859
|
+
const shouldShowLabel = showLabel !== undefined ? showLabel : !!label;
|
|
2860
|
+
// ------ State ----------------------------------------------------------
|
|
2368
2861
|
const [internalValue, setInternalValue] = React.useState(defaultValue || options[0]?.value || '');
|
|
2369
2862
|
// Sync with StateManager on mount if defaultValue is set
|
|
2370
2863
|
React.useEffect(() => {
|
|
2371
2864
|
if (syncWithState && defaultValue && !stateManagerSortBy) {
|
|
2372
2865
|
setSortBy(defaultValue, false); // Don't trigger search on initial sync
|
|
2373
2866
|
}
|
|
2374
|
-
}, []);
|
|
2867
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
2375
2868
|
// Determine the current value: controlled prop > StateManager > internal
|
|
2376
2869
|
const value = valueProp !== undefined
|
|
2377
2870
|
? valueProp
|
|
2378
|
-
:
|
|
2871
|
+
: syncWithState && stateManagerSortBy
|
|
2379
2872
|
? stateManagerSortBy
|
|
2380
2873
|
: internalValue;
|
|
2874
|
+
// ------ Handlers -------------------------------------------------------
|
|
2381
2875
|
const handleChange = (e) => {
|
|
2382
|
-
|
|
2876
|
+
applyValue(e.target.value);
|
|
2877
|
+
};
|
|
2878
|
+
const applyValue = React.useCallback((newValue) => {
|
|
2383
2879
|
setInternalValue(newValue);
|
|
2384
|
-
// Update StateManager (automatically triggers search)
|
|
2385
2880
|
if (syncWithState) {
|
|
2386
2881
|
setSortBy(newValue);
|
|
2387
2882
|
}
|
|
2388
|
-
// Call callback for backwards compatibility
|
|
2389
2883
|
if (onSortChange) {
|
|
2390
2884
|
onSortChange(newValue);
|
|
2391
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,
|
|
2392
2896
|
};
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
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))))));
|
|
2930
|
+
}
|
|
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
|
+
}))));
|
|
2411
2993
|
}
|
|
2412
|
-
|
|
2994
|
+
// Fallback — should never reach here, but satisfies TS exhaustiveness
|
|
2995
|
+
return null;
|
|
2413
2996
|
};
|
|
2414
2997
|
|
|
2415
2998
|
/**
|
|
2416
2999
|
* Facets Component
|
|
2417
3000
|
*
|
|
2418
|
-
* 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.
|
|
2419
3003
|
*/
|
|
2420
|
-
|
|
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', }) => {
|
|
2421
3058
|
const { theme } = useSearchContext();
|
|
2422
3059
|
const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
|
|
2423
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.
|
|
2424
3063
|
const [expandedFacets, setExpandedFacets] = React.useState({});
|
|
3064
|
+
const [searchTerms, setSearchTerms] = React.useState({});
|
|
2425
3065
|
// Use results from prop if provided, otherwise from state manager
|
|
2426
3066
|
const results = resultsProp || stateResults;
|
|
3067
|
+
// -------------------------------------------------------------------
|
|
2427
3068
|
// Extract facets from results
|
|
3069
|
+
// -------------------------------------------------------------------
|
|
2428
3070
|
const extractFacets = () => {
|
|
2429
3071
|
if (facetsProp)
|
|
2430
3072
|
return facetsProp;
|
|
@@ -2471,6 +3113,9 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2471
3113
|
return extracted;
|
|
2472
3114
|
};
|
|
2473
3115
|
const facets = extractFacets();
|
|
3116
|
+
// -------------------------------------------------------------------
|
|
3117
|
+
// Handlers
|
|
3118
|
+
// -------------------------------------------------------------------
|
|
2474
3119
|
const handleFacetToggle = (field, value, selected) => {
|
|
2475
3120
|
const newSelected = !selected;
|
|
2476
3121
|
log.verbose('Facets: Facet toggled', { field, value, selected: newSelected });
|
|
@@ -2503,49 +3148,283 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2503
3148
|
[field]: !prev[field],
|
|
2504
3149
|
}));
|
|
2505
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
|
+
// -------------------------------------------------------------------
|
|
2506
3236
|
const defaultRenderFacetItem = (item, facet, index) => {
|
|
2507
3237
|
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
2508
3238
|
if (!isExpanded && index >= maxItems) {
|
|
2509
3239
|
return null;
|
|
2510
3240
|
}
|
|
2511
|
-
|
|
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: {
|
|
2512
3243
|
display: 'flex',
|
|
2513
3244
|
alignItems: 'center',
|
|
2514
|
-
padding:
|
|
3245
|
+
padding: sizeScale.padding,
|
|
3246
|
+
cursor: 'pointer',
|
|
3247
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
3248
|
+
marginBottom: sizeScale.gap,
|
|
3249
|
+
backgroundColor: isChecked
|
|
3250
|
+
? 'var(--seekora-facet-active-bg, ' + theme.colors.hover + ')'
|
|
3251
|
+
: 'transparent',
|
|
3252
|
+
transition: 'background-color 0.2s ease',
|
|
3253
|
+
} },
|
|
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: {
|
|
3255
|
+
marginRight: sizeScale.gap,
|
|
3256
|
+
cursor: 'pointer',
|
|
3257
|
+
}, "aria-label": `Filter by ${item.value}` }),
|
|
3258
|
+
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
3259
|
+
flex: 1,
|
|
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',
|
|
2515
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,
|
|
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',
|
|
3312
|
+
} }, item.value),
|
|
3313
|
+
showCounts && (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
3314
|
+
fontSize: theme.typography.fontSize.small,
|
|
3315
|
+
color: 'var(--seekora-facet-count-color, ' + (theme.colors.textSecondary || theme.colors.text) + ')',
|
|
3316
|
+
lineHeight: 1,
|
|
3317
|
+
marginTop: '0.125rem',
|
|
3318
|
+
} }, item.count))));
|
|
3319
|
+
};
|
|
3320
|
+
// -------------------------------------------------------------------
|
|
3321
|
+
// Item renderer dispatcher
|
|
3322
|
+
// -------------------------------------------------------------------
|
|
3323
|
+
const renderItem = (item, facet, index) => {
|
|
3324
|
+
if (renderFacetItem) {
|
|
3325
|
+
return renderFacetItem(item, facet, index);
|
|
3326
|
+
}
|
|
3327
|
+
switch (variant) {
|
|
3328
|
+
case 'color-swatch':
|
|
3329
|
+
return renderColorSwatchItem(item, facet, index);
|
|
3330
|
+
case 'collapsible':
|
|
3331
|
+
case 'checkbox':
|
|
3332
|
+
default:
|
|
3333
|
+
return defaultRenderFacetItem(item, facet, index);
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
// -------------------------------------------------------------------
|
|
3337
|
+
// Keyboard handler (shared across variants)
|
|
3338
|
+
// -------------------------------------------------------------------
|
|
3339
|
+
const handleListKeyDown = (e, visibleItems, facet) => {
|
|
3340
|
+
const currentEl = e.currentTarget.querySelector('[aria-selected="true"]');
|
|
3341
|
+
const allItems = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
3342
|
+
const currentIdx = currentEl ? allItems.indexOf(currentEl) : -1;
|
|
3343
|
+
if (e.key === 'ArrowDown') {
|
|
3344
|
+
e.preventDefault();
|
|
3345
|
+
const next = Math.min(currentIdx + 1, allItems.length - 1);
|
|
3346
|
+
allItems[next]?.focus();
|
|
3347
|
+
}
|
|
3348
|
+
else if (e.key === 'ArrowUp') {
|
|
3349
|
+
e.preventDefault();
|
|
3350
|
+
const prev = Math.max(currentIdx - 1, 0);
|
|
3351
|
+
allItems[prev]?.focus();
|
|
3352
|
+
}
|
|
3353
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
3354
|
+
e.preventDefault();
|
|
3355
|
+
if (currentIdx >= 0 && currentIdx < visibleItems.length) {
|
|
3356
|
+
handleFacetToggle(facet.field, visibleItems[currentIdx].value, visibleItems[currentIdx].selected || false);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
// -------------------------------------------------------------------
|
|
3361
|
+
// Show more / less buttons (shared)
|
|
3362
|
+
// -------------------------------------------------------------------
|
|
3363
|
+
const renderShowMoreLess = (facet, filteredItems) => {
|
|
3364
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
3365
|
+
const hasMore = filteredItems.length > maxItems;
|
|
3366
|
+
return (React.createElement(React.Fragment, null,
|
|
3367
|
+
showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
3368
|
+
marginTop: sizeScale.gap,
|
|
3369
|
+
padding: sizeScale.padding,
|
|
3370
|
+
border: 'none',
|
|
3371
|
+
backgroundColor: 'transparent',
|
|
3372
|
+
color: theme.colors.primary,
|
|
3373
|
+
cursor: 'pointer',
|
|
3374
|
+
fontSize: theme.typography.fontSize.small,
|
|
3375
|
+
textDecoration: 'underline',
|
|
3376
|
+
} },
|
|
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"))));
|
|
3390
|
+
};
|
|
3391
|
+
// -------------------------------------------------------------------
|
|
3392
|
+
// Default facet group renderer — Checkbox variant
|
|
3393
|
+
// -------------------------------------------------------------------
|
|
3394
|
+
const renderCheckboxFacet = (facet, _index) => {
|
|
3395
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
3396
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
3397
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
3398
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
3399
|
+
marginBottom: theme.spacing.large,
|
|
3400
|
+
padding: theme.spacing.medium,
|
|
3401
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
3402
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
2516
3403
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
2517
|
-
marginBottom: theme.spacing.small,
|
|
2518
|
-
backgroundColor: (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) ? theme.colors.hover : 'transparent',
|
|
2519
|
-
transition: 'background-color 0.2s ease',
|
|
2520
3404
|
} },
|
|
2521
|
-
React.createElement("
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
flex: 1,
|
|
2527
|
-
fontSize: theme.typography.fontSize.medium,
|
|
3405
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
3406
|
+
fontSize: theme.typography.fontSize.large,
|
|
3407
|
+
fontWeight: 'bold',
|
|
3408
|
+
margin: 0,
|
|
3409
|
+
marginBottom: theme.spacing.medium,
|
|
2528
3410
|
color: theme.colors.text,
|
|
2529
|
-
} },
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
opacity: 0.7,
|
|
2534
|
-
marginLeft: theme.spacing.small,
|
|
2535
|
-
} },
|
|
2536
|
-
"(",
|
|
2537
|
-
item.count,
|
|
2538
|
-
")")));
|
|
3411
|
+
} }, facet.label || facet.field),
|
|
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)));
|
|
2539
3415
|
};
|
|
2540
|
-
|
|
3416
|
+
// -------------------------------------------------------------------
|
|
3417
|
+
// Color-swatch facet group renderer
|
|
3418
|
+
// -------------------------------------------------------------------
|
|
3419
|
+
const renderColorSwatchFacet = (facet, _index) => {
|
|
3420
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
2541
3421
|
const isExpanded = expandedFacets[facet.field] || false;
|
|
2542
|
-
const visibleItems = isExpanded ?
|
|
2543
|
-
const hasMore = facet.items.length > maxItems;
|
|
3422
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
2544
3423
|
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
2545
3424
|
marginBottom: theme.spacing.large,
|
|
2546
3425
|
padding: theme.spacing.medium,
|
|
2547
|
-
backgroundColor: theme.colors.background,
|
|
2548
|
-
border: `1px solid ${theme.colors.border}`,
|
|
3426
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
3427
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
2549
3428
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
2550
3429
|
} },
|
|
2551
3430
|
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
@@ -2555,36 +3434,106 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2555
3434
|
marginBottom: theme.spacing.medium,
|
|
2556
3435
|
color: theme.colors.text,
|
|
2557
3436
|
} }, facet.label || facet.field),
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
:
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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,
|
|
2567
3472
|
border: 'none',
|
|
2568
3473
|
backgroundColor: 'transparent',
|
|
2569
|
-
color: theme.colors.primary,
|
|
2570
3474
|
cursor: 'pointer',
|
|
2571
|
-
|
|
2572
|
-
textDecoration: 'underline',
|
|
3475
|
+
textAlign: 'left',
|
|
2573
3476
|
} },
|
|
2574
|
-
"
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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"))))));
|
|
2587
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
|
+
}
|
|
3533
|
+
};
|
|
3534
|
+
// -------------------------------------------------------------------
|
|
3535
|
+
// Empty state
|
|
3536
|
+
// -------------------------------------------------------------------
|
|
2588
3537
|
if (facets.length === 0) {
|
|
2589
3538
|
log.verbose('Facets: No facets to display', {
|
|
2590
3539
|
hasResults: !!results,
|
|
@@ -2592,7 +3541,13 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2592
3541
|
});
|
|
2593
3542
|
return null;
|
|
2594
3543
|
}
|
|
2595
|
-
|
|
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) => {
|
|
2596
3551
|
return renderFacet
|
|
2597
3552
|
? renderFacet(facet, index)
|
|
2598
3553
|
: defaultRenderFacet(facet);
|
|
@@ -2602,72 +3557,222 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2602
3557
|
/**
|
|
2603
3558
|
* CurrentRefinements Component
|
|
2604
3559
|
*
|
|
2605
|
-
* 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.
|
|
2606
3562
|
*/
|
|
2607
|
-
|
|
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, }) => {
|
|
2608
3643
|
const { theme } = useSearchContext();
|
|
3644
|
+
const { refinements: stateRefinements, removeRefinement, clearRefinements } = useSearchState();
|
|
2609
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]);
|
|
2610
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
|
+
}
|
|
2611
3677
|
if (onRefinementClear) {
|
|
2612
3678
|
onRefinementClear(field, value);
|
|
2613
3679
|
}
|
|
2614
3680
|
};
|
|
2615
3681
|
const handleClearAll = () => {
|
|
3682
|
+
// If synced with StateManager, auto-clear all via StateManager
|
|
3683
|
+
if (refinementsProp === undefined) {
|
|
3684
|
+
clearRefinements();
|
|
3685
|
+
}
|
|
2616
3686
|
if (onClearAll) {
|
|
2617
3687
|
onClearAll();
|
|
2618
3688
|
}
|
|
2619
3689
|
};
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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,
|
|
2634
3706
|
} },
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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
|
+
};
|
|
2657
3730
|
if (refinements.length === 0) {
|
|
2658
3731
|
return null;
|
|
2659
3732
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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) => {
|
|
2667
3772
|
return renderRefinement
|
|
2668
3773
|
? renderRefinement(refinement, index)
|
|
2669
3774
|
: defaultRenderRefinement(refinement, index);
|
|
2670
|
-
})),
|
|
3775
|
+
}))),
|
|
2671
3776
|
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
2672
3777
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
2673
3778
|
border: `1px solid ${theme.colors.border}`,
|
|
@@ -2677,6 +3782,7 @@ const CurrentRefinements = ({ refinements = [], onRefinementClear, onClearAll, r
|
|
|
2677
3782
|
cursor: 'pointer',
|
|
2678
3783
|
fontSize: theme.typography.fontSize.small,
|
|
2679
3784
|
textDecoration: 'underline',
|
|
3785
|
+
transition: 'background-color 150ms ease-in-out',
|
|
2680
3786
|
} }, "Clear all filters"))));
|
|
2681
3787
|
};
|
|
2682
3788
|
|
|
@@ -3525,6 +4631,110 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3525
4631
|
const toggleShowMore = (level) => {
|
|
3526
4632
|
setExpanded(prev => ({ ...prev, [level]: !prev[level] }));
|
|
3527
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]);
|
|
3528
4738
|
// Render a level of the hierarchy
|
|
3529
4739
|
const renderLevel = (items, level) => {
|
|
3530
4740
|
if (!items || items.length === 0)
|
|
@@ -3533,12 +4743,14 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3533
4743
|
const displayLimit = isExpanded ? showMoreLimit : limit;
|
|
3534
4744
|
const displayItems = items.slice(0, displayLimit);
|
|
3535
4745
|
const hasMore = items.length > displayLimit;
|
|
3536
|
-
return (React.createElement("ul", { className: hierarchicalTheme.list, style: {
|
|
4746
|
+
return (React.createElement("ul", { role: level === 0 ? 'tree' : 'group', className: hierarchicalTheme.list, style: {
|
|
3537
4747
|
listStyle: 'none',
|
|
3538
4748
|
margin: 0,
|
|
3539
4749
|
padding: level > 0 ? `0 0 0 ${theme.spacing.medium}` : 0,
|
|
3540
4750
|
} },
|
|
3541
|
-
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: {
|
|
3542
4754
|
padding: `${theme.spacing.small} 0`,
|
|
3543
4755
|
} }, renderItem ? (renderItem(item, level)) : (React.createElement(React.Fragment, null,
|
|
3544
4756
|
React.createElement("button", { type: "button", onClick: () => handleItemClick(item, level), className: hierarchicalTheme.link, style: {
|
|
@@ -3576,7 +4788,7 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3576
4788
|
if (processedItems.length === 0) {
|
|
3577
4789
|
return null;
|
|
3578
4790
|
}
|
|
3579
|
-
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)));
|
|
3580
4792
|
};
|
|
3581
4793
|
|
|
3582
4794
|
/**
|
|
@@ -3676,6 +4888,53 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3676
4888
|
const handleDragEnd = () => {
|
|
3677
4889
|
setIsDragging(false);
|
|
3678
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
|
+
};
|
|
3679
4938
|
// Calculate filled track position
|
|
3680
4939
|
const minPercent = ((internalMin - min) / (max - min)) * 100;
|
|
3681
4940
|
const maxPercent = ((internalMax - min) / (max - min)) * 100;
|
|
@@ -3711,7 +4970,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3711
4970
|
backgroundColor: theme.colors.primary,
|
|
3712
4971
|
borderRadius: '2px',
|
|
3713
4972
|
} }),
|
|
3714
|
-
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: {
|
|
3715
4974
|
position: 'absolute',
|
|
3716
4975
|
width: '100%',
|
|
3717
4976
|
height: '4px',
|
|
@@ -3721,7 +4980,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3721
4980
|
cursor: 'pointer',
|
|
3722
4981
|
pointerEvents: 'none',
|
|
3723
4982
|
}, "aria-label": `Minimum ${label || field}` }),
|
|
3724
|
-
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: {
|
|
3725
4984
|
position: 'absolute',
|
|
3726
4985
|
width: '100%',
|
|
3727
4986
|
height: '4px',
|
|
@@ -4699,12 +5958,29 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4699
5958
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4700
5959
|
if (error.name === 'AbortError')
|
|
4701
5960
|
return;
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
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
|
+
}
|
|
4708
5984
|
}
|
|
4709
5985
|
}
|
|
4710
5986
|
finally {
|
|
@@ -6271,7 +7547,14 @@ const EVENTS = {
|
|
|
6271
7547
|
TRENDING_CLICK: 'suggestions.trending_click',
|
|
6272
7548
|
SEARCH_SUBMIT: 'suggestions.search_submit',
|
|
6273
7549
|
DROPDOWN_OPEN: 'suggestions.dropdown_open',
|
|
6274
|
-
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
|
+
};
|
|
6275
7558
|
// ============================================================================
|
|
6276
7559
|
// Hook Implementation
|
|
6277
7560
|
// ============================================================================
|
|
@@ -6295,11 +7578,16 @@ function useSuggestionsAnalytics(options) {
|
|
|
6295
7578
|
return;
|
|
6296
7579
|
const searchContext = context ?? contextOption;
|
|
6297
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;
|
|
6298
7584
|
await client.trackEvent?.({
|
|
6299
7585
|
event_name: eventName,
|
|
6300
7586
|
analytics_tags: analyticsTags,
|
|
7587
|
+
// Include query at top level for search events
|
|
7588
|
+
...(isSearchEvent && query ? { query } : {}),
|
|
6301
7589
|
metadata: {
|
|
6302
|
-
...
|
|
7590
|
+
...restMetadata,
|
|
6303
7591
|
timestamp: Date.now(),
|
|
6304
7592
|
source: 'suggestions_dropdown',
|
|
6305
7593
|
},
|
|
@@ -7122,7 +8410,7 @@ function DropdownPanel({ children, position = 'absolute', top = '100%', left = 0
|
|
|
7122
8410
|
|
|
7123
8411
|
/**
|
|
7124
8412
|
* Parses suggestion text containing <mark>...</mark> and returns React nodes
|
|
7125
|
-
* with the marked segments rendered as
|
|
8413
|
+
* with the marked segments rendered as styled elements. Safe: inner content
|
|
7126
8414
|
* is rendered as text, not HTML.
|
|
7127
8415
|
*/
|
|
7128
8416
|
const defaultMarkStyle = {
|
|
@@ -7131,9 +8419,34 @@ const defaultMarkStyle = {
|
|
|
7131
8419
|
borderRadius: '2px',
|
|
7132
8420
|
padding: '0 2px',
|
|
7133
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
|
+
}
|
|
7134
8447
|
/**
|
|
7135
8448
|
* Converts a string like "lined <mark>blue</mark>" into React nodes with
|
|
7136
|
-
* the marked part rendered as a
|
|
8449
|
+
* the marked part rendered as a styled element. When no <mark> tags are
|
|
7137
8450
|
* present, returns the string as-is.
|
|
7138
8451
|
*/
|
|
7139
8452
|
function parseHighlightMarkup(text, options = {}) {
|
|
@@ -7142,11 +8455,18 @@ function parseHighlightMarkup(text, options = {}) {
|
|
|
7142
8455
|
const parts = text.split(/(<mark>[\s\S]*?<\/mark>)/g);
|
|
7143
8456
|
if (parts.length <= 1)
|
|
7144
8457
|
return text;
|
|
7145
|
-
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;
|
|
7146
8466
|
return (React.createElement(React.Fragment, null, parts.map((part, i) => {
|
|
7147
8467
|
const m = part.match(/^<mark>([\s\S]*)<\/mark>$/);
|
|
7148
8468
|
if (m) {
|
|
7149
|
-
return (React.createElement(
|
|
8469
|
+
return (React.createElement(Tag, { key: i, className: markClassName, style: { ...computedStyle, ...markStyle } }, m[1]));
|
|
7150
8470
|
}
|
|
7151
8471
|
return part;
|
|
7152
8472
|
})));
|
|
@@ -8021,6 +9341,41 @@ const highlightText = (text, query, options = {}) => {
|
|
|
8021
9341
|
const classAttr = className ? ` class="${className}"` : '';
|
|
8022
9342
|
return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
|
|
8023
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
|
+
};
|
|
8024
9379
|
// ============================================================================
|
|
8025
9380
|
// Variant Utilities
|
|
8026
9381
|
// ============================================================================
|
|
@@ -10565,9 +11920,7 @@ const AmazonDropdown = React.forwardRef(function AmazonDropdown(props, ref) {
|
|
|
10565
11920
|
React.createElement("div", { style: styles.suggestionIcon },
|
|
10566
11921
|
React.createElement(SearchIcon$5, null)),
|
|
10567
11922
|
React.createElement("div", { style: styles.suggestionContent },
|
|
10568
|
-
React.createElement("div", { style: styles.suggestionQuery,
|
|
10569
|
-
__html: highlightText(suggestion.query, query, { className: 'highlight' })
|
|
10570
|
-
} }),
|
|
11923
|
+
React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { className: 'highlight' })),
|
|
10571
11924
|
showDepartments && firstCategory && (React.createElement("div", { style: styles.suggestionContext },
|
|
10572
11925
|
React.createElement("span", { style: styles.suggestionDepartment },
|
|
10573
11926
|
"in ",
|
|
@@ -10912,11 +12265,7 @@ const GoogleDropdown = React.forwardRef(function GoogleDropdown(props, ref) {
|
|
|
10912
12265
|
item.type === 'trending' ? React.createElement(TrendingIcon$2, null) :
|
|
10913
12266
|
React.createElement(SearchIcon$4, null)),
|
|
10914
12267
|
React.createElement("div", { style: styles.itemContent },
|
|
10915
|
-
React.createElement("span", { style: styles.itemText,
|
|
10916
|
-
__html: query
|
|
10917
|
-
? highlightText(item.query, query, { tag: 'b' })
|
|
10918
|
-
: item.query
|
|
10919
|
-
} }),
|
|
12268
|
+
React.createElement("span", { style: styles.itemText }, query ? highlightTextReact(item.query, query, { tag: 'b' }) : item.query),
|
|
10920
12269
|
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
10921
12270
|
item.type === 'trending' && showTrendingIndicator && (React.createElement("span", { style: styles.trending },
|
|
10922
12271
|
React.createElement(TrendingIcon$2, null),
|
|
@@ -11371,11 +12720,7 @@ const PinterestDropdown = React.forwardRef(function PinterestDropdown(props, ref
|
|
|
11371
12720
|
setHoveredSuggestion(idx);
|
|
11372
12721
|
setActiveIndex(idx);
|
|
11373
12722
|
}, onMouseLeave: () => setHoveredSuggestion(-1) },
|
|
11374
|
-
React.createElement("span", { style: styles.suggestionIcon,
|
|
11375
|
-
__html: query
|
|
11376
|
-
? highlightText(s.query, query, { tag: 'strong' })
|
|
11377
|
-
: s.query
|
|
11378
|
-
} })))))),
|
|
12723
|
+
React.createElement("span", { style: styles.suggestionIcon }, query ? highlightTextReact(s.query, query, { tag: 'strong' }) : s.query)))))),
|
|
11379
12724
|
displayProducts.length > 0 && (React.createElement(React.Fragment, null,
|
|
11380
12725
|
React.createElement("div", { style: styles.sectionTitle },
|
|
11381
12726
|
React.createElement(TrendingIcon$1, null),
|
|
@@ -11905,9 +13250,7 @@ const SpotlightDropdown = React.forwardRef(function SpotlightDropdown(props, ref
|
|
|
11905
13250
|
}, onMouseEnter: () => setActiveIndex(idx) },
|
|
11906
13251
|
React.createElement("div", { style: mergeStyles(styles.itemIcon, isActive ? styles.itemIconActive : undefined) }, item.icon),
|
|
11907
13252
|
React.createElement("div", { style: styles.itemContent },
|
|
11908
|
-
React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined),
|
|
11909
|
-
__html: highlightText(item.title, query, { tag: 'strong' })
|
|
11910
|
-
} }))));
|
|
13253
|
+
React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined) }, highlightTextReact(item.title, query, { tag: 'strong' })))));
|
|
11911
13254
|
}))),
|
|
11912
13255
|
processedProducts.length > 0 && (React.createElement("div", { style: styles.section },
|
|
11913
13256
|
React.createElement("div", { style: styles.sectionTitle }, "Products"),
|
|
@@ -12347,9 +13690,7 @@ const ShopifyDropdown = React.forwardRef(function ShopifyDropdown(props, ref) {
|
|
|
12347
13690
|
React.createElement("div", { style: styles.suggestionIcon },
|
|
12348
13691
|
React.createElement(SearchIcon$1, null)),
|
|
12349
13692
|
React.createElement("div", { style: styles.suggestionContent },
|
|
12350
|
-
React.createElement("div", { style: styles.suggestionQuery,
|
|
12351
|
-
__html: highlightText(suggestion.query, query, { tag: 'mark' })
|
|
12352
|
-
} }),
|
|
13693
|
+
React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { tag: 'mark' })),
|
|
12353
13694
|
suggestion.count && (React.createElement("div", { style: styles.suggestionMeta },
|
|
12354
13695
|
suggestion.count,
|
|
12355
13696
|
" results"))),
|
|
@@ -12852,9 +14193,7 @@ const MobileSheetDropdown = React.forwardRef(function MobileSheetDropdown(props,
|
|
|
12852
14193
|
React.createElement("div", { style: styles.itemIcon },
|
|
12853
14194
|
React.createElement(SearchIcon, null)),
|
|
12854
14195
|
React.createElement("div", { style: styles.itemContent },
|
|
12855
|
-
React.createElement("div", { style: styles.itemTitle,
|
|
12856
|
-
__html: highlightText(s.query, inputValue, { tag: 'strong' })
|
|
12857
|
-
} }),
|
|
14196
|
+
React.createElement("div", { style: styles.itemTitle }, highlightTextReact(s.query, inputValue, { tag: 'strong' })),
|
|
12858
14197
|
s.count && (React.createElement("div", { style: styles.itemSubtitle },
|
|
12859
14198
|
s.count,
|
|
12860
14199
|
" results"))),
|
|
@@ -13155,9 +14494,7 @@ const MinimalDropdown = React.forwardRef(function MinimalDropdown(props, ref) {
|
|
|
13155
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) },
|
|
13156
14495
|
showIndices && (React.createElement("span", { style: styles.itemIndex }, itemIdx + 1)),
|
|
13157
14496
|
React.createElement("div", { style: styles.itemContent },
|
|
13158
|
-
React.createElement("div", { style: styles.itemQuery,
|
|
13159
|
-
__html: highlightText(s.query, query, { tag: 'mark' })
|
|
13160
|
-
} }),
|
|
14497
|
+
React.createElement("div", { style: styles.itemQuery }, highlightTextReact(s.query, query, { tag: 'mark' })),
|
|
13161
14498
|
s.count && (React.createElement("div", { style: styles.itemMeta },
|
|
13162
14499
|
s.count.toLocaleString(),
|
|
13163
14500
|
" results"))),
|