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