@seekora-ai/ui-sdk-react 0.2.14 → 0.2.16
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.map +1 -1
- package/dist/components/CurrentRefinements.js +69 -9
- package/dist/components/FacetDropdown.d.ts +94 -0
- package/dist/components/FacetDropdown.d.ts.map +1 -0
- package/dist/components/FacetDropdown.js +396 -0
- package/dist/components/Facets.d.ts +30 -0
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +215 -7
- package/dist/components/FederatedDropdown.d.ts.map +1 -1
- package/dist/components/FederatedDropdown.js +45 -31
- package/dist/components/InfiniteHits.d.ts +0 -7
- package/dist/components/InfiniteHits.d.ts.map +1 -1
- package/dist/components/InfiniteHits.js +2 -13
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +27 -9
- package/dist/components/QuerySuggestions.d.ts +0 -4
- package/dist/components/QuerySuggestions.d.ts.map +1 -1
- package/dist/components/QuerySuggestions.js +1 -17
- package/dist/components/QuerySuggestionsDropdown.d.ts +0 -4
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
- package/dist/components/QuerySuggestionsDropdown.js +32 -33
- package/dist/components/RangeInput.d.ts.map +1 -1
- package/dist/components/RangeInput.js +6 -6
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +54 -32
- package/dist/components/Recommendations.d.ts +0 -7
- package/dist/components/Recommendations.d.ts.map +1 -1
- package/dist/components/Recommendations.js +3 -23
- package/dist/components/RichQuerySuggestions.d.ts +0 -4
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/RichQuerySuggestions.js +40 -35
- package/dist/components/SearchBar.d.ts +0 -4
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +17 -11
- package/dist/components/SearchBarWithSuggestions.js +4 -4
- package/dist/components/SearchLayout.d.ts.map +1 -1
- package/dist/components/SearchLayout.js +22 -17
- package/dist/components/SearchProvider.d.ts.map +1 -1
- package/dist/components/SearchProvider.js +1 -3
- package/dist/components/SearchResults.d.ts +0 -6
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +38 -39
- package/dist/components/primitives/ActionButtons.d.ts.map +1 -1
- package/dist/components/primitives/ActionButtons.js +34 -10
- package/dist/components/primitives/BadgeList.d.ts.map +1 -1
- package/dist/components/primitives/BadgeList.js +33 -13
- package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
- package/dist/components/primitives/ImageDisplay.js +32 -19
- package/dist/components/primitives/ImageZoom.d.ts.map +1 -1
- package/dist/components/primitives/ImageZoom.js +85 -30
- package/dist/components/primitives/VariantSelector.js +10 -10
- package/dist/components/primitives/VariantSwatches.d.ts.map +1 -1
- package/dist/components/primitives/VariantSwatches.js +28 -13
- package/dist/components/product-page/ProductGallery.d.ts +8 -1
- package/dist/components/product-page/ProductGallery.d.ts.map +1 -1
- package/dist/components/product-page/ProductGallery.js +2 -2
- package/dist/components/section-primitives/SectionItemGrid.d.ts +1 -3
- package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
- package/dist/components/section-primitives/SectionItemGrid.js +1 -4
- package/dist/components/section-primitives/SectionSearchProvider.d.ts +3 -1
- package/dist/components/section-primitives/SectionSearchProvider.d.ts.map +1 -1
- package/dist/components/section-primitives/SectionSearchProvider.js +3 -2
- package/dist/components/section-primitives/index.d.ts +0 -1
- package/dist/components/section-primitives/index.d.ts.map +1 -1
- package/dist/components/section-primitives/index.js +0 -1
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +3 -21
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +3 -20
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -2
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +78 -78
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +41 -41
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +40 -41
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -3
- package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
- package/dist/components/suggestions/SuggestionSearchBar.js +2 -15
- package/dist/components/suggestions/types.d.ts +0 -6
- package/dist/components/suggestions/types.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/DropdownPanel.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/DropdownPanel.js +15 -2
- package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemCard.js +48 -11
- package/dist/components/suggestions-primitives/ItemGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemGrid.js +18 -5
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCard.js +36 -12
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +52 -20
- package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductGrid.js +8 -3
- package/dist/components/suggestions-primitives/RecentSearchesList.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/RecentSearchesList.js +12 -5
- package/dist/components/suggestions-primitives/SearchInput.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SearchInput.js +29 -10
- package/dist/components/suggestions-primitives/SuggestionItem.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionItem.js +8 -3
- package/dist/components/suggestions-primitives/SuggestionList.d.ts +1 -8
- package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionList.js +1 -7
- package/dist/components/suggestions-primitives/TrendingList.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/TrendingList.js +14 -7
- package/dist/components/suggestions-primitives/index.d.ts +1 -3
- package/dist/components/suggestions-primitives/index.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/index.js +1 -2
- package/dist/docsearch/components/DocSearch.d.ts.map +1 -1
- package/dist/docsearch/components/DocSearch.js +1 -1
- package/dist/docsearch/components/Results.d.ts +1 -3
- package/dist/docsearch/components/Results.d.ts.map +1 -1
- package/dist/docsearch/components/Results.js +1 -9
- package/dist/docsearch/components/SearchBox.d.ts +1 -2
- package/dist/docsearch/components/SearchBox.d.ts.map +1 -1
- package/dist/docsearch/components/SearchBox.js +4 -6
- package/dist/docsearch/hooks/useSeekoraSearch.d.ts.map +1 -1
- package/dist/docsearch/hooks/useSeekoraSearch.js +6 -0
- package/dist/docsearch/types.d.ts +0 -1
- package/dist/docsearch/types.d.ts.map +1 -1
- package/dist/docsearch.css +2 -5
- package/dist/hooks/useClickTracking.d.ts.map +1 -1
- package/dist/hooks/useClickTracking.js +4 -11
- package/dist/hooks/useExperiment.d.ts.map +1 -1
- package/dist/hooks/useExperiment.js +10 -33
- package/dist/hooks/useFilters.d.ts +27 -0
- package/dist/hooks/useFilters.d.ts.map +1 -0
- package/dist/hooks/useFilters.js +66 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +166 -81
- package/dist/src/index.esm.js +2141 -1048
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2142 -1049
- package/dist/src/index.js.map +1 -1
- package/dist/utils/responsive.d.ts +130 -0
- package/dist/utils/responsive.d.ts.map +1 -0
- package/dist/utils/responsive.js +231 -0
- package/package.json +7 -7
- package/src/docsearch/docsearch.css +2 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CurrentRefinements.d.ts","sourceRoot":"","sources":["../../src/components/CurrentRefinements.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"CurrentRefinements.d.ts","sourceRoot":"","sources":["../../src/components/CurrentRefinements.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AAWpE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEpF,uCAAuC;AACvC,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,UAAU,GAAG,SAAS,CAAC;AAErE,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,4CAA4C;IAC5C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9E,8BAA8B;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yCAAyC;IACzC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,0CAA0C;IAC1C,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iCAAiC;IACjC,eAAe,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACxC,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC;AA0FD,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAuShE,CAAC"}
|
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* Displays currently active filters/refinements with ability to clear them.
|
|
5
5
|
* Supports StateManager auto-sync, display variants, layout modes, and animations.
|
|
6
6
|
*/
|
|
7
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
7
|
+
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
8
8
|
import { useSearchContext } from './SearchProvider';
|
|
9
9
|
import { useSearchState } from '../hooks/useSearchState';
|
|
10
10
|
import { clsx } from 'clsx';
|
|
11
|
+
const TRANSITIONS = {
|
|
12
|
+
fast: '150ms ease-in-out',
|
|
13
|
+
normal: '200ms ease-in-out',
|
|
14
|
+
slow: '300ms ease-in-out',
|
|
15
|
+
};
|
|
11
16
|
/** Get variant-specific styles */
|
|
12
17
|
const getVariantStyles = (variant, themeColors, themeSpacing, themeBorderRadius, fieldColor) => {
|
|
13
18
|
const baseBg = fieldColor || `var(--seekora-refinement-bg, ${themeColors.hover})`;
|
|
@@ -91,10 +96,59 @@ export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementC
|
|
|
91
96
|
const { theme } = useSearchContext();
|
|
92
97
|
const { refinements: stateRefinements, removeRefinement, clearRefinements } = useSearchState();
|
|
93
98
|
const refinementsTheme = customTheme || {};
|
|
99
|
+
// Format a numeric value for display (auto-detect price fields)
|
|
100
|
+
const formatRangeValue = (field, val) => {
|
|
101
|
+
const f = field.toLowerCase();
|
|
102
|
+
if (f.includes('price') || f.includes('cost') || f.includes('amount')) {
|
|
103
|
+
return `$${val.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
104
|
+
}
|
|
105
|
+
return val.toLocaleString();
|
|
106
|
+
};
|
|
107
|
+
// Create a stable serialized key from stateRefinements to detect actual changes
|
|
108
|
+
// (needed because state manager mutates the array in place)
|
|
109
|
+
const stateRefinementsKey = useMemo(() => stateRefinements.map(r => `${r.field}:${r.value}`).sort().join('|'), [stateRefinements.map(r => `${r.field}:${r.value}`).sort().join('|')]);
|
|
94
110
|
// Use props if provided, otherwise auto-read from StateManager
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
// Combines >= / <= range refinements into single formatted pills
|
|
112
|
+
const refinements = useMemo(() => {
|
|
113
|
+
if (refinementsProp !== undefined) {
|
|
114
|
+
return refinementsProp;
|
|
115
|
+
}
|
|
116
|
+
const rangeMap = new Map();
|
|
117
|
+
const nonRange = [];
|
|
118
|
+
stateRefinements.forEach(r => {
|
|
119
|
+
const minMatch = r.value.match(/^>=(\d+(?:\.\d+)?)$/);
|
|
120
|
+
const maxMatch = r.value.match(/^<=(\d+(?:\.\d+)?)$/);
|
|
121
|
+
if (minMatch || maxMatch) {
|
|
122
|
+
if (!rangeMap.has(r.field))
|
|
123
|
+
rangeMap.set(r.field, {});
|
|
124
|
+
const entry = rangeMap.get(r.field);
|
|
125
|
+
if (minMatch)
|
|
126
|
+
entry.minVal = parseFloat(minMatch[1]);
|
|
127
|
+
if (maxMatch)
|
|
128
|
+
entry.maxVal = parseFloat(maxMatch[1]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
nonRange.push({ field: r.field, value: r.value });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
rangeMap.forEach((range, field) => {
|
|
135
|
+
let displayValue;
|
|
136
|
+
if (range.minVal !== undefined && range.maxVal !== undefined) {
|
|
137
|
+
displayValue = `${formatRangeValue(field, range.minVal)} – ${formatRangeValue(field, range.maxVal)}`;
|
|
138
|
+
}
|
|
139
|
+
else if (range.minVal !== undefined) {
|
|
140
|
+
displayValue = `≥ ${formatRangeValue(field, range.minVal)}`;
|
|
141
|
+
}
|
|
142
|
+
else if (range.maxVal !== undefined) {
|
|
143
|
+
displayValue = `≤ ${formatRangeValue(field, range.maxVal)}`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
nonRange.push({ field, value: '__range__', displayValue });
|
|
149
|
+
});
|
|
150
|
+
return nonRange;
|
|
151
|
+
}, [refinementsProp, stateRefinementsKey]);
|
|
98
152
|
// Track items for entry/exit animations
|
|
99
153
|
const [visibleItems, setVisibleItems] = useState(new Set());
|
|
100
154
|
const [exitingItems, setExitingItems] = useState(new Set());
|
|
@@ -118,9 +172,15 @@ export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementC
|
|
|
118
172
|
prevRefinementsRef.current = [...refinements];
|
|
119
173
|
}, [refinements]);
|
|
120
174
|
const handleClear = (field, value) => {
|
|
121
|
-
// If synced with StateManager and no prop provided, auto-clear via StateManager
|
|
122
175
|
if (refinementsProp === undefined) {
|
|
123
|
-
|
|
176
|
+
if (value === '__range__') {
|
|
177
|
+
// Clear all range refinements for this field
|
|
178
|
+
const rangeRefs = stateRefinements.filter(r => r.field === field && (/^>=/.test(r.value) || /^<=/.test(r.value)));
|
|
179
|
+
rangeRefs.forEach((r, i) => removeRefinement(field, r.value, i === rangeRefs.length - 1));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
removeRefinement(field, value);
|
|
183
|
+
}
|
|
124
184
|
}
|
|
125
185
|
if (onRefinementClear) {
|
|
126
186
|
onRefinementClear(field, value);
|
|
@@ -147,7 +207,7 @@ export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementC
|
|
|
147
207
|
margin: layout === 'vertical'
|
|
148
208
|
? `0 0 ${theme.spacing.small} 0`
|
|
149
209
|
: `0 ${theme.spacing.small} ${theme.spacing.small} 0`,
|
|
150
|
-
transition:
|
|
210
|
+
transition: `all ${TRANSITIONS.normal}`,
|
|
151
211
|
opacity: exitingItems.has(key) ? 0 : 1,
|
|
152
212
|
transform: exitingItems.has(key) ? 'scale(0.8)' : 'scale(1)',
|
|
153
213
|
animation: isEntering ? 'seekoraChipIn 200ms ease-out' : undefined,
|
|
@@ -171,7 +231,7 @@ export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementC
|
|
|
171
231
|
alignItems: 'center',
|
|
172
232
|
justifyContent: 'center',
|
|
173
233
|
borderRadius: '50%',
|
|
174
|
-
transition:
|
|
234
|
+
transition: `opacity ${TRANSITIONS.fast}`,
|
|
175
235
|
opacity: 0.6,
|
|
176
236
|
}, "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())));
|
|
177
237
|
};
|
|
@@ -230,6 +290,6 @@ export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementC
|
|
|
230
290
|
cursor: 'pointer',
|
|
231
291
|
fontSize: theme.typography.fontSize.small,
|
|
232
292
|
textDecoration: 'underline',
|
|
233
|
-
transition:
|
|
293
|
+
transition: `background-color ${TRANSITIONS.fast}`,
|
|
234
294
|
} }, "Clear all filters"))));
|
|
235
295
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FacetDropdown Component
|
|
3
|
+
*
|
|
4
|
+
* A dropdown component that displays facet values from a specific field
|
|
5
|
+
* and allows users to filter search results by selecting values.
|
|
6
|
+
*
|
|
7
|
+
* Can be used standalone (fetches its own data) or integrated with SearchProvider.
|
|
8
|
+
*
|
|
9
|
+
* @cssVariables
|
|
10
|
+
* Customize the dropdown appearance using CSS variables:
|
|
11
|
+
*
|
|
12
|
+
* Button:
|
|
13
|
+
* - `--facet-dropdown-button-bg`: Button background color
|
|
14
|
+
* - `--facet-dropdown-text`: Button text color
|
|
15
|
+
* - `--facet-dropdown-border`: Button and panel border color
|
|
16
|
+
* - `--facet-dropdown-font-size`: Button font size (default: 0.875rem)
|
|
17
|
+
* - `--facet-dropdown-font-weight`: Button font weight (default: 500)
|
|
18
|
+
*
|
|
19
|
+
* Dropdown Panel:
|
|
20
|
+
* - `--facet-dropdown-bg`: Panel background color
|
|
21
|
+
* - `--facet-dropdown-shadow`: Panel box shadow
|
|
22
|
+
*
|
|
23
|
+
* Options:
|
|
24
|
+
* - `--facet-dropdown-option-text`: Option text color
|
|
25
|
+
* - `--facet-dropdown-option-font-size`: Option font size (default: 0.875rem)
|
|
26
|
+
* - `--facet-dropdown-option-font-weight-active`: Active option font weight (default: 500)
|
|
27
|
+
* - `--facet-dropdown-option-active-bg`: Active option background color
|
|
28
|
+
*
|
|
29
|
+
* Count Badge:
|
|
30
|
+
* - `--facet-dropdown-count-text`: Count text color
|
|
31
|
+
* - `--facet-dropdown-count-font-size`: Count font size (default: 0.75rem)
|
|
32
|
+
*
|
|
33
|
+
* Scrollbar:
|
|
34
|
+
* - `--facet-dropdown-scrollbar-thumb`: Scrollbar thumb color
|
|
35
|
+
* - `--facet-dropdown-scrollbar-thumb-hover`: Scrollbar thumb hover color
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <FacetDropdown
|
|
40
|
+
* field="category"
|
|
41
|
+
* placeholder="All Categories"
|
|
42
|
+
* applyFilter={true}
|
|
43
|
+
* showCounts={true}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import React from 'react';
|
|
48
|
+
import type { SeekoraClient } from '@seekora-ai/search-sdk';
|
|
49
|
+
export interface FacetDropdownOption {
|
|
50
|
+
value: string;
|
|
51
|
+
count: number;
|
|
52
|
+
label?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface FacetDropdownTheme {
|
|
55
|
+
button?: string;
|
|
56
|
+
buttonOpen?: string;
|
|
57
|
+
panel?: string;
|
|
58
|
+
option?: string;
|
|
59
|
+
optionActive?: string;
|
|
60
|
+
optionHover?: string;
|
|
61
|
+
caretIcon?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface FacetDropdownProps {
|
|
64
|
+
/** The facet field to display (e.g., "category", "brand", "product_type") */
|
|
65
|
+
field: string;
|
|
66
|
+
/** Placeholder text when no value is selected */
|
|
67
|
+
placeholder?: string;
|
|
68
|
+
/** Custom className for the button */
|
|
69
|
+
className?: string;
|
|
70
|
+
/** Custom theme overrides */
|
|
71
|
+
theme?: FacetDropdownTheme;
|
|
72
|
+
/** Called when a value is selected */
|
|
73
|
+
onChange?: (value: string) => void;
|
|
74
|
+
/** Current selected value (controlled mode) */
|
|
75
|
+
value?: string;
|
|
76
|
+
/** Maximum number of options to show before scrolling */
|
|
77
|
+
maxOptions?: number;
|
|
78
|
+
/** Whether to show facet counts */
|
|
79
|
+
showCounts?: boolean;
|
|
80
|
+
/** Custom options (if you want to provide static options instead of fetching) */
|
|
81
|
+
options?: FacetDropdownOption[];
|
|
82
|
+
/** Standalone mode: provide your own search client */
|
|
83
|
+
client?: SeekoraClient;
|
|
84
|
+
/** Apply filter immediately on selection (default: true) */
|
|
85
|
+
applyFilter?: boolean;
|
|
86
|
+
/** Navigate to search page on selection (default: false) */
|
|
87
|
+
navigateOnSelect?: boolean;
|
|
88
|
+
/** Base URL for navigation (e.g., "/search") */
|
|
89
|
+
searchPageUrl?: string;
|
|
90
|
+
/** Use dedicated Filters API instead of search API for fetching facet values */
|
|
91
|
+
useFiltersApi?: boolean;
|
|
92
|
+
}
|
|
93
|
+
export declare const FacetDropdown: React.FC<FacetDropdownProps>;
|
|
94
|
+
//# sourceMappingURL=FacetDropdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacetDropdown.d.ts","sourceRoot":"","sources":["../../src/components/FacetDropdown.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAe5D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iFAAiF;IACjF,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC,sDAAsD;IACtD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAMD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmctD,CAAC"}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FacetDropdown Component
|
|
3
|
+
*
|
|
4
|
+
* A dropdown component that displays facet values from a specific field
|
|
5
|
+
* and allows users to filter search results by selecting values.
|
|
6
|
+
*
|
|
7
|
+
* Can be used standalone (fetches its own data) or integrated with SearchProvider.
|
|
8
|
+
*
|
|
9
|
+
* @cssVariables
|
|
10
|
+
* Customize the dropdown appearance using CSS variables:
|
|
11
|
+
*
|
|
12
|
+
* Button:
|
|
13
|
+
* - `--facet-dropdown-button-bg`: Button background color
|
|
14
|
+
* - `--facet-dropdown-text`: Button text color
|
|
15
|
+
* - `--facet-dropdown-border`: Button and panel border color
|
|
16
|
+
* - `--facet-dropdown-font-size`: Button font size (default: 0.875rem)
|
|
17
|
+
* - `--facet-dropdown-font-weight`: Button font weight (default: 500)
|
|
18
|
+
*
|
|
19
|
+
* Dropdown Panel:
|
|
20
|
+
* - `--facet-dropdown-bg`: Panel background color
|
|
21
|
+
* - `--facet-dropdown-shadow`: Panel box shadow
|
|
22
|
+
*
|
|
23
|
+
* Options:
|
|
24
|
+
* - `--facet-dropdown-option-text`: Option text color
|
|
25
|
+
* - `--facet-dropdown-option-font-size`: Option font size (default: 0.875rem)
|
|
26
|
+
* - `--facet-dropdown-option-font-weight-active`: Active option font weight (default: 500)
|
|
27
|
+
* - `--facet-dropdown-option-active-bg`: Active option background color
|
|
28
|
+
*
|
|
29
|
+
* Count Badge:
|
|
30
|
+
* - `--facet-dropdown-count-text`: Count text color
|
|
31
|
+
* - `--facet-dropdown-count-font-size`: Count font size (default: 0.75rem)
|
|
32
|
+
*
|
|
33
|
+
* Scrollbar:
|
|
34
|
+
* - `--facet-dropdown-scrollbar-thumb`: Scrollbar thumb color
|
|
35
|
+
* - `--facet-dropdown-scrollbar-thumb-hover`: Scrollbar thumb hover color
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <FacetDropdown
|
|
40
|
+
* field="category"
|
|
41
|
+
* placeholder="All Categories"
|
|
42
|
+
* applyFilter={true}
|
|
43
|
+
* showCounts={true}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
48
|
+
import { createPortal } from 'react-dom';
|
|
49
|
+
import { useSearchContext } from './SearchProvider';
|
|
50
|
+
import { useSearchState } from '../hooks/useSearchState';
|
|
51
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
52
|
+
// Z-index scale for consistent layering
|
|
53
|
+
// SearchOverlay uses z-100, so dropdown must be higher
|
|
54
|
+
const Z_INDEX = {
|
|
55
|
+
dropdown: 150, // Higher than SearchOverlay (100)
|
|
56
|
+
modal: 200,
|
|
57
|
+
overlay: 300,
|
|
58
|
+
tooltip: 50,
|
|
59
|
+
};
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Component
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
export const FacetDropdown = ({ field, placeholder = 'All Categories', className, theme: customTheme = {}, onChange, value: controlledValue, maxOptions = 10, showCounts = true, options: providedOptions, client: providedClient, applyFilter = true, navigateOnSelect = false, searchPageUrl = '/search', useFiltersApi = false, }) => {
|
|
64
|
+
const { client: contextClient, theme, stateManager } = useSearchContext();
|
|
65
|
+
const { addRefinement, removeRefinement, refinements, search } = useSearchState();
|
|
66
|
+
const client = providedClient || contextClient;
|
|
67
|
+
const [open, setOpen] = useState(false);
|
|
68
|
+
const [facetOptions, setFacetOptions] = useState(providedOptions || []);
|
|
69
|
+
const [internalValue, setInternalValue] = useState(controlledValue || '');
|
|
70
|
+
const [dropdownPosition, setDropdownPosition] = useState(null);
|
|
71
|
+
const dropdownRef = useRef(null);
|
|
72
|
+
const buttonRef = useRef(null);
|
|
73
|
+
// Determine the current value (controlled or uncontrolled)
|
|
74
|
+
const currentValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Fetch facet values
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const fetchFacetValues = useCallback(async () => {
|
|
79
|
+
if (providedOptions || !client)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
log.verbose('FacetDropdown: Fetching facet values', { field, useFiltersApi });
|
|
83
|
+
if (useFiltersApi) {
|
|
84
|
+
// Use dedicated Filters API via state manager
|
|
85
|
+
const response = await stateManager.fetchFilters({
|
|
86
|
+
facetBy: field,
|
|
87
|
+
maxFacetValues: 100,
|
|
88
|
+
});
|
|
89
|
+
const targetFilter = (response?.filters || []).find((f) => f.field === field);
|
|
90
|
+
if (targetFilter && targetFilter.values) {
|
|
91
|
+
const options = targetFilter.values.map((v) => ({
|
|
92
|
+
value: v.value || '',
|
|
93
|
+
count: v.count || 0,
|
|
94
|
+
label: v.value || '',
|
|
95
|
+
}));
|
|
96
|
+
setFacetOptions(options);
|
|
97
|
+
log.verbose('FacetDropdown: Facet options loaded via Filters API', { field, count: options.length });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
log.verbose('FacetDropdown: No filter data found', { field });
|
|
101
|
+
setFacetOptions([]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Fallback: use search API with per_page=0
|
|
106
|
+
const response = await client.search('*', {
|
|
107
|
+
facet_by: field,
|
|
108
|
+
max_facet_values: 100,
|
|
109
|
+
per_page: 0,
|
|
110
|
+
});
|
|
111
|
+
log.verbose('FacetDropdown: Search response', { response });
|
|
112
|
+
const facetsData = response?.data?.facets || response?.facets;
|
|
113
|
+
const facets = Array.isArray(facetsData) ? facetsData : [];
|
|
114
|
+
const targetFacet = facets.find((f) => f.field_name === field || f.field === field);
|
|
115
|
+
if (targetFacet && targetFacet.counts) {
|
|
116
|
+
const options = targetFacet.counts.map((count) => ({
|
|
117
|
+
value: count.value,
|
|
118
|
+
count: count.count,
|
|
119
|
+
label: count.value,
|
|
120
|
+
}));
|
|
121
|
+
setFacetOptions(options);
|
|
122
|
+
log.verbose('FacetDropdown: Facet options loaded', { field, count: options.length });
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
log.verbose('FacetDropdown: No facet data found', { field, facets });
|
|
126
|
+
setFacetOptions([]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
132
|
+
log.error('FacetDropdown: Error fetching facet values', {
|
|
133
|
+
field,
|
|
134
|
+
error: err.message,
|
|
135
|
+
});
|
|
136
|
+
setFacetOptions([]);
|
|
137
|
+
}
|
|
138
|
+
}, [field, client, providedOptions, useFiltersApi]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!providedOptions && client) {
|
|
141
|
+
fetchFacetValues();
|
|
142
|
+
}
|
|
143
|
+
}, [fetchFacetValues, providedOptions, client]);
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Calculate dropdown position when opened
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!open || !buttonRef.current)
|
|
149
|
+
return;
|
|
150
|
+
const updatePosition = () => {
|
|
151
|
+
if (!buttonRef.current)
|
|
152
|
+
return;
|
|
153
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
154
|
+
setDropdownPosition({
|
|
155
|
+
top: rect.bottom + window.scrollY + 4,
|
|
156
|
+
left: rect.left + window.scrollX,
|
|
157
|
+
width: rect.width,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
updatePosition();
|
|
161
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
162
|
+
window.addEventListener('resize', updatePosition);
|
|
163
|
+
return () => {
|
|
164
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
165
|
+
window.removeEventListener('resize', updatePosition);
|
|
166
|
+
};
|
|
167
|
+
}, [open]);
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Click outside handler
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!open)
|
|
173
|
+
return;
|
|
174
|
+
const handleClickOutside = (e) => {
|
|
175
|
+
if (dropdownRef.current &&
|
|
176
|
+
!dropdownRef.current.contains(e.target) &&
|
|
177
|
+
buttonRef.current &&
|
|
178
|
+
!buttonRef.current.contains(e.target)) {
|
|
179
|
+
setOpen(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const handleEscape = (e) => {
|
|
183
|
+
if (e.key === 'Escape') {
|
|
184
|
+
setOpen(false);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
188
|
+
document.addEventListener('keydown', handleEscape);
|
|
189
|
+
return () => {
|
|
190
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
191
|
+
document.removeEventListener('keydown', handleEscape);
|
|
192
|
+
};
|
|
193
|
+
}, [open]);
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Selection handler
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
const handleSelect = useCallback((value) => {
|
|
198
|
+
log.verbose('FacetDropdown: Value selected', { field, value });
|
|
199
|
+
// Update internal state
|
|
200
|
+
setInternalValue(value);
|
|
201
|
+
setOpen(false);
|
|
202
|
+
// Call onChange callback
|
|
203
|
+
if (onChange) {
|
|
204
|
+
onChange(value);
|
|
205
|
+
}
|
|
206
|
+
// Apply filter if enabled and we have search state
|
|
207
|
+
if (applyFilter && addRefinement && removeRefinement) {
|
|
208
|
+
// Remove any existing refinement for this field
|
|
209
|
+
const existingRefinement = refinements.find(r => r.field === field);
|
|
210
|
+
if (existingRefinement) {
|
|
211
|
+
removeRefinement(field, existingRefinement.value, false);
|
|
212
|
+
}
|
|
213
|
+
// Add new refinement if value is not empty
|
|
214
|
+
if (value) {
|
|
215
|
+
addRefinement(field, value, true); // true = trigger search
|
|
216
|
+
}
|
|
217
|
+
else if (search) {
|
|
218
|
+
// If clearing the filter, trigger search manually
|
|
219
|
+
search();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Navigate to search page if enabled
|
|
223
|
+
if (navigateOnSelect && value && typeof window !== 'undefined') {
|
|
224
|
+
const url = new URL(window.location.origin + searchPageUrl);
|
|
225
|
+
url.searchParams.set(field, value);
|
|
226
|
+
window.location.href = url.toString();
|
|
227
|
+
}
|
|
228
|
+
}, [field, onChange, applyFilter, addRefinement, removeRefinement, refinements, search, navigateOnSelect, searchPageUrl]);
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Render
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
const displayLabel = currentValue || placeholder;
|
|
233
|
+
const hasOptions = facetOptions.length > 0;
|
|
234
|
+
const buttonClass = [
|
|
235
|
+
'facet-dropdown-button',
|
|
236
|
+
customTheme.button,
|
|
237
|
+
className,
|
|
238
|
+
open && customTheme.buttonOpen,
|
|
239
|
+
]
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.join(' ');
|
|
242
|
+
// Render dropdown panel via portal
|
|
243
|
+
const renderDropdown = () => {
|
|
244
|
+
if (!open || !hasOptions || !dropdownPosition || typeof document === 'undefined') {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const dropdown = (React.createElement("div", { ref: dropdownRef, className: customTheme.panel || 'facet-dropdown-panel', role: "listbox", "aria-label": `${field} options`, style: {
|
|
248
|
+
position: 'absolute',
|
|
249
|
+
top: `${dropdownPosition.top}px`,
|
|
250
|
+
left: `${dropdownPosition.left}px`,
|
|
251
|
+
minWidth: `${dropdownPosition.width}px`,
|
|
252
|
+
width: 'max-content',
|
|
253
|
+
maxWidth: 'min(24rem, 90vw)',
|
|
254
|
+
maxHeight: `calc(100vh - 100px)`,
|
|
255
|
+
overflowY: 'auto',
|
|
256
|
+
overflowX: 'hidden',
|
|
257
|
+
backgroundColor: 'var(--facet-dropdown-bg, var(--background))',
|
|
258
|
+
border: '1px solid var(--facet-dropdown-border, var(--border-color))',
|
|
259
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
260
|
+
? theme.borderRadius.medium
|
|
261
|
+
: '0.5rem',
|
|
262
|
+
boxShadow: 'var(--facet-dropdown-shadow, var(--shadow-lg))',
|
|
263
|
+
zIndex: Z_INDEX.dropdown,
|
|
264
|
+
padding: '0.25rem',
|
|
265
|
+
} },
|
|
266
|
+
React.createElement("button", { type: "button", role: "option", "aria-selected": !currentValue, onClick: (e) => {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
e.stopPropagation();
|
|
269
|
+
handleSelect('');
|
|
270
|
+
}, className: [
|
|
271
|
+
customTheme.option,
|
|
272
|
+
!currentValue && customTheme.optionActive,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(' '), style: {
|
|
276
|
+
width: '100%',
|
|
277
|
+
textAlign: 'left',
|
|
278
|
+
padding: '0.5rem 0.75rem',
|
|
279
|
+
fontSize: 'var(--facet-dropdown-option-font-size, 0.875rem)',
|
|
280
|
+
fontWeight: !currentValue ? 'var(--facet-dropdown-option-font-weight-active, 500)' : 'normal',
|
|
281
|
+
color: 'var(--facet-dropdown-option-text, var(--text-color, currentColor))',
|
|
282
|
+
backgroundColor: !currentValue ? 'var(--facet-dropdown-option-active-bg, var(--hover-color))' : 'transparent',
|
|
283
|
+
border: 'none',
|
|
284
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
285
|
+
? theme.borderRadius.small
|
|
286
|
+
: '0.25rem',
|
|
287
|
+
cursor: 'pointer',
|
|
288
|
+
transition: 'background-color 0.15s ease',
|
|
289
|
+
display: 'flex',
|
|
290
|
+
alignItems: 'center',
|
|
291
|
+
justifyContent: 'space-between',
|
|
292
|
+
} },
|
|
293
|
+
React.createElement("span", null, placeholder)),
|
|
294
|
+
facetOptions.map((option) => {
|
|
295
|
+
const isActive = currentValue === option.value;
|
|
296
|
+
return (React.createElement("button", { key: option.value, type: "button", role: "option", "aria-selected": isActive, onClick: (e) => {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
handleSelect(option.value);
|
|
300
|
+
}, className: [
|
|
301
|
+
customTheme.option,
|
|
302
|
+
isActive && customTheme.optionActive,
|
|
303
|
+
]
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
.join(' '), style: {
|
|
306
|
+
width: '100%',
|
|
307
|
+
textAlign: 'left',
|
|
308
|
+
padding: '0.5rem 0.75rem',
|
|
309
|
+
fontSize: 'var(--facet-dropdown-option-font-size, 0.875rem)',
|
|
310
|
+
fontWeight: isActive ? 'var(--facet-dropdown-option-font-weight-active, 500)' : 'normal',
|
|
311
|
+
color: 'var(--facet-dropdown-option-text, var(--text-color, currentColor))',
|
|
312
|
+
backgroundColor: isActive ? 'var(--facet-dropdown-option-active-bg, var(--hover-color))' : 'transparent',
|
|
313
|
+
border: 'none',
|
|
314
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
315
|
+
? theme.borderRadius.small
|
|
316
|
+
: '0.25rem',
|
|
317
|
+
cursor: 'pointer',
|
|
318
|
+
transition: 'background-color 0.15s ease',
|
|
319
|
+
display: 'flex',
|
|
320
|
+
alignItems: 'center',
|
|
321
|
+
justifyContent: 'space-between',
|
|
322
|
+
} },
|
|
323
|
+
React.createElement("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, option.label || option.value),
|
|
324
|
+
showCounts && (React.createElement("span", { style: {
|
|
325
|
+
fontSize: 'var(--facet-dropdown-count-font-size, 0.75rem)',
|
|
326
|
+
color: 'var(--facet-dropdown-count-text, var(--text-secondary, currentColor))',
|
|
327
|
+
opacity: 0.7,
|
|
328
|
+
marginLeft: '0.5rem',
|
|
329
|
+
flexShrink: 0,
|
|
330
|
+
} },
|
|
331
|
+
"(",
|
|
332
|
+
option.count,
|
|
333
|
+
")"))));
|
|
334
|
+
})));
|
|
335
|
+
return createPortal(dropdown, document.body);
|
|
336
|
+
};
|
|
337
|
+
return (React.createElement("div", { className: "facet-dropdown-container", style: { position: 'relative' } },
|
|
338
|
+
React.createElement("style", { dangerouslySetInnerHTML: {
|
|
339
|
+
__html: `
|
|
340
|
+
.facet-dropdown-panel {
|
|
341
|
+
--scrollbar-track: transparent;
|
|
342
|
+
--scrollbar-thumb: var(--facet-dropdown-scrollbar-thumb, var(--border-color));
|
|
343
|
+
--scrollbar-thumb-hover: var(--facet-dropdown-scrollbar-thumb-hover, var(--text-secondary));
|
|
344
|
+
}
|
|
345
|
+
.facet-dropdown-panel::-webkit-scrollbar {
|
|
346
|
+
width: 6px;
|
|
347
|
+
}
|
|
348
|
+
.facet-dropdown-panel::-webkit-scrollbar-track {
|
|
349
|
+
background: var(--scrollbar-track);
|
|
350
|
+
}
|
|
351
|
+
.facet-dropdown-panel::-webkit-scrollbar-thumb {
|
|
352
|
+
background: var(--scrollbar-thumb);
|
|
353
|
+
border-radius: 3px;
|
|
354
|
+
}
|
|
355
|
+
.facet-dropdown-panel::-webkit-scrollbar-thumb:hover {
|
|
356
|
+
background: var(--scrollbar-thumb-hover);
|
|
357
|
+
}
|
|
358
|
+
.facet-dropdown-panel {
|
|
359
|
+
scrollbar-width: thin;
|
|
360
|
+
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
361
|
+
}
|
|
362
|
+
`
|
|
363
|
+
} }),
|
|
364
|
+
React.createElement("button", { ref: buttonRef, type: "button", onClick: (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
setOpen(prev => !prev);
|
|
368
|
+
}, className: buttonClass, "aria-label": `Filter by ${field}`, "aria-expanded": open, "aria-haspopup": "listbox", style: {
|
|
369
|
+
display: 'flex',
|
|
370
|
+
alignItems: 'center',
|
|
371
|
+
gap: '0.375rem',
|
|
372
|
+
padding: '0.625rem 0.75rem',
|
|
373
|
+
fontSize: 'var(--facet-dropdown-font-size, 0.875rem)',
|
|
374
|
+
fontWeight: 'var(--facet-dropdown-font-weight, 500)',
|
|
375
|
+
color: 'var(--facet-dropdown-text, var(--text-color, currentColor))',
|
|
376
|
+
backgroundColor: 'var(--facet-dropdown-button-bg, var(--background, transparent))',
|
|
377
|
+
border: '1px solid var(--facet-dropdown-border, var(--border-color))',
|
|
378
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
379
|
+
? theme.borderRadius.medium
|
|
380
|
+
: '0.5rem',
|
|
381
|
+
cursor: 'pointer',
|
|
382
|
+
transition: 'all 0.2s ease',
|
|
383
|
+
outline: 'none',
|
|
384
|
+
whiteSpace: 'nowrap',
|
|
385
|
+
minWidth: '8rem',
|
|
386
|
+
...(!hasOptions && { opacity: 0.6, cursor: 'not-allowed' }),
|
|
387
|
+
}, disabled: !hasOptions },
|
|
388
|
+
React.createElement("span", { style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' } }, displayLabel),
|
|
389
|
+
React.createElement("svg", { className: customTheme.caretIcon, width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
|
|
390
|
+
transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
391
|
+
transition: 'transform 0.2s ease',
|
|
392
|
+
flexShrink: 0,
|
|
393
|
+
}, "aria-hidden": "true" },
|
|
394
|
+
React.createElement("path", { d: "M4 6L8 10L12 6", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))),
|
|
395
|
+
renderDropdown()));
|
|
396
|
+
};
|