@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.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo,
|
|
1
|
+
import React, { createContext, useContext, useMemo, useEffect, useState, useCallback, useRef, useId, forwardRef, useImperativeHandle, createElement, useReducer } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { SeekoraClient } from '@seekora-ai/search-sdk';
|
|
4
4
|
|
|
@@ -305,6 +305,9 @@ class SearchStateManager {
|
|
|
305
305
|
this.autoSearch = config.autoSearch !== false;
|
|
306
306
|
this.debounceMs = config.debounceMs || 300;
|
|
307
307
|
this.defaultSearchOptions = config.defaultSearchOptions || { widget_mode: true };
|
|
308
|
+
this.keepResultsOnClear = config.keepResultsOnClear !== false;
|
|
309
|
+
this.abTestId = config.abTestId;
|
|
310
|
+
this.abVariant = config.abVariant;
|
|
308
311
|
this.state = {
|
|
309
312
|
query: config.initialQuery || '',
|
|
310
313
|
refinements: [],
|
|
@@ -353,6 +356,15 @@ class SearchStateManager {
|
|
|
353
356
|
}
|
|
354
357
|
return;
|
|
355
358
|
}
|
|
359
|
+
// When query is cleared to empty and keepResultsOnClear is true,
|
|
360
|
+
// preserve previous results and skip triggering a search
|
|
361
|
+
if (query === '' && this.keepResultsOnClear) {
|
|
362
|
+
log.verbose('SearchStateManager: Query cleared, keeping previous results');
|
|
363
|
+
this.state.query = '';
|
|
364
|
+
this.state.currentPage = 1;
|
|
365
|
+
this.notifyListeners();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
356
368
|
this.state.query = query;
|
|
357
369
|
this.state.currentPage = 1; // Reset to first page on new query
|
|
358
370
|
log.verbose('SearchStateManager: Query updated', { query, triggerSearch, autoSearch: this.autoSearch });
|
|
@@ -551,6 +563,23 @@ class SearchStateManager {
|
|
|
551
563
|
}
|
|
552
564
|
});
|
|
553
565
|
}
|
|
566
|
+
/** Explicitly clear results (bypasses keepResultsOnClear) */
|
|
567
|
+
clearResults() {
|
|
568
|
+
this.setState({ results: null, loading: false, error: null });
|
|
569
|
+
log.verbose('SearchStateManager: Results explicitly cleared');
|
|
570
|
+
}
|
|
571
|
+
// A/B test getters
|
|
572
|
+
getAbTestId() {
|
|
573
|
+
return this.abTestId;
|
|
574
|
+
}
|
|
575
|
+
getAbVariant() {
|
|
576
|
+
return this.abVariant;
|
|
577
|
+
}
|
|
578
|
+
setAbTest(abTestId, abVariant) {
|
|
579
|
+
this.abTestId = abTestId;
|
|
580
|
+
this.abVariant = abVariant;
|
|
581
|
+
log.verbose('SearchStateManager: A/B test updated', { abTestId, abVariant });
|
|
582
|
+
}
|
|
554
583
|
// Clear all state
|
|
555
584
|
clear() {
|
|
556
585
|
this.state = {
|
|
@@ -1333,10 +1362,11 @@ const createTheme = (config) => {
|
|
|
1333
1362
|
/**
|
|
1334
1363
|
* SearchProvider Component
|
|
1335
1364
|
*
|
|
1336
|
-
* Provides Seekora client and context to child components
|
|
1365
|
+
* Provides Seekora client and context to child components.
|
|
1366
|
+
* Supports A/B testing via abTestId/abVariant props.
|
|
1337
1367
|
*/
|
|
1338
1368
|
const SearchContext = createContext(null);
|
|
1339
|
-
const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, }) => {
|
|
1369
|
+
const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, autoTrackSearch = true, stateManager: stateManagerConfig, children, abTestId, abVariant, experiments: _experiments, }) => {
|
|
1340
1370
|
const theme = useMemo(() => {
|
|
1341
1371
|
return themeConfig ? createTheme(themeConfig) : defaultTheme;
|
|
1342
1372
|
}, [themeConfig]);
|
|
@@ -1349,8 +1379,19 @@ const SearchProvider = ({ client, theme: themeConfig, enableAnalytics = true, au
|
|
|
1349
1379
|
itemsPerPage: 10,
|
|
1350
1380
|
defaultSearchOptions: { widget_mode: true },
|
|
1351
1381
|
...stateManagerConfig,
|
|
1382
|
+
abTestId,
|
|
1383
|
+
abVariant,
|
|
1352
1384
|
});
|
|
1353
|
-
}, [client, stateManagerConfig]);
|
|
1385
|
+
}, [client, stateManagerConfig, abTestId, abVariant]);
|
|
1386
|
+
// Update A/B test fields on state manager and SDK client when props change
|
|
1387
|
+
useEffect(() => {
|
|
1388
|
+
if (abTestId !== undefined || abVariant !== undefined) {
|
|
1389
|
+
stateManager.setAbTest(abTestId, abVariant);
|
|
1390
|
+
if (typeof client.setAbTest === 'function') {
|
|
1391
|
+
client.setAbTest(abTestId, abVariant);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}, [stateManager, client, abTestId, abVariant]);
|
|
1354
1395
|
const value = useMemo(() => ({
|
|
1355
1396
|
client,
|
|
1356
1397
|
theme,
|
|
@@ -1483,9 +1524,25 @@ const useQuerySuggestions = ({ client, query, enabled = true, debounceMs = 300,
|
|
|
1483
1524
|
}
|
|
1484
1525
|
catch (err) {
|
|
1485
1526
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1527
|
+
// Check if it's a 404 error (suggestions not enabled for this store).
|
|
1528
|
+
// The search SDK wraps axios errors into plain Error objects with the status
|
|
1529
|
+
// embedded in the message, e.g. "[getSuggestions] ... (404)".
|
|
1530
|
+
const errMsg = error.message || '';
|
|
1531
|
+
const is404 = err?.response?.status === 404 ||
|
|
1532
|
+
err?.status === 404 ||
|
|
1533
|
+
/\(404\)/.test(errMsg);
|
|
1534
|
+
if (is404) {
|
|
1535
|
+
// Silently handle 404 - suggestions feature not enabled
|
|
1536
|
+
log.verbose('Query suggestions not enabled for this store (404)');
|
|
1537
|
+
setSuggestions([]);
|
|
1538
|
+
setError(null);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
// For other errors, log and set error state
|
|
1542
|
+
log.error('Error in useQuerySuggestions:', error);
|
|
1543
|
+
setError(error);
|
|
1544
|
+
setSuggestions([]);
|
|
1545
|
+
}
|
|
1489
1546
|
}
|
|
1490
1547
|
finally {
|
|
1491
1548
|
setLoading(false);
|
|
@@ -1511,7 +1568,42 @@ function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else i
|
|
|
1511
1568
|
*
|
|
1512
1569
|
* Interactive search input component with query suggestions support
|
|
1513
1570
|
*/
|
|
1514
|
-
const
|
|
1571
|
+
const SIZE_CONFIG = {
|
|
1572
|
+
small: {
|
|
1573
|
+
padding: '0.375rem 0.5rem',
|
|
1574
|
+
fontSize: '0.875rem',
|
|
1575
|
+
iconSize: 14,
|
|
1576
|
+
iconPaddingLeft: '1.75rem',
|
|
1577
|
+
iconPaddingRight: '1.75rem',
|
|
1578
|
+
iconLeft: '0.5rem',
|
|
1579
|
+
iconRight: '0.5rem',
|
|
1580
|
+
},
|
|
1581
|
+
medium: {
|
|
1582
|
+
padding: '0.625rem 1rem',
|
|
1583
|
+
fontSize: '1rem',
|
|
1584
|
+
iconSize: 18,
|
|
1585
|
+
iconPaddingLeft: '2.25rem',
|
|
1586
|
+
iconPaddingRight: '2.25rem',
|
|
1587
|
+
iconLeft: '0.625rem',
|
|
1588
|
+
iconRight: '0.625rem',
|
|
1589
|
+
},
|
|
1590
|
+
large: {
|
|
1591
|
+
padding: '0.875rem 1.25rem',
|
|
1592
|
+
fontSize: '1.25rem',
|
|
1593
|
+
iconSize: 22,
|
|
1594
|
+
iconPaddingLeft: '2.75rem',
|
|
1595
|
+
iconPaddingRight: '2.75rem',
|
|
1596
|
+
iconLeft: '0.75rem',
|
|
1597
|
+
iconRight: '0.75rem',
|
|
1598
|
+
},
|
|
1599
|
+
};
|
|
1600
|
+
const DefaultSearchIcon = ({ size = 18 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
1601
|
+
React.createElement("circle", { cx: "11", cy: "11", r: "8" }),
|
|
1602
|
+
React.createElement("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })));
|
|
1603
|
+
const DefaultClearIcon = ({ size = 14 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" },
|
|
1604
|
+
React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
1605
|
+
React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })));
|
|
1606
|
+
const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, showLoadingState = false, renderSuggestion, renderLoading, renderSearchIcon, showClearButton = true, renderClearIcon, showSubmitButton = false, renderSubmitButton, size = 'medium', }) => {
|
|
1515
1607
|
const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
|
|
1516
1608
|
const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
|
|
1517
1609
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -1652,28 +1744,104 @@ const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounce
|
|
|
1652
1744
|
const processingTime = res?.processingTimeMS
|
|
1653
1745
|
|| res?.data?.processingTimeMS
|
|
1654
1746
|
|| res?.data?.data?.processingTimeMS;
|
|
1747
|
+
// Size-based configuration
|
|
1748
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
1749
|
+
const hasClearBtn = showClearButton && query.length > 0;
|
|
1750
|
+
// Compute input padding accounting for icons
|
|
1751
|
+
const inputPaddingLeft = sizeConfig.iconPaddingLeft ;
|
|
1752
|
+
const inputPaddingRight = hasClearBtn ? sizeConfig.iconPaddingRight : sizeConfig.padding.split(' ')[1] || sizeConfig.padding;
|
|
1753
|
+
const borderRadius = typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium;
|
|
1754
|
+
const focusBorderColor = isFocused ? theme.colors.focus : theme.colors.border;
|
|
1755
|
+
const focusRingShadow = isFocused
|
|
1756
|
+
? `0 0 0 3px ${theme.colors.focus}33`
|
|
1757
|
+
: undefined;
|
|
1758
|
+
const handleClear = useCallback(() => {
|
|
1759
|
+
setQuery('', false);
|
|
1760
|
+
setSelectedIndex(-1);
|
|
1761
|
+
inputRef.current?.focus();
|
|
1762
|
+
}, [setQuery]);
|
|
1763
|
+
const handleSubmit = useCallback(() => {
|
|
1764
|
+
const currentValue = inputRef.current?.value || query;
|
|
1765
|
+
handleSearch(currentValue);
|
|
1766
|
+
}, [query, handleSearch]);
|
|
1655
1767
|
return (React.createElement("div", { ref: containerRef, className: clsx(searchBarTheme.container, className), style: {
|
|
1656
1768
|
position: 'relative',
|
|
1657
1769
|
display: 'flex',
|
|
1658
1770
|
alignItems: 'center',
|
|
1771
|
+
// CSS custom properties for external styling
|
|
1772
|
+
'--seekora-searchbar-bg': theme.colors.background,
|
|
1773
|
+
'--seekora-searchbar-border': theme.colors.border,
|
|
1774
|
+
'--seekora-searchbar-focus-border': theme.colors.focus,
|
|
1775
|
+
'--seekora-searchbar-radius': borderRadius,
|
|
1776
|
+
'--seekora-searchbar-icon-color': theme.colors.textSecondary,
|
|
1659
1777
|
...style,
|
|
1660
1778
|
} },
|
|
1661
|
-
React.createElement("
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1779
|
+
React.createElement("div", { style: { position: 'relative', flex: 1, display: 'flex', alignItems: 'center' } },
|
|
1780
|
+
(React.createElement("span", { className: searchBarTheme.searchIcon, "aria-hidden": "true", style: {
|
|
1781
|
+
position: 'absolute',
|
|
1782
|
+
left: sizeConfig.iconLeft,
|
|
1783
|
+
top: '50%',
|
|
1784
|
+
transform: 'translateY(-50%)',
|
|
1785
|
+
display: 'flex',
|
|
1786
|
+
alignItems: 'center',
|
|
1787
|
+
justifyContent: 'center',
|
|
1788
|
+
pointerEvents: 'none',
|
|
1789
|
+
color: 'var(--seekora-searchbar-icon-color)',
|
|
1790
|
+
zIndex: 1,
|
|
1791
|
+
} }, renderSearchIcon ? renderSearchIcon() : React.createElement(DefaultSearchIcon, { size: sizeConfig.iconSize }))),
|
|
1792
|
+
React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
|
|
1793
|
+
width: '100%',
|
|
1794
|
+
paddingTop: sizeConfig.padding.split(' ')[0],
|
|
1795
|
+
paddingBottom: sizeConfig.padding.split(' ')[0],
|
|
1796
|
+
paddingLeft: inputPaddingLeft,
|
|
1797
|
+
paddingRight: inputPaddingRight,
|
|
1798
|
+
fontSize: sizeConfig.fontSize,
|
|
1799
|
+
fontFamily: theme.typography.fontFamily,
|
|
1800
|
+
backgroundColor: 'var(--seekora-searchbar-bg)',
|
|
1801
|
+
color: theme.colors.text,
|
|
1802
|
+
borderWidth: '1px',
|
|
1803
|
+
borderStyle: 'solid',
|
|
1804
|
+
borderColor: focusBorderColor,
|
|
1805
|
+
borderRadius: 'var(--seekora-searchbar-radius)',
|
|
1806
|
+
outline: 'none',
|
|
1807
|
+
boxShadow: focusRingShadow,
|
|
1808
|
+
transition: theme.transitions?.fast || '150ms ease-in-out',
|
|
1809
|
+
boxSizing: 'border-box',
|
|
1810
|
+
} }),
|
|
1811
|
+
hasClearBtn && (React.createElement("button", { type: "button", onClick: handleClear, className: searchBarTheme.clearButton, "aria-label": "Clear search", style: {
|
|
1812
|
+
position: 'absolute',
|
|
1813
|
+
right: sizeConfig.iconRight,
|
|
1814
|
+
top: '50%',
|
|
1815
|
+
transform: 'translateY(-50%)',
|
|
1816
|
+
display: 'flex',
|
|
1817
|
+
alignItems: 'center',
|
|
1818
|
+
justifyContent: 'center',
|
|
1819
|
+
background: 'none',
|
|
1820
|
+
border: 'none',
|
|
1821
|
+
cursor: 'pointer',
|
|
1822
|
+
padding: '2px',
|
|
1823
|
+
borderRadius: '50%',
|
|
1824
|
+
color: 'var(--seekora-searchbar-icon-color)',
|
|
1825
|
+
transition: theme.transitions?.fast || '150ms ease-in-out',
|
|
1826
|
+
zIndex: 1,
|
|
1827
|
+
}, onMouseDown: (e) => {
|
|
1828
|
+
// Prevent input blur so the clear action doesn't race with blur handler
|
|
1829
|
+
e.preventDefault();
|
|
1830
|
+
} }, renderClearIcon ? renderClearIcon() : React.createElement(DefaultClearIcon, { size: sizeConfig.iconSize - 4 })))),
|
|
1831
|
+
showSubmitButton && (renderSubmitButton ? (React.createElement("div", { onClick: handleSubmit, style: { marginLeft: theme.spacing.small, cursor: 'pointer' } }, renderSubmitButton())) : (React.createElement("button", { type: "button", onClick: handleSubmit, className: searchBarTheme.submitButton, style: {
|
|
1832
|
+
marginLeft: theme.spacing.small,
|
|
1833
|
+
padding: sizeConfig.padding,
|
|
1834
|
+
fontSize: sizeConfig.fontSize,
|
|
1665
1835
|
fontFamily: theme.typography.fontFamily,
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
}),
|
|
1676
|
-
} }),
|
|
1836
|
+
fontWeight: theme.typography.fontWeight?.medium ?? 500,
|
|
1837
|
+
backgroundColor: theme.colors.primary,
|
|
1838
|
+
color: '#ffffff',
|
|
1839
|
+
border: 'none',
|
|
1840
|
+
borderRadius: 'var(--seekora-searchbar-radius)',
|
|
1841
|
+
cursor: 'pointer',
|
|
1842
|
+
whiteSpace: 'nowrap',
|
|
1843
|
+
transition: theme.transitions?.fast || '150ms ease-in-out',
|
|
1844
|
+
} }, "Search"))),
|
|
1677
1845
|
processingTime !== undefined && (React.createElement("span", { style: {
|
|
1678
1846
|
marginLeft: theme.spacing.small,
|
|
1679
1847
|
fontSize: theme.typography.fontSize.small,
|
|
@@ -1740,7 +1908,7 @@ const formatPrice$1 = (value, currency = '₹') => {
|
|
|
1740
1908
|
}
|
|
1741
1909
|
return String(value);
|
|
1742
1910
|
};
|
|
1743
|
-
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, }) => {
|
|
1911
|
+
const SearchResults = ({ results: resultsProp, loading: loadingProp, error: errorProp, onResultClick, renderResult, renderEmpty, showLoadingState = false, renderLoading, renderError, className, style, theme: customTheme, itemsPerPage = 20, showPagination = false, viewMode = 'list', fieldMapping, extractResults, enableKeyboardNavigation = true, autoFocus = false, minHeight = '400px', minWidth = '100%', loadingOpacity = 0.7, }) => {
|
|
1744
1912
|
const { theme, client, enableAnalytics } = useSearchContext();
|
|
1745
1913
|
const { results: stateResults, loading: stateLoading, error: stateError, currentPage, itemsPerPage: stateItemsPerPage } = useSearchState();
|
|
1746
1914
|
const searchResultsTheme = customTheme || {};
|
|
@@ -2084,6 +2252,10 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2084
2252
|
});
|
|
2085
2253
|
// Determine container style based on view mode
|
|
2086
2254
|
const containerStyle = {
|
|
2255
|
+
minHeight: `var(--seekora-results-min-height, ${minHeight})`,
|
|
2256
|
+
minWidth: `var(--seekora-results-min-width, ${minWidth})`,
|
|
2257
|
+
transition: 'opacity 200ms ease-in-out',
|
|
2258
|
+
opacity: loading && resultItems.length > 0 ? loadingOpacity : 1,
|
|
2087
2259
|
...style,
|
|
2088
2260
|
};
|
|
2089
2261
|
// Determine results list style based on view mode
|
|
@@ -2110,19 +2282,19 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2110
2282
|
// When loading with no previous results, show loading only if showLoadingState (default: show previous results, no loading screen)
|
|
2111
2283
|
if (loading && resultItems.length === 0 && showLoadingState) {
|
|
2112
2284
|
log.verbose('SearchResults: Rendering loading state');
|
|
2113
|
-
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style:
|
|
2285
|
+
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderLoading ? renderLoading() : defaultRenderLoading()));
|
|
2114
2286
|
}
|
|
2115
|
-
// When loading with previous results, fall through and render them (
|
|
2287
|
+
// When loading with previous results, fall through and render them (with opacity transition)
|
|
2116
2288
|
if (error) {
|
|
2117
2289
|
log.error('SearchResults: Rendering error state', {
|
|
2118
2290
|
error: error.message,
|
|
2119
2291
|
stack: error.stack,
|
|
2120
2292
|
});
|
|
2121
|
-
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style:
|
|
2293
|
+
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderError ? renderError(error) : defaultRenderError(error)));
|
|
2122
2294
|
}
|
|
2123
2295
|
if (!results || resultItems.length === 0) {
|
|
2124
2296
|
log.verbose('SearchResults: No results to display');
|
|
2125
|
-
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style:
|
|
2297
|
+
return (React.createElement("div", { className: clsx(searchResultsTheme.container, className), style: containerStyle }, renderEmpty ? renderEmpty() : defaultRenderEmpty()));
|
|
2126
2298
|
}
|
|
2127
2299
|
const renderFn = renderResult || defaultRenderResult;
|
|
2128
2300
|
return (React.createElement("div", { ref: containerRef, className: clsx(searchResultsTheme.container, className), style: containerStyle, tabIndex: enableKeyboardNavigation ? 0 : undefined, onKeyDown: handleKeyDown, role: "listbox", "aria-label": "Search results", "aria-activedescendant": activeIndex >= 0 ? `result-${activeIndex}` : undefined },
|
|
@@ -2167,10 +2339,44 @@ const SearchResults = ({ results: resultsProp, loading: loadingProp, error: erro
|
|
|
2167
2339
|
* Stats Component
|
|
2168
2340
|
*
|
|
2169
2341
|
* Displays search statistics (total results, processing time, etc.)
|
|
2342
|
+
* Supports inline, badge, and detailed display variants.
|
|
2170
2343
|
*/
|
|
2171
|
-
|
|
2344
|
+
/** CSS class name used for the animated count fade transition */
|
|
2345
|
+
const ANIMATE_CLASS = 'seekora-stats-animate';
|
|
2346
|
+
/** Inline <style> for the count-change animation and CSS custom properties */
|
|
2347
|
+
const statsStyleId = 'seekora-stats-styles';
|
|
2348
|
+
function ensureStatsStyles() {
|
|
2349
|
+
if (typeof document === 'undefined')
|
|
2350
|
+
return;
|
|
2351
|
+
if (document.getElementById(statsStyleId))
|
|
2352
|
+
return;
|
|
2353
|
+
const style = document.createElement('style');
|
|
2354
|
+
style.id = statsStyleId;
|
|
2355
|
+
style.textContent = `
|
|
2356
|
+
.seekora-stats-root {
|
|
2357
|
+
--seekora-stats-color: inherit;
|
|
2358
|
+
--seekora-stats-bg: transparent;
|
|
2359
|
+
--seekora-stats-font-size: inherit;
|
|
2360
|
+
--seekora-stats-badge-bg: inherit;
|
|
2361
|
+
--seekora-stats-badge-color: inherit;
|
|
2362
|
+
}
|
|
2363
|
+
.${ANIMATE_CLASS} {
|
|
2364
|
+
opacity: 0;
|
|
2365
|
+
transition: opacity 250ms ease-in-out;
|
|
2366
|
+
}
|
|
2367
|
+
`;
|
|
2368
|
+
document.head.appendChild(style);
|
|
2369
|
+
}
|
|
2370
|
+
const Stats = ({ results: resultsProp, renderStats, className, style, theme: customTheme, showProcessingTime = false, showQuery = false, separator = ' \u2022 ', variant = 'inline', showResultCount = true, formatNumber, }) => {
|
|
2172
2371
|
const { theme } = useSearchContext();
|
|
2372
|
+
const { results: stateResults } = useSearchState();
|
|
2173
2373
|
const statsTheme = customTheme || {};
|
|
2374
|
+
// Use results from prop if provided, otherwise from state manager
|
|
2375
|
+
const results = resultsProp || stateResults;
|
|
2376
|
+
// Inject keyframe / transition styles once
|
|
2377
|
+
useEffect(() => {
|
|
2378
|
+
ensureStatsStyles();
|
|
2379
|
+
}, []);
|
|
2174
2380
|
// Extract stats from results
|
|
2175
2381
|
const res = results;
|
|
2176
2382
|
const totalResults = res?.totalResults
|
|
@@ -2181,44 +2387,173 @@ const Stats = ({ results, renderStats, className, style, theme: customTheme, sho
|
|
|
2181
2387
|
|| res?.data?.processingTimeMS
|
|
2182
2388
|
|| res?.data?.data?.processingTimeMS;
|
|
2183
2389
|
const query = (res?.query ?? '');
|
|
2184
|
-
|
|
2390
|
+
// Number formatter
|
|
2391
|
+
const fmt = formatNumber || ((n) => n.toLocaleString());
|
|
2392
|
+
// Animated count change — toggle a CSS class briefly on totalResults change
|
|
2393
|
+
const countRef = useRef(null);
|
|
2394
|
+
const prevTotalRef = useRef(totalResults);
|
|
2395
|
+
const [animating, setAnimating] = useState(false);
|
|
2396
|
+
useEffect(() => {
|
|
2397
|
+
if (prevTotalRef.current !== totalResults) {
|
|
2398
|
+
prevTotalRef.current = totalResults;
|
|
2399
|
+
setAnimating(true);
|
|
2400
|
+
// After a frame, remove the class so the transition plays (opacity 0 -> 1)
|
|
2401
|
+
const raf = requestAnimationFrame(() => {
|
|
2402
|
+
setAnimating(false);
|
|
2403
|
+
});
|
|
2404
|
+
return () => cancelAnimationFrame(raf);
|
|
2405
|
+
}
|
|
2406
|
+
}, [totalResults]);
|
|
2407
|
+
// Helper: border radius value from theme
|
|
2408
|
+
const borderRadiusSmall = typeof theme.borderRadius === 'string'
|
|
2409
|
+
? theme.borderRadius
|
|
2410
|
+
: theme.borderRadius.small;
|
|
2411
|
+
// ─── Custom render ────────────────────────────────────────
|
|
2412
|
+
if (renderStats) {
|
|
2413
|
+
return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: style }, renderStats({
|
|
2414
|
+
totalResults,
|
|
2415
|
+
processingTime,
|
|
2416
|
+
query,
|
|
2417
|
+
})));
|
|
2418
|
+
}
|
|
2419
|
+
// ─── Inline variant (original behaviour) ──────────────────
|
|
2420
|
+
if (variant === 'inline') {
|
|
2185
2421
|
const parts = [];
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2422
|
+
if (showResultCount) {
|
|
2423
|
+
if (totalResults > 0) {
|
|
2424
|
+
parts.push(React.createElement("span", { key: "count", className: statsTheme.text },
|
|
2425
|
+
React.createElement("span", { ref: countRef, className: clsx(animating && ANIMATE_CLASS), style: { transition: 'opacity 250ms ease-in-out' } }, fmt(totalResults)),
|
|
2426
|
+
` result${totalResults !== 1 ? 's' : ''}`));
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
parts.push(React.createElement("span", { key: "count", className: statsTheme.text }, "No results"));
|
|
2430
|
+
}
|
|
2191
2431
|
}
|
|
2192
2432
|
if (showProcessingTime && processingTime !== undefined) {
|
|
2193
|
-
parts.push(
|
|
2433
|
+
parts.push(React.createElement("span", { key: "time", className: statsTheme.text },
|
|
2434
|
+
fmt(processingTime),
|
|
2435
|
+
"ms"));
|
|
2194
2436
|
}
|
|
2195
2437
|
if (showQuery && query) {
|
|
2196
|
-
parts.push(
|
|
2438
|
+
parts.push(React.createElement("span", { key: "query", className: statsTheme.text },
|
|
2439
|
+
"for \"",
|
|
2440
|
+
query,
|
|
2441
|
+
"\""));
|
|
2197
2442
|
}
|
|
2198
|
-
return (React.createElement("div", { className: clsx(statsTheme.container, className), style: {
|
|
2199
|
-
fontSize: theme.typography.fontSize.medium,
|
|
2200
|
-
color: theme.colors.text,
|
|
2443
|
+
return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: {
|
|
2444
|
+
fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.medium + ')',
|
|
2445
|
+
color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
|
|
2446
|
+
backgroundColor: 'var(--seekora-stats-bg, transparent)',
|
|
2201
2447
|
...style,
|
|
2202
2448
|
} }, parts.map((part, index) => (React.createElement("span", { key: index },
|
|
2203
|
-
index > 0 && React.createElement("span", { className: statsTheme.separator, style: { margin: `0 ${theme.spacing.small}` } }, separator),
|
|
2204
|
-
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2449
|
+
index > 0 && (React.createElement("span", { className: statsTheme.separator, style: { margin: `0 ${theme.spacing.small}` } }, separator)),
|
|
2450
|
+
part)))));
|
|
2451
|
+
}
|
|
2452
|
+
// ─── Badge variant ────────────────────────────────────────
|
|
2453
|
+
if (variant === 'badge') {
|
|
2454
|
+
const badges = [];
|
|
2455
|
+
const badgeStyle = {
|
|
2456
|
+
display: 'inline-flex',
|
|
2457
|
+
alignItems: 'center',
|
|
2458
|
+
gap: theme.spacing.small,
|
|
2459
|
+
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
2460
|
+
borderRadius: borderRadiusSmall,
|
|
2461
|
+
backgroundColor: `var(--seekora-stats-badge-bg, ${theme.colors.primary}1A)`, // 10 % opacity hex
|
|
2462
|
+
color: `var(--seekora-stats-badge-color, ${theme.colors.primary})`,
|
|
2463
|
+
fontSize: theme.typography.fontSize.small,
|
|
2464
|
+
fontWeight: 500,
|
|
2465
|
+
};
|
|
2466
|
+
if (showResultCount) {
|
|
2467
|
+
badges.push(React.createElement("span", { key: "count", className: clsx(statsTheme.badge), style: badgeStyle },
|
|
2468
|
+
React.createElement("span", { className: statsTheme.badgeLabel }, "Results"),
|
|
2469
|
+
React.createElement("span", { className: clsx(statsTheme.badgeValue, animating && ANIMATE_CLASS), style: { fontWeight: 600, transition: 'opacity 250ms ease-in-out' } }, totalResults > 0 ? fmt(totalResults) : '0')));
|
|
2470
|
+
}
|
|
2471
|
+
if (showQuery && query) {
|
|
2472
|
+
badges.push(React.createElement("span", { key: "query", className: clsx(statsTheme.badge), style: badgeStyle },
|
|
2473
|
+
React.createElement("span", { className: statsTheme.badgeLabel }, "Query"),
|
|
2474
|
+
React.createElement("span", { className: statsTheme.badgeValue, style: { fontWeight: 600 } }, query)));
|
|
2475
|
+
}
|
|
2476
|
+
if (showProcessingTime && processingTime !== undefined) {
|
|
2477
|
+
badges.push(React.createElement("span", { key: "time", className: clsx(statsTheme.badge), style: badgeStyle },
|
|
2478
|
+
React.createElement("span", { className: statsTheme.badgeLabel }, "Time"),
|
|
2479
|
+
React.createElement("span", { className: statsTheme.badgeValue, style: { fontWeight: 600 } },
|
|
2480
|
+
fmt(processingTime),
|
|
2481
|
+
"ms")));
|
|
2482
|
+
}
|
|
2483
|
+
return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.container, className), style: {
|
|
2484
|
+
display: 'flex',
|
|
2485
|
+
flexWrap: 'wrap',
|
|
2486
|
+
gap: theme.spacing.small,
|
|
2487
|
+
fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.small + ')',
|
|
2488
|
+
color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
|
|
2489
|
+
backgroundColor: 'var(--seekora-stats-bg, transparent)',
|
|
2490
|
+
...style,
|
|
2491
|
+
} }, badges));
|
|
2492
|
+
}
|
|
2493
|
+
// ─── Detailed variant ─────────────────────────────────────
|
|
2494
|
+
if (variant === 'detailed') {
|
|
2495
|
+
const rows = [];
|
|
2496
|
+
if (showResultCount) {
|
|
2497
|
+
rows.push({
|
|
2498
|
+
label: 'Total Results',
|
|
2499
|
+
value: (React.createElement("span", { className: clsx(animating && ANIMATE_CLASS), style: { transition: 'opacity 250ms ease-in-out' } }, fmt(totalResults))),
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
if (showQuery && query) {
|
|
2503
|
+
rows.push({ label: 'Query', value: query });
|
|
2504
|
+
}
|
|
2505
|
+
if (showProcessingTime && processingTime !== undefined) {
|
|
2506
|
+
rows.push({ label: 'Time', value: `${fmt(processingTime)}ms` });
|
|
2507
|
+
}
|
|
2508
|
+
return (React.createElement("div", { className: clsx('seekora-stats-root', statsTheme.detailed, statsTheme.container, className), style: {
|
|
2509
|
+
display: 'flex',
|
|
2510
|
+
flexDirection: 'column',
|
|
2511
|
+
gap: theme.spacing.small,
|
|
2512
|
+
fontSize: 'var(--seekora-stats-font-size,' + theme.typography.fontSize.medium + ')',
|
|
2513
|
+
color: 'var(--seekora-stats-color,' + theme.colors.text + ')',
|
|
2514
|
+
backgroundColor: 'var(--seekora-stats-bg, transparent)',
|
|
2515
|
+
...style,
|
|
2516
|
+
} }, rows.map((row, index) => (React.createElement("div", { key: index, className: statsTheme.detailedRow, style: {
|
|
2517
|
+
display: 'flex',
|
|
2518
|
+
alignItems: 'center',
|
|
2519
|
+
gap: theme.spacing.small,
|
|
2520
|
+
} },
|
|
2521
|
+
React.createElement("span", { className: statsTheme.detailedLabel, style: {
|
|
2522
|
+
color: theme.colors.textSecondary,
|
|
2523
|
+
fontWeight: 500,
|
|
2524
|
+
} },
|
|
2525
|
+
row.label,
|
|
2526
|
+
":"),
|
|
2527
|
+
React.createElement("span", { className: statsTheme.detailedValue, style: {
|
|
2528
|
+
color: theme.colors.text,
|
|
2529
|
+
fontWeight: 600,
|
|
2530
|
+
} }, row.value))))));
|
|
2212
2531
|
}
|
|
2213
|
-
|
|
2532
|
+
// Fallback (should not be reached)
|
|
2533
|
+
return null;
|
|
2214
2534
|
};
|
|
2215
2535
|
|
|
2216
2536
|
/**
|
|
2217
2537
|
* Pagination Component
|
|
2218
2538
|
*
|
|
2219
|
-
* Displays pagination controls for search results
|
|
2539
|
+
* Displays pagination controls for search results.
|
|
2540
|
+
* Supports three display variants: numbered, load-more, and simple.
|
|
2541
|
+
*
|
|
2542
|
+
* CSS Variables (applied to the container element):
|
|
2543
|
+
* --seekora-pagination-bg
|
|
2544
|
+
* --seekora-pagination-color
|
|
2545
|
+
* --seekora-pagination-active-bg
|
|
2546
|
+
* --seekora-pagination-active-color
|
|
2547
|
+
* --seekora-pagination-border
|
|
2548
|
+
* --seekora-pagination-radius
|
|
2220
2549
|
*/
|
|
2221
|
-
|
|
2550
|
+
/** Size-specific style tokens */
|
|
2551
|
+
const SIZE_TOKENS = {
|
|
2552
|
+
small: { paddingKey: 'small', fontSizeKey: 'small', minWidth: '32px' },
|
|
2553
|
+
medium: { paddingKey: 'small', fontSizeKey: 'medium', minWidth: '40px' },
|
|
2554
|
+
large: { paddingKey: 'medium', fontSizeKey: 'large', minWidth: '48px' },
|
|
2555
|
+
};
|
|
2556
|
+
const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsPerPage: itemsPerPageProp, totalPages: totalPagesProp, onPageChange, maxPages = 7, showFirstLast = true, showPrevNext = true, renderPageButton, className, style, theme: customTheme, variant = 'numbered', loadMoreText = 'Load More', size = 'medium', showPageInfo, previousLabel = 'Previous', nextLabel = 'Next', }) => {
|
|
2222
2557
|
const { theme } = useSearchContext();
|
|
2223
2558
|
const { results: stateResults, currentPage: stateCurrentPage, setPage } = useSearchState();
|
|
2224
2559
|
const paginationTheme = customTheme || {};
|
|
@@ -2241,6 +2576,19 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2241
2576
|
|| res?.data?.total_pages
|
|
2242
2577
|
|| res?.data?.data?.total_pages
|
|
2243
2578
|
|| Math.ceil(totalResults / itemsPerPage);
|
|
2579
|
+
// Resolve whether to show page info text
|
|
2580
|
+
const resolvedShowPageInfo = showPageInfo !== undefined
|
|
2581
|
+
? showPageInfo
|
|
2582
|
+
: variant === 'simple';
|
|
2583
|
+
// Size tokens
|
|
2584
|
+
const sizeTokens = SIZE_TOKENS[size];
|
|
2585
|
+
// CSS variable aware helpers — allow overrides via custom properties
|
|
2586
|
+
const cssVarBg = 'var(--seekora-pagination-bg, ' + theme.colors.background + ')';
|
|
2587
|
+
const cssVarColor = 'var(--seekora-pagination-color, ' + theme.colors.text + ')';
|
|
2588
|
+
const cssVarActiveBg = 'var(--seekora-pagination-active-bg, ' + theme.colors.primary + ')';
|
|
2589
|
+
const cssVarActiveColor = 'var(--seekora-pagination-active-color, #fff)';
|
|
2590
|
+
const cssVarBorder = 'var(--seekora-pagination-border, ' + theme.colors.border + ')';
|
|
2591
|
+
const cssVarRadius = 'var(--seekora-pagination-radius, ' + (typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium) + ')';
|
|
2244
2592
|
const handlePageChange = (page) => {
|
|
2245
2593
|
if (page < 1 || page > totalPages || page === currentPage)
|
|
2246
2594
|
return;
|
|
@@ -2251,22 +2599,26 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2251
2599
|
onPageChange(page);
|
|
2252
2600
|
}
|
|
2253
2601
|
};
|
|
2254
|
-
const defaultRenderPageButton = (page, isActive, isDisabled) => (React.createElement("button", { type: "button", disabled: isDisabled, onClick: () => handlePageChange(page), className: clsx(paginationTheme.item, isActive && paginationTheme.itemActive, isDisabled), style: {
|
|
2255
|
-
padding: theme.spacing.
|
|
2602
|
+
const defaultRenderPageButton = (page, isActive, isDisabled) => (React.createElement("button", { type: "button", disabled: isDisabled, onClick: () => handlePageChange(page), "aria-current": isActive ? 'page' : undefined, "aria-label": `Page ${page}`, className: clsx(paginationTheme.item, isActive && paginationTheme.itemActive, isDisabled), style: {
|
|
2603
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2256
2604
|
margin: `0 ${theme.spacing.small}`,
|
|
2257
|
-
border: `1px solid ${
|
|
2258
|
-
borderRadius:
|
|
2259
|
-
backgroundColor: isActive ?
|
|
2260
|
-
color: isActive ?
|
|
2605
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2606
|
+
borderRadius: cssVarRadius,
|
|
2607
|
+
backgroundColor: isActive ? cssVarActiveBg : cssVarBg,
|
|
2608
|
+
color: isActive ? cssVarActiveColor : cssVarColor,
|
|
2261
2609
|
cursor: 'pointer',
|
|
2262
2610
|
opacity: 1,
|
|
2263
|
-
fontSize: theme.typography.fontSize.
|
|
2264
|
-
minWidth:
|
|
2611
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2612
|
+
minWidth: sizeTokens.minWidth,
|
|
2265
2613
|
...(isActive && {
|
|
2266
2614
|
fontWeight: 'bold',
|
|
2267
2615
|
}),
|
|
2268
2616
|
} }, page));
|
|
2269
|
-
if (totalPages <= 1) {
|
|
2617
|
+
if (totalPages <= 1 && variant !== 'load-more') {
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
// For load-more, hide when there are no more pages to load
|
|
2621
|
+
if (variant === 'load-more' && currentPage >= totalPages) {
|
|
2270
2622
|
return null;
|
|
2271
2623
|
}
|
|
2272
2624
|
// Calculate page range to display
|
|
@@ -2299,6 +2651,94 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2299
2651
|
}
|
|
2300
2652
|
return pages;
|
|
2301
2653
|
};
|
|
2654
|
+
// ── Page info element ──────────────────────────────────────────────
|
|
2655
|
+
const pageInfoElement = resolvedShowPageInfo ? (React.createElement("span", { className: clsx(paginationTheme.pageInfo), style: {
|
|
2656
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2657
|
+
color: cssVarColor,
|
|
2658
|
+
padding: `0 ${theme.spacing.small}`,
|
|
2659
|
+
whiteSpace: 'nowrap',
|
|
2660
|
+
}, "aria-live": "polite" },
|
|
2661
|
+
"Page ",
|
|
2662
|
+
currentPage,
|
|
2663
|
+
" of ",
|
|
2664
|
+
totalPages)) : null;
|
|
2665
|
+
// ── Load More variant ──────────────────────────────────────────────
|
|
2666
|
+
if (variant === 'load-more') {
|
|
2667
|
+
const remaining = Math.max(0, totalResults - currentPage * itemsPerPage);
|
|
2668
|
+
return (React.createElement("nav", { className: clsx(paginationTheme.container, className), style: {
|
|
2669
|
+
display: 'flex',
|
|
2670
|
+
flexDirection: 'column',
|
|
2671
|
+
alignItems: 'center',
|
|
2672
|
+
gap: theme.spacing.small,
|
|
2673
|
+
...style,
|
|
2674
|
+
}, "aria-label": "Pagination" },
|
|
2675
|
+
React.createElement("button", { type: "button", onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.loadMoreButton), style: {
|
|
2676
|
+
padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.large}`,
|
|
2677
|
+
border: 'none',
|
|
2678
|
+
borderRadius: cssVarRadius,
|
|
2679
|
+
backgroundColor: cssVarActiveBg,
|
|
2680
|
+
color: cssVarActiveColor,
|
|
2681
|
+
cursor: 'pointer',
|
|
2682
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2683
|
+
fontWeight: theme.typography.fontWeight?.medium ?? 500,
|
|
2684
|
+
transition: theme.transitions?.fast ?? '150ms ease-in-out',
|
|
2685
|
+
minWidth: sizeTokens.minWidth,
|
|
2686
|
+
}, "aria-label": remaining > 0 ? `${loadMoreText} (${remaining} remaining)` : loadMoreText },
|
|
2687
|
+
loadMoreText,
|
|
2688
|
+
remaining > 0 && (React.createElement("span", { className: clsx(paginationTheme.loadMoreText), style: {
|
|
2689
|
+
marginLeft: theme.spacing.small,
|
|
2690
|
+
opacity: 0.85,
|
|
2691
|
+
fontSize: theme.typography.fontSize.small,
|
|
2692
|
+
} },
|
|
2693
|
+
"(",
|
|
2694
|
+
remaining,
|
|
2695
|
+
" remaining)"))),
|
|
2696
|
+
pageInfoElement));
|
|
2697
|
+
}
|
|
2698
|
+
// ── Simple variant ─────────────────────────────────────────────────
|
|
2699
|
+
if (variant === 'simple') {
|
|
2700
|
+
return (React.createElement("nav", { className: clsx(paginationTheme.container, paginationTheme.simpleContainer, className), style: {
|
|
2701
|
+
display: 'flex',
|
|
2702
|
+
alignItems: 'center',
|
|
2703
|
+
justifyContent: 'center',
|
|
2704
|
+
gap: theme.spacing.medium,
|
|
2705
|
+
...style,
|
|
2706
|
+
}, "aria-label": "Pagination" },
|
|
2707
|
+
React.createElement("button", { type: "button", disabled: currentPage === 1, onClick: () => handlePageChange(currentPage - 1), className: clsx(paginationTheme.simpleButton, currentPage === 1 && paginationTheme.itemDisabled), style: {
|
|
2708
|
+
padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.medium}`,
|
|
2709
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2710
|
+
borderRadius: cssVarRadius,
|
|
2711
|
+
backgroundColor: cssVarBg,
|
|
2712
|
+
color: cssVarColor,
|
|
2713
|
+
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
|
|
2714
|
+
opacity: currentPage === 1 ? 0.5 : 1,
|
|
2715
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2716
|
+
minWidth: sizeTokens.minWidth,
|
|
2717
|
+
transition: theme.transitions?.fast ?? '150ms ease-in-out',
|
|
2718
|
+
}, "aria-label": "Previous page" }, previousLabel),
|
|
2719
|
+
React.createElement("span", { className: clsx(paginationTheme.simpleText, paginationTheme.pageInfo), style: {
|
|
2720
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2721
|
+
color: cssVarColor,
|
|
2722
|
+
whiteSpace: 'nowrap',
|
|
2723
|
+
}, "aria-live": "polite" },
|
|
2724
|
+
"Page ",
|
|
2725
|
+
currentPage,
|
|
2726
|
+
" of ",
|
|
2727
|
+
totalPages),
|
|
2728
|
+
React.createElement("button", { type: "button", disabled: currentPage === totalPages, onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.simpleButton, currentPage === totalPages && paginationTheme.itemDisabled), style: {
|
|
2729
|
+
padding: `${theme.spacing[sizeTokens.paddingKey]} ${theme.spacing.medium}`,
|
|
2730
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2731
|
+
borderRadius: cssVarRadius,
|
|
2732
|
+
backgroundColor: cssVarBg,
|
|
2733
|
+
color: cssVarColor,
|
|
2734
|
+
cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
|
|
2735
|
+
opacity: currentPage === totalPages ? 0.5 : 1,
|
|
2736
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2737
|
+
minWidth: sizeTokens.minWidth,
|
|
2738
|
+
transition: theme.transitions?.fast ?? '150ms ease-in-out',
|
|
2739
|
+
}, "aria-label": "Next page" }, nextLabel)));
|
|
2740
|
+
}
|
|
2741
|
+
// ── Numbered variant (default — original behavior) ─────────────────
|
|
2302
2742
|
const pageNumbers = getPageNumbers();
|
|
2303
2743
|
return (React.createElement("nav", { className: clsx(paginationTheme.container, className), style: style, "aria-label": "Pagination" },
|
|
2304
2744
|
React.createElement("ul", { className: paginationTheme.list, style: {
|
|
@@ -2309,27 +2749,44 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2309
2749
|
padding: 0,
|
|
2310
2750
|
margin: 0,
|
|
2311
2751
|
flexWrap: 'wrap',
|
|
2752
|
+
}, tabIndex: 0, onKeyDown: (e) => {
|
|
2753
|
+
if (e.key === 'ArrowLeft') {
|
|
2754
|
+
e.preventDefault();
|
|
2755
|
+
handlePageChange(currentPage - 1);
|
|
2756
|
+
}
|
|
2757
|
+
else if (e.key === 'ArrowRight') {
|
|
2758
|
+
e.preventDefault();
|
|
2759
|
+
handlePageChange(currentPage + 1);
|
|
2760
|
+
}
|
|
2761
|
+
else if (e.key === 'Home') {
|
|
2762
|
+
e.preventDefault();
|
|
2763
|
+
handlePageChange(1);
|
|
2764
|
+
}
|
|
2765
|
+
else if (e.key === 'End') {
|
|
2766
|
+
e.preventDefault();
|
|
2767
|
+
handlePageChange(totalPages);
|
|
2768
|
+
}
|
|
2312
2769
|
} },
|
|
2313
2770
|
showPrevNext && (React.createElement("li", null,
|
|
2314
2771
|
React.createElement("button", { type: "button", disabled: currentPage === 1, onClick: () => handlePageChange(currentPage - 1), className: clsx(paginationTheme.item, currentPage === 1 && paginationTheme.itemDisabled), style: {
|
|
2315
|
-
padding: theme.spacing.
|
|
2772
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2316
2773
|
margin: `0 ${theme.spacing.small}`,
|
|
2317
|
-
border: `1px solid ${
|
|
2318
|
-
borderRadius:
|
|
2319
|
-
backgroundColor:
|
|
2320
|
-
color:
|
|
2774
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2775
|
+
borderRadius: cssVarRadius,
|
|
2776
|
+
backgroundColor: cssVarBg,
|
|
2777
|
+
color: cssVarColor,
|
|
2321
2778
|
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
|
|
2322
2779
|
opacity: currentPage === 1 ? 0.5 : 1,
|
|
2323
|
-
fontSize: theme.typography.fontSize.
|
|
2324
|
-
}, "aria-label": "Previous page" },
|
|
2780
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2781
|
+
}, "aria-label": "Previous page" }, previousLabel))),
|
|
2325
2782
|
pageNumbers.map((page, index) => {
|
|
2326
2783
|
if (page === 'ellipsis') {
|
|
2327
2784
|
return (React.createElement("li", { key: `ellipsis-${index}` },
|
|
2328
2785
|
React.createElement("span", { className: paginationTheme.ellipsis, style: {
|
|
2329
|
-
padding: theme.spacing.
|
|
2786
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2330
2787
|
margin: `0 ${theme.spacing.small}`,
|
|
2331
|
-
color:
|
|
2332
|
-
fontSize: theme.typography.fontSize.
|
|
2788
|
+
color: cssVarColor,
|
|
2789
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2333
2790
|
} }, "...")));
|
|
2334
2791
|
}
|
|
2335
2792
|
const isActive = page === currentPage;
|
|
@@ -2340,89 +2797,274 @@ const Pagination = ({ results: resultsProp, currentPage: currentPageProp, itemsP
|
|
|
2340
2797
|
}),
|
|
2341
2798
|
showPrevNext && (React.createElement("li", null,
|
|
2342
2799
|
React.createElement("button", { type: "button", disabled: currentPage === totalPages, onClick: () => handlePageChange(currentPage + 1), className: clsx(paginationTheme.item, currentPage === totalPages && paginationTheme.itemDisabled), style: {
|
|
2343
|
-
padding: theme.spacing.
|
|
2800
|
+
padding: theme.spacing[sizeTokens.paddingKey],
|
|
2344
2801
|
margin: `0 ${theme.spacing.small}`,
|
|
2345
|
-
border: `1px solid ${
|
|
2346
|
-
borderRadius:
|
|
2347
|
-
backgroundColor:
|
|
2348
|
-
color:
|
|
2802
|
+
border: `1px solid ${cssVarBorder}`,
|
|
2803
|
+
borderRadius: cssVarRadius,
|
|
2804
|
+
backgroundColor: cssVarBg,
|
|
2805
|
+
color: cssVarColor,
|
|
2349
2806
|
cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
|
|
2350
2807
|
opacity: currentPage === totalPages ? 0.5 : 1,
|
|
2351
|
-
fontSize: theme.typography.fontSize.
|
|
2352
|
-
}, "aria-label": "Next page" },
|
|
2808
|
+
fontSize: theme.typography.fontSize[sizeTokens.fontSizeKey],
|
|
2809
|
+
}, "aria-label": "Next page" }, nextLabel))),
|
|
2810
|
+
resolvedShowPageInfo && (React.createElement("li", { style: { marginLeft: theme.spacing.small } }, pageInfoElement)))));
|
|
2353
2811
|
};
|
|
2354
2812
|
|
|
2355
2813
|
/**
|
|
2356
2814
|
* SortBy Component
|
|
2357
2815
|
*
|
|
2358
|
-
* Displays sort options for search results
|
|
2359
|
-
*
|
|
2816
|
+
* Displays sort options for search results in multiple display variants:
|
|
2817
|
+
* - dropdown (default) — native select element
|
|
2818
|
+
* - button-group — horizontal row of toggle buttons
|
|
2819
|
+
* - radio-group — vertical list of radio inputs
|
|
2820
|
+
*
|
|
2821
|
+
* Integrates with SearchStateManager for automatic state sync.
|
|
2822
|
+
*
|
|
2823
|
+
* CSS Variables (apply on a parent element to customize):
|
|
2824
|
+
* --seekora-sort-bg — background color
|
|
2825
|
+
* --seekora-sort-color — text color
|
|
2826
|
+
* --seekora-sort-border — border color
|
|
2827
|
+
* --seekora-sort-active-bg — active item background
|
|
2828
|
+
* --seekora-sort-active-color — active item text color
|
|
2360
2829
|
*/
|
|
2361
|
-
|
|
2830
|
+
// ---------------------------------------------------------------------------
|
|
2831
|
+
// Helpers
|
|
2832
|
+
// ---------------------------------------------------------------------------
|
|
2833
|
+
/** Map a size token to theme fontSize and spacing values. */
|
|
2834
|
+
function sizeStyles(size, theme) {
|
|
2835
|
+
switch (size) {
|
|
2836
|
+
case 'small':
|
|
2837
|
+
return { fontSize: theme.typography.fontSize.small, padding: theme.spacing.small };
|
|
2838
|
+
case 'large':
|
|
2839
|
+
return { fontSize: theme.typography.fontSize.large, padding: theme.spacing.medium };
|
|
2840
|
+
case 'medium':
|
|
2841
|
+
default:
|
|
2842
|
+
return { fontSize: theme.typography.fontSize.medium, padding: theme.spacing.small };
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function resolveBorderRadius(br) {
|
|
2846
|
+
return typeof br === 'string' ? br : br.medium;
|
|
2847
|
+
}
|
|
2848
|
+
// ---------------------------------------------------------------------------
|
|
2849
|
+
// Component
|
|
2850
|
+
// ---------------------------------------------------------------------------
|
|
2851
|
+
const SortBy = ({ options, value: valueProp, defaultValue, onSortChange, renderSelect, className, style, theme: customTheme, placeholder = 'Sort by...', syncWithState = true, variant = 'dropdown', label, showLabel, size = 'medium', }) => {
|
|
2362
2852
|
const { theme } = useSearchContext();
|
|
2363
2853
|
const { sortBy: stateManagerSortBy, setSortBy } = useSearchState();
|
|
2364
2854
|
const sortByTheme = customTheme || {};
|
|
2365
|
-
|
|
2855
|
+
const instanceId = useId();
|
|
2856
|
+
// Determine whether the label should render.
|
|
2857
|
+
const shouldShowLabel = showLabel !== undefined ? showLabel : !!label;
|
|
2858
|
+
// ------ State ----------------------------------------------------------
|
|
2366
2859
|
const [internalValue, setInternalValue] = React.useState(defaultValue || options[0]?.value || '');
|
|
2367
2860
|
// Sync with StateManager on mount if defaultValue is set
|
|
2368
2861
|
useEffect(() => {
|
|
2369
2862
|
if (syncWithState && defaultValue && !stateManagerSortBy) {
|
|
2370
2863
|
setSortBy(defaultValue, false); // Don't trigger search on initial sync
|
|
2371
2864
|
}
|
|
2372
|
-
}, []);
|
|
2865
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
2373
2866
|
// Determine the current value: controlled prop > StateManager > internal
|
|
2374
2867
|
const value = valueProp !== undefined
|
|
2375
2868
|
? valueProp
|
|
2376
|
-
:
|
|
2869
|
+
: syncWithState && stateManagerSortBy
|
|
2377
2870
|
? stateManagerSortBy
|
|
2378
2871
|
: internalValue;
|
|
2872
|
+
// ------ Handlers -------------------------------------------------------
|
|
2379
2873
|
const handleChange = (e) => {
|
|
2380
|
-
|
|
2874
|
+
applyValue(e.target.value);
|
|
2875
|
+
};
|
|
2876
|
+
const applyValue = useCallback((newValue) => {
|
|
2381
2877
|
setInternalValue(newValue);
|
|
2382
|
-
// Update StateManager (automatically triggers search)
|
|
2383
2878
|
if (syncWithState) {
|
|
2384
2879
|
setSortBy(newValue);
|
|
2385
2880
|
}
|
|
2386
|
-
// Call callback for backwards compatibility
|
|
2387
2881
|
if (onSortChange) {
|
|
2388
2882
|
onSortChange(newValue);
|
|
2389
2883
|
}
|
|
2884
|
+
}, [syncWithState, setSortBy, onSortChange]);
|
|
2885
|
+
// ------ Derived styles -------------------------------------------------
|
|
2886
|
+
const { fontSize, padding } = sizeStyles(size, theme);
|
|
2887
|
+
const borderRadius = resolveBorderRadius(theme.borderRadius);
|
|
2888
|
+
const cssVarStyle = {
|
|
2889
|
+
'--seekora-sort-bg': theme.colors.background,
|
|
2890
|
+
'--seekora-sort-color': theme.colors.text,
|
|
2891
|
+
'--seekora-sort-border': theme.colors.border,
|
|
2892
|
+
'--seekora-sort-active-bg': theme.colors.primary,
|
|
2893
|
+
'--seekora-sort-active-color': theme.colors.background,
|
|
2390
2894
|
};
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2895
|
+
// ------ Label ----------------------------------------------------------
|
|
2896
|
+
const labelElement = shouldShowLabel && label ? (React.createElement("span", { className: clsx(sortByTheme.label), style: {
|
|
2897
|
+
display: 'block',
|
|
2898
|
+
marginBottom: theme.spacing.small,
|
|
2899
|
+
fontSize,
|
|
2900
|
+
color: 'var(--seekora-sort-color)',
|
|
2901
|
+
fontWeight: 500,
|
|
2902
|
+
} }, label)) : null;
|
|
2903
|
+
// ------ Dropdown variant (original) ------------------------------------
|
|
2904
|
+
if (variant === 'dropdown') {
|
|
2905
|
+
if (renderSelect) {
|
|
2906
|
+
return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
|
|
2907
|
+
labelElement,
|
|
2908
|
+
renderSelect({
|
|
2909
|
+
value,
|
|
2910
|
+
onChange: handleChange,
|
|
2911
|
+
options,
|
|
2912
|
+
})));
|
|
2913
|
+
}
|
|
2914
|
+
return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
|
|
2915
|
+
labelElement,
|
|
2916
|
+
React.createElement("select", { value: value, onChange: handleChange, className: clsx(sortByTheme.select), style: {
|
|
2917
|
+
padding,
|
|
2918
|
+
paddingRight: theme.spacing.medium,
|
|
2919
|
+
fontSize,
|
|
2920
|
+
border: '1px solid var(--seekora-sort-border)',
|
|
2921
|
+
borderRadius,
|
|
2922
|
+
backgroundColor: 'var(--seekora-sort-bg)',
|
|
2923
|
+
color: 'var(--seekora-sort-color)',
|
|
2924
|
+
cursor: 'pointer',
|
|
2925
|
+
outline: 'none',
|
|
2926
|
+
width: '100%',
|
|
2927
|
+
}, "aria-label": label || 'Sort results' }, options.map((option) => (React.createElement("option", { key: option.value, value: option.value, className: sortByTheme.option }, option.label))))));
|
|
2928
|
+
}
|
|
2929
|
+
// ------ Button group variant -------------------------------------------
|
|
2930
|
+
if (variant === 'button-group') {
|
|
2931
|
+
return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
|
|
2932
|
+
labelElement,
|
|
2933
|
+
React.createElement("div", { role: "group", "aria-label": label || 'Sort results', className: clsx(sortByTheme.buttonGroup), style: {
|
|
2934
|
+
display: 'inline-flex',
|
|
2935
|
+
borderRadius,
|
|
2936
|
+
overflow: 'hidden',
|
|
2937
|
+
border: '1px solid var(--seekora-sort-border)',
|
|
2938
|
+
} }, options.map((option, index) => {
|
|
2939
|
+
const isActive = option.value === value;
|
|
2940
|
+
return (React.createElement("button", { key: option.value, type: "button", role: "button", "aria-pressed": isActive, onClick: () => applyValue(option.value), className: clsx(sortByTheme.buttonGroupItem, isActive && sortByTheme.buttonGroupItemActive), style: {
|
|
2941
|
+
padding,
|
|
2942
|
+
fontSize,
|
|
2943
|
+
border: 'none',
|
|
2944
|
+
borderRight: index < options.length - 1
|
|
2945
|
+
? '1px solid var(--seekora-sort-border)'
|
|
2946
|
+
: 'none',
|
|
2947
|
+
backgroundColor: isActive
|
|
2948
|
+
? 'var(--seekora-sort-active-bg)'
|
|
2949
|
+
: 'var(--seekora-sort-bg)',
|
|
2950
|
+
color: isActive
|
|
2951
|
+
? 'var(--seekora-sort-active-color)'
|
|
2952
|
+
: 'var(--seekora-sort-color)',
|
|
2953
|
+
cursor: 'pointer',
|
|
2954
|
+
fontWeight: isActive ? 600 : 400,
|
|
2955
|
+
transition: 'background-color 0.15s ease, color 0.15s ease',
|
|
2956
|
+
outline: 'none',
|
|
2957
|
+
} }, option.label));
|
|
2958
|
+
}))));
|
|
2959
|
+
}
|
|
2960
|
+
// ------ Radio group variant --------------------------------------------
|
|
2961
|
+
if (variant === 'radio-group') {
|
|
2962
|
+
const radioName = `seekora-sort-${instanceId}`;
|
|
2963
|
+
return (React.createElement("div", { className: clsx(sortByTheme.container, className), style: { ...cssVarStyle, ...style } },
|
|
2964
|
+
labelElement,
|
|
2965
|
+
React.createElement("div", { role: "radiogroup", "aria-label": label || 'Sort results', className: clsx(sortByTheme.radioGroup), style: {
|
|
2966
|
+
display: 'flex',
|
|
2967
|
+
flexDirection: 'column',
|
|
2968
|
+
gap: theme.spacing.small,
|
|
2969
|
+
} }, options.map((option) => {
|
|
2970
|
+
const isActive = option.value === value;
|
|
2971
|
+
const radioId = `${radioName}-${option.value}`;
|
|
2972
|
+
return (React.createElement("label", { key: option.value, htmlFor: radioId, className: clsx(sortByTheme.radioItem, isActive && sortByTheme.radioItemActive), style: {
|
|
2973
|
+
display: 'flex',
|
|
2974
|
+
alignItems: 'center',
|
|
2975
|
+
gap: theme.spacing.small,
|
|
2976
|
+
padding,
|
|
2977
|
+
borderRadius,
|
|
2978
|
+
cursor: 'pointer',
|
|
2979
|
+
backgroundColor: isActive
|
|
2980
|
+
? 'var(--seekora-sort-active-bg)'
|
|
2981
|
+
: 'transparent',
|
|
2982
|
+
color: isActive
|
|
2983
|
+
? 'var(--seekora-sort-active-color)'
|
|
2984
|
+
: 'var(--seekora-sort-color)',
|
|
2985
|
+
fontWeight: isActive ? 600 : 400,
|
|
2986
|
+
transition: 'background-color 0.15s ease, color 0.15s ease',
|
|
2987
|
+
} },
|
|
2988
|
+
React.createElement("input", { type: "radio", id: radioId, name: radioName, value: option.value, checked: isActive, onChange: () => applyValue(option.value), style: { margin: 0 } }),
|
|
2989
|
+
React.createElement("span", { className: clsx(sortByTheme.radioLabel), style: { fontSize } }, option.label)));
|
|
2990
|
+
}))));
|
|
2409
2991
|
}
|
|
2410
|
-
|
|
2992
|
+
// Fallback — should never reach here, but satisfies TS exhaustiveness
|
|
2993
|
+
return null;
|
|
2411
2994
|
};
|
|
2412
2995
|
|
|
2413
2996
|
/**
|
|
2414
2997
|
* Facets Component
|
|
2415
2998
|
*
|
|
2416
|
-
* Displays facet filters for search results
|
|
2999
|
+
* Displays facet filters for search results with multiple display variants,
|
|
3000
|
+
* client-side search, count badges, and color swatch support.
|
|
2417
3001
|
*/
|
|
2418
|
-
|
|
3002
|
+
// ---------------------------------------------------------------------------
|
|
3003
|
+
// Helpers
|
|
3004
|
+
// ---------------------------------------------------------------------------
|
|
3005
|
+
/** Generate a deterministic colour from a string (used as fallback for color-swatch). */
|
|
3006
|
+
function stringToColor(str) {
|
|
3007
|
+
let hash = 0;
|
|
3008
|
+
for (let i = 0; i < str.length; i++) {
|
|
3009
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
3010
|
+
hash |= 0; // Convert to 32-bit int
|
|
3011
|
+
}
|
|
3012
|
+
const h = Math.abs(hash) % 360;
|
|
3013
|
+
return `hsl(${h}, 65%, 55%)`;
|
|
3014
|
+
}
|
|
3015
|
+
/** Return swatch pixel size from the size prop. */
|
|
3016
|
+
function swatchSize(size) {
|
|
3017
|
+
switch (size) {
|
|
3018
|
+
case 'small':
|
|
3019
|
+
return 24;
|
|
3020
|
+
case 'large':
|
|
3021
|
+
return 40;
|
|
3022
|
+
case 'medium':
|
|
3023
|
+
default:
|
|
3024
|
+
return 32;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
// ---------------------------------------------------------------------------
|
|
3028
|
+
// Chevron SVG icon for collapsible variant
|
|
3029
|
+
// ---------------------------------------------------------------------------
|
|
3030
|
+
const ChevronIcon$1 = ({ expanded, color = 'currentColor', size = 16, }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
|
|
3031
|
+
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
3032
|
+
transition: 'transform 200ms ease',
|
|
3033
|
+
flexShrink: 0,
|
|
3034
|
+
}, "aria-hidden": "true" },
|
|
3035
|
+
React.createElement("path", { d: "M4 6L8 10L12 6", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
3036
|
+
// ---------------------------------------------------------------------------
|
|
3037
|
+
// Checkmark SVG for selected color swatches
|
|
3038
|
+
// ---------------------------------------------------------------------------
|
|
3039
|
+
const CheckmarkIcon = ({ size = 16 }) => (React.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
|
|
3040
|
+
React.createElement("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
3041
|
+
// ---------------------------------------------------------------------------
|
|
3042
|
+
// CSS variable defaults
|
|
3043
|
+
// ---------------------------------------------------------------------------
|
|
3044
|
+
const CSS_VAR_DEFAULTS = {
|
|
3045
|
+
'--seekora-facet-bg': '#ffffff',
|
|
3046
|
+
'--seekora-facet-border': '#dee2e6',
|
|
3047
|
+
'--seekora-facet-active-bg': '#f0f7ff',
|
|
3048
|
+
'--seekora-facet-swatch-size': '32px',
|
|
3049
|
+
'--seekora-facet-count-bg': '#e9ecef',
|
|
3050
|
+
'--seekora-facet-count-color': '#495057',
|
|
3051
|
+
};
|
|
3052
|
+
// ---------------------------------------------------------------------------
|
|
3053
|
+
// Component
|
|
3054
|
+
// ---------------------------------------------------------------------------
|
|
3055
|
+
const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, variant = 'checkbox', searchable = false, showCounts = true, colorMap, defaultCollapsed = false, size = 'medium', }) => {
|
|
2419
3056
|
const { theme } = useSearchContext();
|
|
2420
3057
|
const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
|
|
2421
3058
|
const facetsTheme = customTheme || {};
|
|
3059
|
+
// expandedFacets is used for "Show more/less" in checkbox/color-swatch variants
|
|
3060
|
+
// AND for collapse/expand in collapsible variant.
|
|
2422
3061
|
const [expandedFacets, setExpandedFacets] = useState({});
|
|
3062
|
+
const [searchTerms, setSearchTerms] = useState({});
|
|
2423
3063
|
// Use results from prop if provided, otherwise from state manager
|
|
2424
3064
|
const results = resultsProp || stateResults;
|
|
3065
|
+
// -------------------------------------------------------------------
|
|
2425
3066
|
// Extract facets from results
|
|
3067
|
+
// -------------------------------------------------------------------
|
|
2426
3068
|
const extractFacets = () => {
|
|
2427
3069
|
if (facetsProp)
|
|
2428
3070
|
return facetsProp;
|
|
@@ -2469,6 +3111,9 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2469
3111
|
return extracted;
|
|
2470
3112
|
};
|
|
2471
3113
|
const facets = extractFacets();
|
|
3114
|
+
// -------------------------------------------------------------------
|
|
3115
|
+
// Handlers
|
|
3116
|
+
// -------------------------------------------------------------------
|
|
2472
3117
|
const handleFacetToggle = (field, value, selected) => {
|
|
2473
3118
|
const newSelected = !selected;
|
|
2474
3119
|
log.verbose('Facets: Facet toggled', { field, value, selected: newSelected });
|
|
@@ -2501,49 +3146,283 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2501
3146
|
[field]: !prev[field],
|
|
2502
3147
|
}));
|
|
2503
3148
|
};
|
|
3149
|
+
/** For collapsible variant — determine if a facet group is open. */
|
|
3150
|
+
const isFacetGroupOpen = (field) => {
|
|
3151
|
+
if (field in expandedFacets) {
|
|
3152
|
+
return expandedFacets[field];
|
|
3153
|
+
}
|
|
3154
|
+
// Default based on defaultCollapsed prop
|
|
3155
|
+
return !defaultCollapsed;
|
|
3156
|
+
};
|
|
3157
|
+
const toggleCollapsible = (field) => {
|
|
3158
|
+
setExpandedFacets((prev) => ({
|
|
3159
|
+
...prev,
|
|
3160
|
+
[field]: !(prev[field] ?? !defaultCollapsed),
|
|
3161
|
+
}));
|
|
3162
|
+
};
|
|
3163
|
+
const getSearchTerm = (field) => searchTerms[field] || '';
|
|
3164
|
+
const setSearchTerm = (field, term) => {
|
|
3165
|
+
setSearchTerms((prev) => ({ ...prev, [field]: term }));
|
|
3166
|
+
};
|
|
3167
|
+
/** Filter facet items by search term. */
|
|
3168
|
+
const filterItems = (items, field) => {
|
|
3169
|
+
if (!searchable)
|
|
3170
|
+
return items;
|
|
3171
|
+
const term = getSearchTerm(field).toLowerCase();
|
|
3172
|
+
if (!term)
|
|
3173
|
+
return items;
|
|
3174
|
+
return items.filter((item) => item.value.toLowerCase().includes(term));
|
|
3175
|
+
};
|
|
3176
|
+
// -------------------------------------------------------------------
|
|
3177
|
+
// Size helpers
|
|
3178
|
+
// -------------------------------------------------------------------
|
|
3179
|
+
const sizeScale = useMemo(() => {
|
|
3180
|
+
switch (size) {
|
|
3181
|
+
case 'small':
|
|
3182
|
+
return { font: theme.typography.fontSize.small, padding: '0.25rem', gap: '0.25rem' };
|
|
3183
|
+
case 'large':
|
|
3184
|
+
return { font: theme.typography.fontSize.large, padding: '0.75rem', gap: '0.75rem' };
|
|
3185
|
+
case 'medium':
|
|
3186
|
+
default:
|
|
3187
|
+
return { font: theme.typography.fontSize.medium, padding: theme.spacing.small, gap: theme.spacing.small };
|
|
3188
|
+
}
|
|
3189
|
+
}, [size, theme]);
|
|
3190
|
+
// -------------------------------------------------------------------
|
|
3191
|
+
// Count badge renderer
|
|
3192
|
+
// -------------------------------------------------------------------
|
|
3193
|
+
const renderCountBadge = (count) => {
|
|
3194
|
+
if (!showCounts)
|
|
3195
|
+
return null;
|
|
3196
|
+
return (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
3197
|
+
display: 'inline-flex',
|
|
3198
|
+
alignItems: 'center',
|
|
3199
|
+
justifyContent: 'center',
|
|
3200
|
+
minWidth: '1.5em',
|
|
3201
|
+
padding: '0.1em 0.5em',
|
|
3202
|
+
marginLeft: sizeScale.gap,
|
|
3203
|
+
fontSize: theme.typography.fontSize.small,
|
|
3204
|
+
fontWeight: theme.typography.fontWeight?.medium ?? 500,
|
|
3205
|
+
lineHeight: 1,
|
|
3206
|
+
color: 'var(--seekora-facet-count-color, #495057)',
|
|
3207
|
+
backgroundColor: 'var(--seekora-facet-count-bg, #e9ecef)',
|
|
3208
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.full,
|
|
3209
|
+
flexShrink: 0,
|
|
3210
|
+
} }, count));
|
|
3211
|
+
};
|
|
3212
|
+
// -------------------------------------------------------------------
|
|
3213
|
+
// Search input renderer
|
|
3214
|
+
// -------------------------------------------------------------------
|
|
3215
|
+
const renderSearchInput = (facet) => {
|
|
3216
|
+
if (!searchable)
|
|
3217
|
+
return null;
|
|
3218
|
+
return (React.createElement("input", { type: "text", value: getSearchTerm(facet.field), onChange: (e) => setSearchTerm(facet.field, e.target.value), placeholder: `Search ${facet.label || facet.field}...`, className: clsx(facetsTheme.searchInput), "aria-label": `Search within ${facet.label || facet.field}`, style: {
|
|
3219
|
+
width: '100%',
|
|
3220
|
+
boxSizing: 'border-box',
|
|
3221
|
+
padding: sizeScale.padding,
|
|
3222
|
+
marginBottom: sizeScale.gap,
|
|
3223
|
+
fontSize: theme.typography.fontSize.small,
|
|
3224
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
3225
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.small,
|
|
3226
|
+
outline: 'none',
|
|
3227
|
+
color: theme.colors.text,
|
|
3228
|
+
backgroundColor: 'var(--seekora-facet-bg, transparent)',
|
|
3229
|
+
} }));
|
|
3230
|
+
};
|
|
3231
|
+
// -------------------------------------------------------------------
|
|
3232
|
+
// Checkbox variant item renderer (original behaviour, preserved)
|
|
3233
|
+
// -------------------------------------------------------------------
|
|
2504
3234
|
const defaultRenderFacetItem = (item, facet, index) => {
|
|
2505
3235
|
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
2506
3236
|
if (!isExpanded && index >= maxItems) {
|
|
2507
3237
|
return null;
|
|
2508
3238
|
}
|
|
2509
|
-
|
|
3239
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
3240
|
+
return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, isChecked && facetsTheme.facetItemActive), role: "option", "aria-selected": isChecked, "aria-checked": isChecked, tabIndex: -1, onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), style: {
|
|
2510
3241
|
display: 'flex',
|
|
2511
3242
|
alignItems: 'center',
|
|
2512
|
-
padding:
|
|
3243
|
+
padding: sizeScale.padding,
|
|
3244
|
+
cursor: 'pointer',
|
|
3245
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
3246
|
+
marginBottom: sizeScale.gap,
|
|
3247
|
+
backgroundColor: isChecked
|
|
3248
|
+
? 'var(--seekora-facet-active-bg, ' + theme.colors.hover + ')'
|
|
3249
|
+
: 'transparent',
|
|
3250
|
+
transition: 'background-color 0.2s ease',
|
|
3251
|
+
} },
|
|
3252
|
+
React.createElement("input", { type: "checkbox", checked: refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false, onChange: () => handleFacetToggle(facet.field, item.value, item.selected || false), className: facetsTheme.checkbox, style: {
|
|
3253
|
+
marginRight: sizeScale.gap,
|
|
3254
|
+
cursor: 'pointer',
|
|
3255
|
+
}, "aria-label": `Filter by ${item.value}` }),
|
|
3256
|
+
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
3257
|
+
flex: 1,
|
|
3258
|
+
fontSize: sizeScale.font,
|
|
3259
|
+
color: theme.colors.text,
|
|
3260
|
+
} }, item.value),
|
|
3261
|
+
renderCountBadge(item.count)));
|
|
3262
|
+
};
|
|
3263
|
+
// -------------------------------------------------------------------
|
|
3264
|
+
// Color swatch variant item renderer
|
|
3265
|
+
// -------------------------------------------------------------------
|
|
3266
|
+
const renderColorSwatchItem = (item, facet, index) => {
|
|
3267
|
+
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
3268
|
+
if (!isExpanded && index >= maxItems) {
|
|
3269
|
+
return null;
|
|
3270
|
+
}
|
|
3271
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
3272
|
+
const color = colorMap?.[item.value] ?? stringToColor(item.value);
|
|
3273
|
+
const pxSize = swatchSize(size);
|
|
3274
|
+
return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, isChecked && facetsTheme.facetItemActive), role: "option", "aria-selected": isChecked, "aria-checked": isChecked, tabIndex: -1, onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), title: `${item.value}${showCounts ? ` (${item.count})` : ''}`, style: {
|
|
3275
|
+
display: 'inline-flex',
|
|
3276
|
+
flexDirection: 'column',
|
|
3277
|
+
alignItems: 'center',
|
|
2513
3278
|
cursor: 'pointer',
|
|
3279
|
+
margin: sizeScale.gap,
|
|
3280
|
+
} },
|
|
3281
|
+
React.createElement("div", { className: clsx(facetsTheme.colorSwatch, isChecked && facetsTheme.colorSwatchSelected), style: {
|
|
3282
|
+
width: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
3283
|
+
height: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
3284
|
+
borderRadius: '50%',
|
|
3285
|
+
backgroundColor: color,
|
|
3286
|
+
border: isChecked
|
|
3287
|
+
? `3px solid ${theme.colors.primary}`
|
|
3288
|
+
: `2px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
3289
|
+
display: 'flex',
|
|
3290
|
+
alignItems: 'center',
|
|
3291
|
+
justifyContent: 'center',
|
|
3292
|
+
transition: 'border 0.2s ease, box-shadow 0.2s ease',
|
|
3293
|
+
boxShadow: isChecked ? `0 0 0 2px ${theme.colors.primary}33` : 'none',
|
|
3294
|
+
position: 'relative',
|
|
3295
|
+
} }, isChecked && (React.createElement("span", { className: clsx(facetsTheme.colorSwatchInner), style: {
|
|
3296
|
+
display: 'flex',
|
|
3297
|
+
alignItems: 'center',
|
|
3298
|
+
justifyContent: 'center',
|
|
3299
|
+
} },
|
|
3300
|
+
React.createElement(CheckmarkIcon, { size: Math.round(pxSize * 0.5) })))),
|
|
3301
|
+
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
3302
|
+
fontSize: theme.typography.fontSize.small,
|
|
3303
|
+
color: theme.colors.text,
|
|
3304
|
+
marginTop: '0.25rem',
|
|
3305
|
+
textAlign: 'center',
|
|
3306
|
+
maxWidth: `${pxSize + 16}px`,
|
|
3307
|
+
overflow: 'hidden',
|
|
3308
|
+
textOverflow: 'ellipsis',
|
|
3309
|
+
whiteSpace: 'nowrap',
|
|
3310
|
+
} }, item.value),
|
|
3311
|
+
showCounts && (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
3312
|
+
fontSize: theme.typography.fontSize.small,
|
|
3313
|
+
color: 'var(--seekora-facet-count-color, ' + (theme.colors.textSecondary || theme.colors.text) + ')',
|
|
3314
|
+
lineHeight: 1,
|
|
3315
|
+
marginTop: '0.125rem',
|
|
3316
|
+
} }, item.count))));
|
|
3317
|
+
};
|
|
3318
|
+
// -------------------------------------------------------------------
|
|
3319
|
+
// Item renderer dispatcher
|
|
3320
|
+
// -------------------------------------------------------------------
|
|
3321
|
+
const renderItem = (item, facet, index) => {
|
|
3322
|
+
if (renderFacetItem) {
|
|
3323
|
+
return renderFacetItem(item, facet, index);
|
|
3324
|
+
}
|
|
3325
|
+
switch (variant) {
|
|
3326
|
+
case 'color-swatch':
|
|
3327
|
+
return renderColorSwatchItem(item, facet, index);
|
|
3328
|
+
case 'collapsible':
|
|
3329
|
+
case 'checkbox':
|
|
3330
|
+
default:
|
|
3331
|
+
return defaultRenderFacetItem(item, facet, index);
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
// -------------------------------------------------------------------
|
|
3335
|
+
// Keyboard handler (shared across variants)
|
|
3336
|
+
// -------------------------------------------------------------------
|
|
3337
|
+
const handleListKeyDown = (e, visibleItems, facet) => {
|
|
3338
|
+
const currentEl = e.currentTarget.querySelector('[aria-selected="true"]');
|
|
3339
|
+
const allItems = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
3340
|
+
const currentIdx = currentEl ? allItems.indexOf(currentEl) : -1;
|
|
3341
|
+
if (e.key === 'ArrowDown') {
|
|
3342
|
+
e.preventDefault();
|
|
3343
|
+
const next = Math.min(currentIdx + 1, allItems.length - 1);
|
|
3344
|
+
allItems[next]?.focus();
|
|
3345
|
+
}
|
|
3346
|
+
else if (e.key === 'ArrowUp') {
|
|
3347
|
+
e.preventDefault();
|
|
3348
|
+
const prev = Math.max(currentIdx - 1, 0);
|
|
3349
|
+
allItems[prev]?.focus();
|
|
3350
|
+
}
|
|
3351
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
3352
|
+
e.preventDefault();
|
|
3353
|
+
if (currentIdx >= 0 && currentIdx < visibleItems.length) {
|
|
3354
|
+
handleFacetToggle(facet.field, visibleItems[currentIdx].value, visibleItems[currentIdx].selected || false);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
// -------------------------------------------------------------------
|
|
3359
|
+
// Show more / less buttons (shared)
|
|
3360
|
+
// -------------------------------------------------------------------
|
|
3361
|
+
const renderShowMoreLess = (facet, filteredItems) => {
|
|
3362
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
3363
|
+
const hasMore = filteredItems.length > maxItems;
|
|
3364
|
+
return (React.createElement(React.Fragment, null,
|
|
3365
|
+
showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
3366
|
+
marginTop: sizeScale.gap,
|
|
3367
|
+
padding: sizeScale.padding,
|
|
3368
|
+
border: 'none',
|
|
3369
|
+
backgroundColor: 'transparent',
|
|
3370
|
+
color: theme.colors.primary,
|
|
3371
|
+
cursor: 'pointer',
|
|
3372
|
+
fontSize: theme.typography.fontSize.small,
|
|
3373
|
+
textDecoration: 'underline',
|
|
3374
|
+
} },
|
|
3375
|
+
"Show more (",
|
|
3376
|
+
filteredItems.length - maxItems,
|
|
3377
|
+
" more)")),
|
|
3378
|
+
isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
3379
|
+
marginTop: sizeScale.gap,
|
|
3380
|
+
padding: sizeScale.padding,
|
|
3381
|
+
border: 'none',
|
|
3382
|
+
backgroundColor: 'transparent',
|
|
3383
|
+
color: theme.colors.primary,
|
|
3384
|
+
cursor: 'pointer',
|
|
3385
|
+
fontSize: theme.typography.fontSize.small,
|
|
3386
|
+
textDecoration: 'underline',
|
|
3387
|
+
} }, "Show less"))));
|
|
3388
|
+
};
|
|
3389
|
+
// -------------------------------------------------------------------
|
|
3390
|
+
// Default facet group renderer — Checkbox variant
|
|
3391
|
+
// -------------------------------------------------------------------
|
|
3392
|
+
const renderCheckboxFacet = (facet, _index) => {
|
|
3393
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
3394
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
3395
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
3396
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
3397
|
+
marginBottom: theme.spacing.large,
|
|
3398
|
+
padding: theme.spacing.medium,
|
|
3399
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
3400
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
2514
3401
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
2515
|
-
marginBottom: theme.spacing.small,
|
|
2516
|
-
backgroundColor: (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) ? theme.colors.hover : 'transparent',
|
|
2517
|
-
transition: 'background-color 0.2s ease',
|
|
2518
3402
|
} },
|
|
2519
|
-
React.createElement("
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
flex: 1,
|
|
2525
|
-
fontSize: theme.typography.fontSize.medium,
|
|
3403
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
3404
|
+
fontSize: theme.typography.fontSize.large,
|
|
3405
|
+
fontWeight: 'bold',
|
|
3406
|
+
margin: 0,
|
|
3407
|
+
marginBottom: theme.spacing.medium,
|
|
2526
3408
|
color: theme.colors.text,
|
|
2527
|
-
} },
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
opacity: 0.7,
|
|
2532
|
-
marginLeft: theme.spacing.small,
|
|
2533
|
-
} },
|
|
2534
|
-
"(",
|
|
2535
|
-
item.count,
|
|
2536
|
-
")")));
|
|
3409
|
+
} }, facet.label || facet.field),
|
|
3410
|
+
renderSearchInput(facet),
|
|
3411
|
+
React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet) }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
|
|
3412
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
2537
3413
|
};
|
|
2538
|
-
|
|
3414
|
+
// -------------------------------------------------------------------
|
|
3415
|
+
// Color-swatch facet group renderer
|
|
3416
|
+
// -------------------------------------------------------------------
|
|
3417
|
+
const renderColorSwatchFacet = (facet, _index) => {
|
|
3418
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
2539
3419
|
const isExpanded = expandedFacets[facet.field] || false;
|
|
2540
|
-
const visibleItems = isExpanded ?
|
|
2541
|
-
const hasMore = facet.items.length > maxItems;
|
|
3420
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
2542
3421
|
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
2543
3422
|
marginBottom: theme.spacing.large,
|
|
2544
3423
|
padding: theme.spacing.medium,
|
|
2545
|
-
backgroundColor: theme.colors.background,
|
|
2546
|
-
border: `1px solid ${theme.colors.border}`,
|
|
3424
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
3425
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
2547
3426
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
2548
3427
|
} },
|
|
2549
3428
|
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
@@ -2553,36 +3432,106 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2553
3432
|
marginBottom: theme.spacing.medium,
|
|
2554
3433
|
color: theme.colors.text,
|
|
2555
3434
|
} }, facet.label || facet.field),
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
:
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
3435
|
+
renderSearchInput(facet),
|
|
3436
|
+
React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet), style: {
|
|
3437
|
+
display: 'flex',
|
|
3438
|
+
flexWrap: 'wrap',
|
|
3439
|
+
gap: sizeScale.gap,
|
|
3440
|
+
} }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
|
|
3441
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
3442
|
+
};
|
|
3443
|
+
// -------------------------------------------------------------------
|
|
3444
|
+
// Collapsible facet group renderer
|
|
3445
|
+
// -------------------------------------------------------------------
|
|
3446
|
+
const renderCollapsibleFacet = (facet, _index) => {
|
|
3447
|
+
const isOpen = isFacetGroupOpen(facet.field);
|
|
3448
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
3449
|
+
expandedFacets[facet.field] || false;
|
|
3450
|
+
// Note: For collapsible, the expandedFacets state controls the collapse/expand
|
|
3451
|
+
// of the group itself. We use a separate concept for Show more/less within items.
|
|
3452
|
+
// To avoid collision, Show more/less for collapsible uses the same expandedFacets
|
|
3453
|
+
// key prefixed with `_items_`.
|
|
3454
|
+
const isShowMoreExpanded = expandedFacets[`_items_${facet.field}`] || false;
|
|
3455
|
+
const visibleItems = isShowMoreExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
3456
|
+
const hasMore = filteredItems.length > maxItems;
|
|
3457
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
3458
|
+
marginBottom: theme.spacing.large,
|
|
3459
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
3460
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
3461
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
3462
|
+
overflow: 'hidden',
|
|
3463
|
+
} },
|
|
3464
|
+
React.createElement("button", { type: "button", className: clsx(facetsTheme.collapsibleHeader), onClick: () => toggleCollapsible(facet.field), "aria-expanded": isOpen, "aria-controls": `facet-group-${facet.field}`, style: {
|
|
3465
|
+
display: 'flex',
|
|
3466
|
+
alignItems: 'center',
|
|
3467
|
+
justifyContent: 'space-between',
|
|
3468
|
+
width: '100%',
|
|
3469
|
+
padding: theme.spacing.medium,
|
|
2565
3470
|
border: 'none',
|
|
2566
3471
|
backgroundColor: 'transparent',
|
|
2567
|
-
color: theme.colors.primary,
|
|
2568
3472
|
cursor: 'pointer',
|
|
2569
|
-
|
|
2570
|
-
textDecoration: 'underline',
|
|
3473
|
+
textAlign: 'left',
|
|
2571
3474
|
} },
|
|
2572
|
-
"
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3475
|
+
React.createElement("span", { className: facetsTheme.facetTitle, style: {
|
|
3476
|
+
fontSize: theme.typography.fontSize.large,
|
|
3477
|
+
fontWeight: 'bold',
|
|
3478
|
+
color: theme.colors.text,
|
|
3479
|
+
flex: 1,
|
|
3480
|
+
} }, facet.label || facet.field),
|
|
3481
|
+
React.createElement("span", { className: clsx(facetsTheme.collapsibleIcon) },
|
|
3482
|
+
React.createElement(ChevronIcon$1, { expanded: isOpen, color: theme.colors.textSecondary || theme.colors.text }))),
|
|
3483
|
+
isOpen && (React.createElement("div", { id: `facet-group-${facet.field}`, style: {
|
|
3484
|
+
padding: `0 ${theme.spacing.medium} ${theme.spacing.medium}`,
|
|
3485
|
+
} },
|
|
3486
|
+
renderSearchInput(facet),
|
|
3487
|
+
React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet) }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
|
|
3488
|
+
showMore && hasMore && !isShowMoreExpanded && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
3489
|
+
...prev,
|
|
3490
|
+
[`_items_${facet.field}`]: true,
|
|
3491
|
+
})), style: {
|
|
3492
|
+
marginTop: sizeScale.gap,
|
|
3493
|
+
padding: sizeScale.padding,
|
|
3494
|
+
border: 'none',
|
|
3495
|
+
backgroundColor: 'transparent',
|
|
3496
|
+
color: theme.colors.primary,
|
|
3497
|
+
cursor: 'pointer',
|
|
3498
|
+
fontSize: theme.typography.fontSize.small,
|
|
3499
|
+
textDecoration: 'underline',
|
|
3500
|
+
} },
|
|
3501
|
+
"Show more (",
|
|
3502
|
+
filteredItems.length - maxItems,
|
|
3503
|
+
" more)")),
|
|
3504
|
+
isShowMoreExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
3505
|
+
...prev,
|
|
3506
|
+
[`_items_${facet.field}`]: false,
|
|
3507
|
+
})), style: {
|
|
3508
|
+
marginTop: sizeScale.gap,
|
|
3509
|
+
padding: sizeScale.padding,
|
|
3510
|
+
border: 'none',
|
|
3511
|
+
backgroundColor: 'transparent',
|
|
3512
|
+
color: theme.colors.primary,
|
|
3513
|
+
cursor: 'pointer',
|
|
3514
|
+
fontSize: theme.typography.fontSize.small,
|
|
3515
|
+
textDecoration: 'underline',
|
|
3516
|
+
} }, "Show less"))))));
|
|
2585
3517
|
};
|
|
3518
|
+
// -------------------------------------------------------------------
|
|
3519
|
+
// Default facet renderer dispatcher
|
|
3520
|
+
// -------------------------------------------------------------------
|
|
3521
|
+
const defaultRenderFacet = (facet, index) => {
|
|
3522
|
+
switch (variant) {
|
|
3523
|
+
case 'color-swatch':
|
|
3524
|
+
return renderColorSwatchFacet(facet);
|
|
3525
|
+
case 'collapsible':
|
|
3526
|
+
return renderCollapsibleFacet(facet);
|
|
3527
|
+
case 'checkbox':
|
|
3528
|
+
default:
|
|
3529
|
+
return renderCheckboxFacet(facet);
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
// -------------------------------------------------------------------
|
|
3533
|
+
// Empty state
|
|
3534
|
+
// -------------------------------------------------------------------
|
|
2586
3535
|
if (facets.length === 0) {
|
|
2587
3536
|
log.verbose('Facets: No facets to display', {
|
|
2588
3537
|
hasResults: !!results,
|
|
@@ -2590,7 +3539,13 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2590
3539
|
});
|
|
2591
3540
|
return null;
|
|
2592
3541
|
}
|
|
2593
|
-
|
|
3542
|
+
// -------------------------------------------------------------------
|
|
3543
|
+
// Render
|
|
3544
|
+
// -------------------------------------------------------------------
|
|
3545
|
+
return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: {
|
|
3546
|
+
...CSS_VAR_DEFAULTS,
|
|
3547
|
+
...style,
|
|
3548
|
+
} }, facets.map((facet, index) => {
|
|
2594
3549
|
return renderFacet
|
|
2595
3550
|
? renderFacet(facet, index)
|
|
2596
3551
|
: defaultRenderFacet(facet);
|
|
@@ -2600,72 +3555,222 @@ const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, rende
|
|
|
2600
3555
|
/**
|
|
2601
3556
|
* CurrentRefinements Component
|
|
2602
3557
|
*
|
|
2603
|
-
* Displays currently active filters/refinements with ability to clear them
|
|
3558
|
+
* Displays currently active filters/refinements with ability to clear them.
|
|
3559
|
+
* Supports StateManager auto-sync, display variants, layout modes, and animations.
|
|
2604
3560
|
*/
|
|
2605
|
-
|
|
3561
|
+
/** Get variant-specific styles */
|
|
3562
|
+
const getVariantStyles = (variant, themeColors, themeSpacing, themeBorderRadius, fieldColor) => {
|
|
3563
|
+
const baseBg = fieldColor || `var(--seekora-refinement-bg, ${themeColors.hover})`;
|
|
3564
|
+
const baseColor = `var(--seekora-refinement-color, ${themeColors.text})`;
|
|
3565
|
+
const baseBorder = `var(--seekora-refinement-border, ${themeColors.border})`;
|
|
3566
|
+
switch (variant) {
|
|
3567
|
+
case 'tags':
|
|
3568
|
+
return {
|
|
3569
|
+
display: 'inline-flex',
|
|
3570
|
+
alignItems: 'center',
|
|
3571
|
+
padding: `2px ${themeSpacing.medium}`,
|
|
3572
|
+
backgroundColor: baseBg,
|
|
3573
|
+
border: `1px solid ${baseBorder}`,
|
|
3574
|
+
borderRadius: `var(--seekora-refinement-radius, 4px)`,
|
|
3575
|
+
fontSize: '12px',
|
|
3576
|
+
color: baseColor,
|
|
3577
|
+
fontWeight: 500,
|
|
3578
|
+
};
|
|
3579
|
+
case 'pills':
|
|
3580
|
+
return {
|
|
3581
|
+
display: 'inline-flex',
|
|
3582
|
+
alignItems: 'center',
|
|
3583
|
+
padding: `${themeSpacing.small} ${themeSpacing.medium}`,
|
|
3584
|
+
backgroundColor: baseBg,
|
|
3585
|
+
border: 'none',
|
|
3586
|
+
borderRadius: `var(--seekora-refinement-radius, 9999px)`,
|
|
3587
|
+
fontSize: '13px',
|
|
3588
|
+
color: baseColor,
|
|
3589
|
+
};
|
|
3590
|
+
case 'badges':
|
|
3591
|
+
return {
|
|
3592
|
+
display: 'inline-flex',
|
|
3593
|
+
alignItems: 'center',
|
|
3594
|
+
padding: `3px ${themeSpacing.small}`,
|
|
3595
|
+
backgroundColor: themeColors.primary,
|
|
3596
|
+
border: 'none',
|
|
3597
|
+
borderRadius: `var(--seekora-refinement-radius, 4px)`,
|
|
3598
|
+
fontSize: '11px',
|
|
3599
|
+
color: '#fff',
|
|
3600
|
+
fontWeight: 600,
|
|
3601
|
+
textTransform: 'uppercase',
|
|
3602
|
+
letterSpacing: '0.5px',
|
|
3603
|
+
};
|
|
3604
|
+
case 'text-list':
|
|
3605
|
+
return {
|
|
3606
|
+
display: 'flex',
|
|
3607
|
+
alignItems: 'center',
|
|
3608
|
+
padding: `${themeSpacing.small} 0`,
|
|
3609
|
+
backgroundColor: 'transparent',
|
|
3610
|
+
border: 'none',
|
|
3611
|
+
borderBottom: `1px solid ${baseBorder}`,
|
|
3612
|
+
borderRadius: '0',
|
|
3613
|
+
fontSize: '14px',
|
|
3614
|
+
color: baseColor,
|
|
3615
|
+
};
|
|
3616
|
+
case 'chips':
|
|
3617
|
+
default:
|
|
3618
|
+
return {
|
|
3619
|
+
display: 'inline-flex',
|
|
3620
|
+
alignItems: 'center',
|
|
3621
|
+
padding: `${themeSpacing.small} ${themeSpacing.medium}`,
|
|
3622
|
+
backgroundColor: baseBg,
|
|
3623
|
+
border: `1px solid ${baseBorder}`,
|
|
3624
|
+
borderRadius: `var(--seekora-refinement-radius, ${typeof themeBorderRadius === 'string' ? themeBorderRadius : themeBorderRadius.medium})`,
|
|
3625
|
+
fontSize: '13px',
|
|
3626
|
+
color: baseColor,
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
3630
|
+
/** Get variant-specific class name from theme */
|
|
3631
|
+
const getVariantClass = (variant, refinementsTheme) => {
|
|
3632
|
+
switch (variant) {
|
|
3633
|
+
case 'tags': return refinementsTheme.tag;
|
|
3634
|
+
case 'pills': return refinementsTheme.pill;
|
|
3635
|
+
case 'badges': return refinementsTheme.badge;
|
|
3636
|
+
case 'chips': return refinementsTheme.chip;
|
|
3637
|
+
default: return refinementsTheme.item;
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, onClearAll, renderRefinement, showClearAll = true, variant = 'chips', layout = 'horizontal', fieldColors, renderCloseIcon, className, style, theme: customTheme, }) => {
|
|
2606
3641
|
const { theme } = useSearchContext();
|
|
3642
|
+
const { refinements: stateRefinements, removeRefinement, clearRefinements } = useSearchState();
|
|
2607
3643
|
const refinementsTheme = customTheme || {};
|
|
3644
|
+
// Use props if provided, otherwise auto-read from StateManager
|
|
3645
|
+
const refinements = refinementsProp !== undefined
|
|
3646
|
+
? refinementsProp
|
|
3647
|
+
: stateRefinements.map(r => ({ field: r.field, value: r.value }));
|
|
3648
|
+
// Track items for entry/exit animations
|
|
3649
|
+
const [visibleItems, setVisibleItems] = useState(new Set());
|
|
3650
|
+
const [exitingItems, setExitingItems] = useState(new Set());
|
|
3651
|
+
const prevRefinementsRef = useRef([]);
|
|
3652
|
+
useEffect(() => {
|
|
3653
|
+
const currentKeys = new Set(refinements.map(r => `${r.field}:${r.value}`));
|
|
3654
|
+
const prevKeys = new Set(prevRefinementsRef.current.map(r => `${r.field}:${r.value}`));
|
|
3655
|
+
// Detect removed items for exit animation
|
|
3656
|
+
const removed = new Set();
|
|
3657
|
+
prevKeys.forEach(key => {
|
|
3658
|
+
if (!currentKeys.has(key))
|
|
3659
|
+
removed.add(key);
|
|
3660
|
+
});
|
|
3661
|
+
if (removed.size > 0) {
|
|
3662
|
+
setExitingItems(removed);
|
|
3663
|
+
// Remove after animation completes
|
|
3664
|
+
setTimeout(() => setExitingItems(new Set()), 200);
|
|
3665
|
+
}
|
|
3666
|
+
// Mark new items for entry animation
|
|
3667
|
+
setVisibleItems(currentKeys);
|
|
3668
|
+
prevRefinementsRef.current = [...refinements];
|
|
3669
|
+
}, [refinements]);
|
|
2608
3670
|
const handleClear = (field, value) => {
|
|
3671
|
+
// If synced with StateManager and no prop provided, auto-clear via StateManager
|
|
3672
|
+
if (refinementsProp === undefined) {
|
|
3673
|
+
removeRefinement(field, value);
|
|
3674
|
+
}
|
|
2609
3675
|
if (onRefinementClear) {
|
|
2610
3676
|
onRefinementClear(field, value);
|
|
2611
3677
|
}
|
|
2612
3678
|
};
|
|
2613
3679
|
const handleClearAll = () => {
|
|
3680
|
+
// If synced with StateManager, auto-clear all via StateManager
|
|
3681
|
+
if (refinementsProp === undefined) {
|
|
3682
|
+
clearRefinements();
|
|
3683
|
+
}
|
|
2614
3684
|
if (onClearAll) {
|
|
2615
3685
|
onClearAll();
|
|
2616
3686
|
}
|
|
2617
3687
|
};
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3688
|
+
const defaultCloseIcon = () => (React.createElement("span", { "aria-hidden": "true", style: { lineHeight: 1 } }, "\u00D7"));
|
|
3689
|
+
const defaultRenderRefinement = (refinement, index) => {
|
|
3690
|
+
const key = `${refinement.field}:${refinement.value}`;
|
|
3691
|
+
const fieldColor = fieldColors?.[refinement.field];
|
|
3692
|
+
const isEntering = visibleItems.has(key) && !prevRefinementsRef.current.some(r => `${r.field}:${r.value}` === key);
|
|
3693
|
+
const variantStyles = getVariantStyles(variant, theme.colors, theme.spacing, theme.borderRadius, fieldColor);
|
|
3694
|
+
const variantClass = getVariantClass(variant, refinementsTheme);
|
|
3695
|
+
return (React.createElement("div", { key: `${key}-${index}`, className: clsx(refinementsTheme.item, variantClass), role: "listitem", style: {
|
|
3696
|
+
...variantStyles,
|
|
3697
|
+
margin: layout === 'vertical'
|
|
3698
|
+
? `0 0 ${theme.spacing.small} 0`
|
|
3699
|
+
: `0 ${theme.spacing.small} ${theme.spacing.small} 0`,
|
|
3700
|
+
transition: 'all 200ms ease-in-out',
|
|
3701
|
+
opacity: exitingItems.has(key) ? 0 : 1,
|
|
3702
|
+
transform: exitingItems.has(key) ? 'scale(0.8)' : 'scale(1)',
|
|
3703
|
+
animation: isEntering ? 'seekoraChipIn 200ms ease-out' : undefined,
|
|
2632
3704
|
} },
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
3705
|
+
variant !== 'badges' && (React.createElement("span", { className: refinementsTheme.label, style: {
|
|
3706
|
+
marginRight: theme.spacing.small,
|
|
3707
|
+
fontWeight: '500',
|
|
3708
|
+
opacity: 0.7,
|
|
3709
|
+
} },
|
|
3710
|
+
refinement.label || refinement.field,
|
|
3711
|
+
":")),
|
|
3712
|
+
React.createElement("span", { className: refinementsTheme.value }, refinement.displayValue || refinement.value),
|
|
3713
|
+
React.createElement("button", { type: "button", onClick: () => handleClear(refinement.field, refinement.value), className: refinementsTheme.clearButton, style: {
|
|
3714
|
+
border: 'none',
|
|
3715
|
+
backgroundColor: 'transparent',
|
|
3716
|
+
color: 'inherit',
|
|
3717
|
+
cursor: 'pointer',
|
|
3718
|
+
fontSize: '14px',
|
|
3719
|
+
padding: '0 0 0 6px',
|
|
3720
|
+
display: 'flex',
|
|
3721
|
+
alignItems: 'center',
|
|
3722
|
+
justifyContent: 'center',
|
|
3723
|
+
borderRadius: '50%',
|
|
3724
|
+
transition: 'opacity 150ms ease-in-out',
|
|
3725
|
+
opacity: 0.6,
|
|
3726
|
+
}, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}`, onMouseEnter: e => (e.currentTarget.style.opacity = '1'), onMouseLeave: e => (e.currentTarget.style.opacity = '0.6') }, renderCloseIcon ? renderCloseIcon() : defaultCloseIcon())));
|
|
3727
|
+
};
|
|
2655
3728
|
if (refinements.length === 0) {
|
|
2656
3729
|
return null;
|
|
2657
3730
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3731
|
+
// Group refinements by field for grouped layout
|
|
3732
|
+
const groupedRefinements = layout === 'grouped'
|
|
3733
|
+
? refinements.reduce((acc, r) => {
|
|
3734
|
+
if (!acc[r.field])
|
|
3735
|
+
acc[r.field] = [];
|
|
3736
|
+
acc[r.field].push(r);
|
|
3737
|
+
return acc;
|
|
3738
|
+
}, {})
|
|
3739
|
+
: null;
|
|
3740
|
+
const containerStyles = {
|
|
3741
|
+
...style,
|
|
3742
|
+
};
|
|
3743
|
+
const listStyles = {
|
|
3744
|
+
display: 'flex',
|
|
3745
|
+
flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
|
|
3746
|
+
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
3747
|
+
alignItems: layout === 'vertical' ? 'flex-start' : 'center',
|
|
3748
|
+
marginBottom: showClearAll ? theme.spacing.medium : 0,
|
|
3749
|
+
};
|
|
3750
|
+
return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
|
|
3751
|
+
React.createElement("style", null, `
|
|
3752
|
+
@keyframes seekoraChipIn {
|
|
3753
|
+
from { opacity: 0; transform: scale(0.8); }
|
|
3754
|
+
to { opacity: 1; transform: scale(1); }
|
|
3755
|
+
}
|
|
3756
|
+
`),
|
|
3757
|
+
layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
|
|
3758
|
+
React.createElement("div", { className: refinementsTheme.groupLabel, style: {
|
|
3759
|
+
fontSize: theme.typography.fontSize.small,
|
|
3760
|
+
fontWeight: 600,
|
|
3761
|
+
color: theme.colors.text,
|
|
3762
|
+
marginBottom: theme.spacing.small,
|
|
3763
|
+
textTransform: 'capitalize',
|
|
3764
|
+
} }, items[0]?.label || field),
|
|
3765
|
+
React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
|
|
3766
|
+
return renderRefinement
|
|
3767
|
+
? renderRefinement(refinement, index)
|
|
3768
|
+
: defaultRenderRefinement(refinement, index);
|
|
3769
|
+
})))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
|
|
2665
3770
|
return renderRefinement
|
|
2666
3771
|
? renderRefinement(refinement, index)
|
|
2667
3772
|
: defaultRenderRefinement(refinement, index);
|
|
2668
|
-
})),
|
|
3773
|
+
}))),
|
|
2669
3774
|
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
2670
3775
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
2671
3776
|
border: `1px solid ${theme.colors.border}`,
|
|
@@ -2675,6 +3780,7 @@ const CurrentRefinements = ({ refinements = [], onRefinementClear, onClearAll, r
|
|
|
2675
3780
|
cursor: 'pointer',
|
|
2676
3781
|
fontSize: theme.typography.fontSize.small,
|
|
2677
3782
|
textDecoration: 'underline',
|
|
3783
|
+
transition: 'background-color 150ms ease-in-out',
|
|
2678
3784
|
} }, "Clear all filters"))));
|
|
2679
3785
|
};
|
|
2680
3786
|
|
|
@@ -3523,6 +4629,110 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3523
4629
|
const toggleShowMore = (level) => {
|
|
3524
4630
|
setExpanded(prev => ({ ...prev, [level]: !prev[level] }));
|
|
3525
4631
|
};
|
|
4632
|
+
const containerRef = useRef(null);
|
|
4633
|
+
// Collect all visible treeitem elements within the container
|
|
4634
|
+
const getVisibleTreeItems = useCallback(() => {
|
|
4635
|
+
if (!containerRef.current)
|
|
4636
|
+
return [];
|
|
4637
|
+
return Array.from(containerRef.current.querySelectorAll('[role="treeitem"]'));
|
|
4638
|
+
}, []);
|
|
4639
|
+
// Keyboard navigation handler for the tree
|
|
4640
|
+
const handleKeyDown = useCallback((e) => {
|
|
4641
|
+
const items = getVisibleTreeItems();
|
|
4642
|
+
if (items.length === 0)
|
|
4643
|
+
return;
|
|
4644
|
+
const activeElement = document.activeElement;
|
|
4645
|
+
// Find the treeitem that is or contains the active element
|
|
4646
|
+
const currentItem = items.find(item => item === activeElement || item.contains(activeElement));
|
|
4647
|
+
const currentIndex = currentItem ? items.indexOf(currentItem) : -1;
|
|
4648
|
+
switch (e.key) {
|
|
4649
|
+
case 'ArrowDown': {
|
|
4650
|
+
e.preventDefault();
|
|
4651
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
4652
|
+
const button = items[nextIndex].querySelector('button');
|
|
4653
|
+
if (button)
|
|
4654
|
+
button.focus();
|
|
4655
|
+
else
|
|
4656
|
+
items[nextIndex].focus();
|
|
4657
|
+
break;
|
|
4658
|
+
}
|
|
4659
|
+
case 'ArrowUp': {
|
|
4660
|
+
e.preventDefault();
|
|
4661
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
4662
|
+
const button = items[prevIndex].querySelector('button');
|
|
4663
|
+
if (button)
|
|
4664
|
+
button.focus();
|
|
4665
|
+
else
|
|
4666
|
+
items[prevIndex].focus();
|
|
4667
|
+
break;
|
|
4668
|
+
}
|
|
4669
|
+
case 'ArrowRight': {
|
|
4670
|
+
if (!currentItem)
|
|
4671
|
+
break;
|
|
4672
|
+
const isExpanded = currentItem.getAttribute('aria-expanded');
|
|
4673
|
+
if (isExpanded === 'false') {
|
|
4674
|
+
// Expand (select) this item
|
|
4675
|
+
e.preventDefault();
|
|
4676
|
+
const button = currentItem.querySelector('button');
|
|
4677
|
+
if (button)
|
|
4678
|
+
button.click();
|
|
4679
|
+
}
|
|
4680
|
+
else if (isExpanded === 'true') {
|
|
4681
|
+
// Move to first child
|
|
4682
|
+
e.preventDefault();
|
|
4683
|
+
const childList = currentItem.querySelector('[role="group"]');
|
|
4684
|
+
if (childList) {
|
|
4685
|
+
const firstChild = childList.querySelector('[role="treeitem"]');
|
|
4686
|
+
if (firstChild) {
|
|
4687
|
+
const button = firstChild.querySelector('button');
|
|
4688
|
+
if (button)
|
|
4689
|
+
button.focus();
|
|
4690
|
+
else
|
|
4691
|
+
firstChild.focus();
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
break;
|
|
4696
|
+
}
|
|
4697
|
+
case 'ArrowLeft': {
|
|
4698
|
+
if (!currentItem)
|
|
4699
|
+
break;
|
|
4700
|
+
const isExpanded = currentItem.getAttribute('aria-expanded');
|
|
4701
|
+
if (isExpanded === 'true') {
|
|
4702
|
+
// Collapse this item
|
|
4703
|
+
e.preventDefault();
|
|
4704
|
+
const button = currentItem.querySelector('button');
|
|
4705
|
+
if (button)
|
|
4706
|
+
button.click();
|
|
4707
|
+
}
|
|
4708
|
+
else {
|
|
4709
|
+
// Move focus to parent treeitem
|
|
4710
|
+
e.preventDefault();
|
|
4711
|
+
const parentGroup = currentItem.closest('[role="group"]');
|
|
4712
|
+
if (parentGroup) {
|
|
4713
|
+
const parentItem = parentGroup.closest('[role="treeitem"]');
|
|
4714
|
+
if (parentItem) {
|
|
4715
|
+
const button = parentItem.querySelector(':scope > button');
|
|
4716
|
+
if (button)
|
|
4717
|
+
button.focus();
|
|
4718
|
+
else
|
|
4719
|
+
parentItem.focus();
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
break;
|
|
4724
|
+
}
|
|
4725
|
+
case 'Enter': {
|
|
4726
|
+
if (!currentItem)
|
|
4727
|
+
break;
|
|
4728
|
+
e.preventDefault();
|
|
4729
|
+
const button = currentItem.querySelector('button');
|
|
4730
|
+
if (button)
|
|
4731
|
+
button.click();
|
|
4732
|
+
break;
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
}, [getVisibleTreeItems]);
|
|
3526
4736
|
// Render a level of the hierarchy
|
|
3527
4737
|
const renderLevel = (items, level) => {
|
|
3528
4738
|
if (!items || items.length === 0)
|
|
@@ -3531,12 +4741,14 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3531
4741
|
const displayLimit = isExpanded ? showMoreLimit : limit;
|
|
3532
4742
|
const displayItems = items.slice(0, displayLimit);
|
|
3533
4743
|
const hasMore = items.length > displayLimit;
|
|
3534
|
-
return (React.createElement("ul", { className: hierarchicalTheme.list, style: {
|
|
4744
|
+
return (React.createElement("ul", { role: level === 0 ? 'tree' : 'group', className: hierarchicalTheme.list, style: {
|
|
3535
4745
|
listStyle: 'none',
|
|
3536
4746
|
margin: 0,
|
|
3537
4747
|
padding: level > 0 ? `0 0 0 ${theme.spacing.medium}` : 0,
|
|
3538
4748
|
} },
|
|
3539
|
-
displayItems.map((item, index) => (React.createElement("li", { key: item.value,
|
|
4749
|
+
displayItems.map((item, index) => (React.createElement("li", { key: item.value, role: "treeitem", ...(item.data && item.data.length > 0
|
|
4750
|
+
? { 'aria-expanded': !!item.isRefined }
|
|
4751
|
+
: {}), className: clsx(hierarchicalTheme.item, item.isRefined && hierarchicalTheme.itemSelected, item.data && item.data.length > 0 && hierarchicalTheme.itemParent), style: {
|
|
3540
4752
|
padding: `${theme.spacing.small} 0`,
|
|
3541
4753
|
} }, renderItem ? (renderItem(item, level)) : (React.createElement(React.Fragment, null,
|
|
3542
4754
|
React.createElement("button", { type: "button", onClick: () => handleItemClick(item, level), className: hierarchicalTheme.link, style: {
|
|
@@ -3574,7 +4786,7 @@ const HierarchicalMenu = ({ attributes, separator = ' > ', limit = 10, showMore
|
|
|
3574
4786
|
if (processedItems.length === 0) {
|
|
3575
4787
|
return null;
|
|
3576
4788
|
}
|
|
3577
|
-
return (React.createElement("div", { className: clsx(hierarchicalTheme.root, className), style: style }, renderLevel(processedItems, 0)));
|
|
4789
|
+
return (React.createElement("div", { ref: containerRef, className: clsx(hierarchicalTheme.root, className), style: style, tabIndex: 0, onKeyDown: handleKeyDown }, renderLevel(processedItems, 0)));
|
|
3578
4790
|
};
|
|
3579
4791
|
|
|
3580
4792
|
/**
|
|
@@ -3674,6 +4886,53 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3674
4886
|
const handleDragEnd = () => {
|
|
3675
4887
|
setIsDragging(false);
|
|
3676
4888
|
};
|
|
4889
|
+
// Handle keyboard navigation for enhanced control (Shift+Arrow for 10x step, Home/End)
|
|
4890
|
+
const handleMinKeyDown = (e) => {
|
|
4891
|
+
let newValue = null;
|
|
4892
|
+
if (e.key === 'Home') {
|
|
4893
|
+
e.preventDefault();
|
|
4894
|
+
newValue = min;
|
|
4895
|
+
}
|
|
4896
|
+
else if (e.key === 'End') {
|
|
4897
|
+
e.preventDefault();
|
|
4898
|
+
newValue = internalMax - step;
|
|
4899
|
+
}
|
|
4900
|
+
else if (e.shiftKey && (e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
|
|
4901
|
+
e.preventDefault();
|
|
4902
|
+
newValue = Math.max(min, internalMin - step * 10);
|
|
4903
|
+
}
|
|
4904
|
+
else if (e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowUp')) {
|
|
4905
|
+
e.preventDefault();
|
|
4906
|
+
newValue = Math.min(internalMax - step, internalMin + step * 10);
|
|
4907
|
+
}
|
|
4908
|
+
if (newValue !== null) {
|
|
4909
|
+
setInternalMin(newValue);
|
|
4910
|
+
debouncedUpdate(newValue, internalMax);
|
|
4911
|
+
}
|
|
4912
|
+
};
|
|
4913
|
+
const handleMaxKeyDown = (e) => {
|
|
4914
|
+
let newValue = null;
|
|
4915
|
+
if (e.key === 'Home') {
|
|
4916
|
+
e.preventDefault();
|
|
4917
|
+
newValue = internalMin + step;
|
|
4918
|
+
}
|
|
4919
|
+
else if (e.key === 'End') {
|
|
4920
|
+
e.preventDefault();
|
|
4921
|
+
newValue = max;
|
|
4922
|
+
}
|
|
4923
|
+
else if (e.shiftKey && (e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
|
|
4924
|
+
e.preventDefault();
|
|
4925
|
+
newValue = Math.max(internalMin + step, internalMax - step * 10);
|
|
4926
|
+
}
|
|
4927
|
+
else if (e.shiftKey && (e.key === 'ArrowRight' || e.key === 'ArrowUp')) {
|
|
4928
|
+
e.preventDefault();
|
|
4929
|
+
newValue = Math.min(max, internalMax + step * 10);
|
|
4930
|
+
}
|
|
4931
|
+
if (newValue !== null) {
|
|
4932
|
+
setInternalMax(newValue);
|
|
4933
|
+
debouncedUpdate(internalMin, newValue);
|
|
4934
|
+
}
|
|
4935
|
+
};
|
|
3677
4936
|
// Calculate filled track position
|
|
3678
4937
|
const minPercent = ((internalMin - min) / (max - min)) * 100;
|
|
3679
4938
|
const maxPercent = ((internalMax - min) / (max - min)) * 100;
|
|
@@ -3709,7 +4968,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3709
4968
|
backgroundColor: theme.colors.primary,
|
|
3710
4969
|
borderRadius: '2px',
|
|
3711
4970
|
} }),
|
|
3712
|
-
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMin, onChange: handleMinChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
|
|
4971
|
+
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMin, onChange: handleMinChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, onKeyDown: handleMinKeyDown, tabIndex: 0, "aria-valuenow": internalMin, "aria-valuemin": min, "aria-valuemax": max, className: rangeSliderTheme.thumb, style: {
|
|
3713
4972
|
position: 'absolute',
|
|
3714
4973
|
width: '100%',
|
|
3715
4974
|
height: '4px',
|
|
@@ -3719,7 +4978,7 @@ const RangeSlider = ({ field, label, min, max, step = 1, currentMin: currentMinP
|
|
|
3719
4978
|
cursor: 'pointer',
|
|
3720
4979
|
pointerEvents: 'none',
|
|
3721
4980
|
}, "aria-label": `Minimum ${label || field}` }),
|
|
3722
|
-
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMax, onChange: handleMaxChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, className: rangeSliderTheme.thumb, style: {
|
|
4981
|
+
React.createElement("input", { type: "range", min: min, max: max, step: step, value: internalMax, onChange: handleMaxChange, onMouseUp: handleDragEnd, onTouchEnd: handleDragEnd, onKeyDown: handleMaxKeyDown, tabIndex: 0, "aria-valuenow": internalMax, "aria-valuemin": min, "aria-valuemax": max, className: rangeSliderTheme.thumb, style: {
|
|
3723
4982
|
position: 'absolute',
|
|
3724
4983
|
width: '100%',
|
|
3725
4984
|
height: '4px',
|
|
@@ -4697,12 +5956,29 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4697
5956
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4698
5957
|
if (error.name === 'AbortError')
|
|
4699
5958
|
return;
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
5959
|
+
// Check if it's a 404 error (suggestions not enabled for this store).
|
|
5960
|
+
// The search SDK wraps axios errors into plain Error objects with the status
|
|
5961
|
+
// embedded in the message, e.g. "[getSuggestions] ... (404)".
|
|
5962
|
+
const errMsg = error.message || '';
|
|
5963
|
+
const is404 = err?.response?.status === 404 ||
|
|
5964
|
+
err?.status === 404 ||
|
|
5965
|
+
/\(404\)/.test(errMsg);
|
|
5966
|
+
if (is404) {
|
|
5967
|
+
// Silently handle 404 - suggestions feature not enabled
|
|
5968
|
+
log.verbose('Query suggestions not enabled for this store (404)');
|
|
5969
|
+
setSuggestions([]);
|
|
5970
|
+
setDropdownRecommendations(null);
|
|
5971
|
+
setError(null);
|
|
5972
|
+
}
|
|
5973
|
+
else {
|
|
5974
|
+
// For other errors, log and set error state
|
|
5975
|
+
log.error('Failed to fetch query suggestions', { query: searchQuery, error: error.message });
|
|
5976
|
+
setError(error);
|
|
5977
|
+
setSuggestions([]);
|
|
5978
|
+
setDropdownRecommendations(null);
|
|
5979
|
+
if (onError) {
|
|
5980
|
+
onError(error);
|
|
5981
|
+
}
|
|
4706
5982
|
}
|
|
4707
5983
|
}
|
|
4708
5984
|
finally {
|
|
@@ -6269,7 +7545,14 @@ const EVENTS = {
|
|
|
6269
7545
|
TRENDING_CLICK: 'suggestions.trending_click',
|
|
6270
7546
|
SEARCH_SUBMIT: 'suggestions.search_submit',
|
|
6271
7547
|
DROPDOWN_OPEN: 'suggestions.dropdown_open',
|
|
6272
|
-
DROPDOWN_CLOSE: 'suggestions.dropdown_close'
|
|
7548
|
+
DROPDOWN_CLOSE: 'suggestions.dropdown_close',
|
|
7549
|
+
// Variant & product interaction events
|
|
7550
|
+
VARIANT_SELECT: 'product.variant_select',
|
|
7551
|
+
VARIANT_HOVER: 'product.variant_hover',
|
|
7552
|
+
ADD_TO_CART: 'product.add_to_cart',
|
|
7553
|
+
PRODUCT_IMPRESSION: 'product.impression',
|
|
7554
|
+
SWATCH_CLICK: 'product.swatch_click',
|
|
7555
|
+
};
|
|
6273
7556
|
// ============================================================================
|
|
6274
7557
|
// Hook Implementation
|
|
6275
7558
|
// ============================================================================
|
|
@@ -6293,11 +7576,16 @@ function useSuggestionsAnalytics(options) {
|
|
|
6293
7576
|
return;
|
|
6294
7577
|
const searchContext = context ?? contextOption;
|
|
6295
7578
|
try {
|
|
7579
|
+
// Extract query for search-related events (backend requires query at top level for search events)
|
|
7580
|
+
const { query, ...restMetadata } = metadata;
|
|
7581
|
+
const isSearchEvent = eventName.includes('search') || eventName === EVENTS.SEARCH_SUBMIT;
|
|
6296
7582
|
await client.trackEvent?.({
|
|
6297
7583
|
event_name: eventName,
|
|
6298
7584
|
analytics_tags: analyticsTags,
|
|
7585
|
+
// Include query at top level for search events
|
|
7586
|
+
...(isSearchEvent && query ? { query } : {}),
|
|
6299
7587
|
metadata: {
|
|
6300
|
-
...
|
|
7588
|
+
...restMetadata,
|
|
6301
7589
|
timestamp: Date.now(),
|
|
6302
7590
|
source: 'suggestions_dropdown',
|
|
6303
7591
|
},
|
|
@@ -7120,7 +8408,7 @@ function DropdownPanel({ children, position = 'absolute', top = '100%', left = 0
|
|
|
7120
8408
|
|
|
7121
8409
|
/**
|
|
7122
8410
|
* Parses suggestion text containing <mark>...</mark> and returns React nodes
|
|
7123
|
-
* with the marked segments rendered as
|
|
8411
|
+
* with the marked segments rendered as styled elements. Safe: inner content
|
|
7124
8412
|
* is rendered as text, not HTML.
|
|
7125
8413
|
*/
|
|
7126
8414
|
const defaultMarkStyle = {
|
|
@@ -7129,9 +8417,34 @@ const defaultMarkStyle = {
|
|
|
7129
8417
|
borderRadius: '2px',
|
|
7130
8418
|
padding: '0 2px',
|
|
7131
8419
|
};
|
|
8420
|
+
/** Compute styles based on highlight options */
|
|
8421
|
+
function computeHighlightStyles(options) {
|
|
8422
|
+
const style = options.highlightStyle || 'background';
|
|
8423
|
+
const base = {};
|
|
8424
|
+
switch (style) {
|
|
8425
|
+
case 'background':
|
|
8426
|
+
base.backgroundColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))';
|
|
8427
|
+
base.borderRadius = '2px';
|
|
8428
|
+
base.padding = '0 2px';
|
|
8429
|
+
break;
|
|
8430
|
+
case 'underline':
|
|
8431
|
+
base.textDecoration = 'underline';
|
|
8432
|
+
base.textDecorationColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.8))';
|
|
8433
|
+
base.textUnderlineOffset = '2px';
|
|
8434
|
+
break;
|
|
8435
|
+
}
|
|
8436
|
+
if (options.highlightTextColor) {
|
|
8437
|
+
base.color = options.highlightTextColor;
|
|
8438
|
+
}
|
|
8439
|
+
else {
|
|
8440
|
+
base.color = 'var(--seekora-highlight-color, inherit)';
|
|
8441
|
+
}
|
|
8442
|
+
base.fontWeight = options.highlightFontWeight || 'var(--seekora-highlight-weight, 500)';
|
|
8443
|
+
return base;
|
|
8444
|
+
}
|
|
7132
8445
|
/**
|
|
7133
8446
|
* Converts a string like "lined <mark>blue</mark>" into React nodes with
|
|
7134
|
-
* the marked part rendered as a
|
|
8447
|
+
* the marked part rendered as a styled element. When no <mark> tags are
|
|
7135
8448
|
* present, returns the string as-is.
|
|
7136
8449
|
*/
|
|
7137
8450
|
function parseHighlightMarkup(text, options = {}) {
|
|
@@ -7140,11 +8453,18 @@ function parseHighlightMarkup(text, options = {}) {
|
|
|
7140
8453
|
const parts = text.split(/(<mark>[\s\S]*?<\/mark>)/g);
|
|
7141
8454
|
if (parts.length <= 1)
|
|
7142
8455
|
return text;
|
|
7143
|
-
const { markClassName, markStyle } = options;
|
|
8456
|
+
const { markClassName, markStyle, highlightTag } = options;
|
|
8457
|
+
const Tag = (highlightTag || 'mark');
|
|
8458
|
+
// Compute styles: if no custom options provided, use legacy defaults
|
|
8459
|
+
const hasCustomOptions = options.highlightColor || options.highlightTextColor
|
|
8460
|
+
|| options.highlightFontWeight || options.highlightStyle;
|
|
8461
|
+
const computedStyle = hasCustomOptions
|
|
8462
|
+
? computeHighlightStyles(options)
|
|
8463
|
+
: defaultMarkStyle;
|
|
7144
8464
|
return (React.createElement(React.Fragment, null, parts.map((part, i) => {
|
|
7145
8465
|
const m = part.match(/^<mark>([\s\S]*)<\/mark>$/);
|
|
7146
8466
|
if (m) {
|
|
7147
|
-
return (React.createElement(
|
|
8467
|
+
return (React.createElement(Tag, { key: i, className: markClassName, style: { ...computedStyle, ...markStyle } }, m[1]));
|
|
7148
8468
|
}
|
|
7149
8469
|
return part;
|
|
7150
8470
|
})));
|
|
@@ -8019,6 +9339,41 @@ const highlightText = (text, query, options = {}) => {
|
|
|
8019
9339
|
const classAttr = className ? ` class="${className}"` : '';
|
|
8020
9340
|
return escapeHtml(text).replace(new RegExp(`(${escapeHtml(escapedQuery)})`, 'gi'), `<${tag}${classAttr}${styleAttr}>$1</${tag}>`);
|
|
8021
9341
|
};
|
|
9342
|
+
/**
|
|
9343
|
+
* Safe React-based highlight rendering (no dangerouslySetInnerHTML).
|
|
9344
|
+
* Returns React nodes with matched text wrapped in the specified tag element.
|
|
9345
|
+
*/
|
|
9346
|
+
const highlightTextReact = (text, query, options = {}) => {
|
|
9347
|
+
if (!query || !text)
|
|
9348
|
+
return text;
|
|
9349
|
+
const { tag = 'mark', className, style } = options;
|
|
9350
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9351
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
9352
|
+
const parts = text.split(regex);
|
|
9353
|
+
if (parts.length <= 1)
|
|
9354
|
+
return text;
|
|
9355
|
+
const defaultHighlightStyle = {
|
|
9356
|
+
backgroundColor: 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))',
|
|
9357
|
+
fontWeight: 500,
|
|
9358
|
+
borderRadius: '2px',
|
|
9359
|
+
padding: '0 2px',
|
|
9360
|
+
...style,
|
|
9361
|
+
};
|
|
9362
|
+
return parts.map((part, i) => {
|
|
9363
|
+
if (regex.test(part)) {
|
|
9364
|
+
// Reset lastIndex after test
|
|
9365
|
+
regex.lastIndex = 0;
|
|
9366
|
+
return createElement(tag, {
|
|
9367
|
+
key: i,
|
|
9368
|
+
className,
|
|
9369
|
+
style: defaultHighlightStyle,
|
|
9370
|
+
}, part);
|
|
9371
|
+
}
|
|
9372
|
+
// Reset lastIndex after test
|
|
9373
|
+
regex.lastIndex = 0;
|
|
9374
|
+
return part;
|
|
9375
|
+
});
|
|
9376
|
+
};
|
|
8022
9377
|
// ============================================================================
|
|
8023
9378
|
// Variant Utilities
|
|
8024
9379
|
// ============================================================================
|
|
@@ -10563,9 +11918,7 @@ const AmazonDropdown = forwardRef(function AmazonDropdown(props, ref) {
|
|
|
10563
11918
|
React.createElement("div", { style: styles.suggestionIcon },
|
|
10564
11919
|
React.createElement(SearchIcon$5, null)),
|
|
10565
11920
|
React.createElement("div", { style: styles.suggestionContent },
|
|
10566
|
-
React.createElement("div", { style: styles.suggestionQuery,
|
|
10567
|
-
__html: highlightText(suggestion.query, query, { className: 'highlight' })
|
|
10568
|
-
} }),
|
|
11921
|
+
React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { className: 'highlight' })),
|
|
10569
11922
|
showDepartments && firstCategory && (React.createElement("div", { style: styles.suggestionContext },
|
|
10570
11923
|
React.createElement("span", { style: styles.suggestionDepartment },
|
|
10571
11924
|
"in ",
|
|
@@ -10910,11 +12263,7 @@ const GoogleDropdown = forwardRef(function GoogleDropdown(props, ref) {
|
|
|
10910
12263
|
item.type === 'trending' ? React.createElement(TrendingIcon$2, null) :
|
|
10911
12264
|
React.createElement(SearchIcon$4, null)),
|
|
10912
12265
|
React.createElement("div", { style: styles.itemContent },
|
|
10913
|
-
React.createElement("span", { style: styles.itemText,
|
|
10914
|
-
__html: query
|
|
10915
|
-
? highlightText(item.query, query, { tag: 'b' })
|
|
10916
|
-
: item.query
|
|
10917
|
-
} }),
|
|
12266
|
+
React.createElement("span", { style: styles.itemText }, query ? highlightTextReact(item.query, query, { tag: 'b' }) : item.query),
|
|
10918
12267
|
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
10919
12268
|
item.type === 'trending' && showTrendingIndicator && (React.createElement("span", { style: styles.trending },
|
|
10920
12269
|
React.createElement(TrendingIcon$2, null),
|
|
@@ -11369,11 +12718,7 @@ const PinterestDropdown = forwardRef(function PinterestDropdown(props, ref) {
|
|
|
11369
12718
|
setHoveredSuggestion(idx);
|
|
11370
12719
|
setActiveIndex(idx);
|
|
11371
12720
|
}, onMouseLeave: () => setHoveredSuggestion(-1) },
|
|
11372
|
-
React.createElement("span", { style: styles.suggestionIcon,
|
|
11373
|
-
__html: query
|
|
11374
|
-
? highlightText(s.query, query, { tag: 'strong' })
|
|
11375
|
-
: s.query
|
|
11376
|
-
} })))))),
|
|
12721
|
+
React.createElement("span", { style: styles.suggestionIcon }, query ? highlightTextReact(s.query, query, { tag: 'strong' }) : s.query)))))),
|
|
11377
12722
|
displayProducts.length > 0 && (React.createElement(React.Fragment, null,
|
|
11378
12723
|
React.createElement("div", { style: styles.sectionTitle },
|
|
11379
12724
|
React.createElement(TrendingIcon$1, null),
|
|
@@ -11903,9 +13248,7 @@ const SpotlightDropdown = forwardRef(function SpotlightDropdown(props, ref) {
|
|
|
11903
13248
|
}, onMouseEnter: () => setActiveIndex(idx) },
|
|
11904
13249
|
React.createElement("div", { style: mergeStyles(styles.itemIcon, isActive ? styles.itemIconActive : undefined) }, item.icon),
|
|
11905
13250
|
React.createElement("div", { style: styles.itemContent },
|
|
11906
|
-
React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined),
|
|
11907
|
-
__html: highlightText(item.title, query, { tag: 'strong' })
|
|
11908
|
-
} }))));
|
|
13251
|
+
React.createElement("div", { style: mergeStyles(styles.itemTitle, isActive ? styles.itemTitleActive : undefined) }, highlightTextReact(item.title, query, { tag: 'strong' })))));
|
|
11909
13252
|
}))),
|
|
11910
13253
|
processedProducts.length > 0 && (React.createElement("div", { style: styles.section },
|
|
11911
13254
|
React.createElement("div", { style: styles.sectionTitle }, "Products"),
|
|
@@ -12345,9 +13688,7 @@ const ShopifyDropdown = forwardRef(function ShopifyDropdown(props, ref) {
|
|
|
12345
13688
|
React.createElement("div", { style: styles.suggestionIcon },
|
|
12346
13689
|
React.createElement(SearchIcon$1, null)),
|
|
12347
13690
|
React.createElement("div", { style: styles.suggestionContent },
|
|
12348
|
-
React.createElement("div", { style: styles.suggestionQuery,
|
|
12349
|
-
__html: highlightText(suggestion.query, query, { tag: 'mark' })
|
|
12350
|
-
} }),
|
|
13691
|
+
React.createElement("div", { style: styles.suggestionQuery }, highlightTextReact(suggestion.query, query, { tag: 'mark' })),
|
|
12351
13692
|
suggestion.count && (React.createElement("div", { style: styles.suggestionMeta },
|
|
12352
13693
|
suggestion.count,
|
|
12353
13694
|
" results"))),
|
|
@@ -12850,9 +14191,7 @@ const MobileSheetDropdown = forwardRef(function MobileSheetDropdown(props, ref)
|
|
|
12850
14191
|
React.createElement("div", { style: styles.itemIcon },
|
|
12851
14192
|
React.createElement(SearchIcon, null)),
|
|
12852
14193
|
React.createElement("div", { style: styles.itemContent },
|
|
12853
|
-
React.createElement("div", { style: styles.itemTitle,
|
|
12854
|
-
__html: highlightText(s.query, inputValue, { tag: 'strong' })
|
|
12855
|
-
} }),
|
|
14194
|
+
React.createElement("div", { style: styles.itemTitle }, highlightTextReact(s.query, inputValue, { tag: 'strong' })),
|
|
12856
14195
|
s.count && (React.createElement("div", { style: styles.itemSubtitle },
|
|
12857
14196
|
s.count,
|
|
12858
14197
|
" results"))),
|
|
@@ -13153,9 +14492,7 @@ const MinimalDropdown = forwardRef(function MinimalDropdown(props, ref) {
|
|
|
13153
14492
|
return (React.createElement("div", { key: s.id || `suggestion-${idx}`, "data-index": itemIdx, style: mergeStyles(styles.item, isActive ? styles.itemActive : undefined, isLast ? styles.itemLast : undefined), onClick: () => onSuggestionSelect?.(s._raw, idx), onMouseEnter: () => setActiveIndex(itemIdx) },
|
|
13154
14493
|
showIndices && (React.createElement("span", { style: styles.itemIndex }, itemIdx + 1)),
|
|
13155
14494
|
React.createElement("div", { style: styles.itemContent },
|
|
13156
|
-
React.createElement("div", { style: styles.itemQuery,
|
|
13157
|
-
__html: highlightText(s.query, query, { tag: 'mark' })
|
|
13158
|
-
} }),
|
|
14495
|
+
React.createElement("div", { style: styles.itemQuery }, highlightTextReact(s.query, query, { tag: 'mark' })),
|
|
13159
14496
|
s.count && (React.createElement("div", { style: styles.itemMeta },
|
|
13160
14497
|
s.count.toLocaleString(),
|
|
13161
14498
|
" results"))),
|