@seekora-ai/ui-sdk-react 0.2.13 → 0.2.15
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 +259 -47
- package/dist/components/FacetDropdown.d.ts +92 -0
- package/dist/components/FacetDropdown.d.ts.map +1 -0
- package/dist/components/FacetDropdown.js +374 -0
- package/dist/components/Facets.d.ts +56 -1
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +602 -41
- package/dist/components/FederatedDropdown.d.ts.map +1 -1
- package/dist/components/FederatedDropdown.js +45 -31
- package/dist/components/HierarchicalMenu.d.ts.map +1 -1
- package/dist/components/HierarchicalMenu.js +112 -4
- package/dist/components/Pagination.d.ts +47 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +166 -28
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
- package/dist/components/QuerySuggestionsDropdown.js +32 -18
- 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 +101 -32
- package/dist/components/RichQuerySuggestions.d.ts +7 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/RichQuerySuggestions.js +40 -26
- package/dist/components/SearchBar.d.ts +16 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +139 -17
- package/dist/components/SearchBarWithSuggestions.js +3 -3
- package/dist/components/SearchLayout.d.ts.map +1 -1
- package/dist/components/SearchLayout.js +10 -1
- package/dist/components/SearchProvider.d.ts +8 -1
- package/dist/components/SearchProvider.d.ts.map +1 -1
- package/dist/components/SearchProvider.js +16 -4
- package/dist/components/SearchResults.d.ts +10 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +46 -30
- 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/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 +11 -8
- package/dist/components/primitives/ImageZoom.js +26 -26
- package/dist/components/primitives/VariantSelector.js +10 -10
- package/dist/components/primitives/VariantSwatches.js +3 -3
- 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/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/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +2 -4
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +2 -6
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -4
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +20 -22
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +2 -6
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +39 -41
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -4
- package/dist/components/suggestions/utils.d.ts +10 -1
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +36 -0
- package/dist/components/suggestions-primitives/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 +21 -8
- package/dist/components/suggestions-primitives/ItemGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemGrid.js +9 -3
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCard.js +25 -10
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +24 -12
- package/dist/components/suggestions-primitives/SearchInput.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SearchInput.js +28 -9
- package/dist/components/suggestions-primitives/SuggestionItem.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionItem.js +3 -0
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
- package/dist/hooks/useClickTracking.d.ts +36 -0
- package/dist/hooks/useClickTracking.d.ts.map +1 -0
- package/dist/hooks/useClickTracking.js +96 -0
- package/dist/hooks/useExperiment.d.ts +25 -0
- package/dist/hooks/useExperiment.d.ts.map +1 -0
- package/dist/hooks/useExperiment.js +146 -0
- package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
- package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
- package/dist/hooks/useKeyboardNavigation.js +113 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestions.js +19 -3
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestionsEnhanced.js +23 -6
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +345 -19
- package/dist/src/index.esm.js +2869 -787
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2868 -785
- package/dist/src/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,21 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Facets Component
|
|
3
3
|
*
|
|
4
|
-
* Displays facet filters for search results
|
|
4
|
+
* Displays facet filters for search results with multiple display variants,
|
|
5
|
+
* client-side search, count badges, and color swatch support.
|
|
5
6
|
*/
|
|
6
|
-
import React, { useState } from 'react';
|
|
7
|
+
import React, { useState, useMemo } from 'react';
|
|
7
8
|
import { useSearchContext } from './SearchProvider';
|
|
8
9
|
import { useSearchState } from '../hooks/useSearchState';
|
|
9
10
|
import { log } from '@seekora-ai/ui-sdk-core';
|
|
10
11
|
import { clsx } from 'clsx';
|
|
11
|
-
|
|
12
|
+
import { RangeSlider } from './RangeSlider';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Generate a deterministic colour from a string (used as fallback for color-swatch). */
|
|
17
|
+
function stringToColor(str) {
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < str.length; i++) {
|
|
20
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
21
|
+
hash |= 0; // Convert to 32-bit int
|
|
22
|
+
}
|
|
23
|
+
const h = Math.abs(hash) % 360;
|
|
24
|
+
return `hsl(${h}, 65%, 55%)`;
|
|
25
|
+
}
|
|
26
|
+
/** Return swatch pixel size from the size prop. */
|
|
27
|
+
function swatchSize(size) {
|
|
28
|
+
switch (size) {
|
|
29
|
+
case 'small':
|
|
30
|
+
return 24;
|
|
31
|
+
case 'large':
|
|
32
|
+
return 40;
|
|
33
|
+
case 'medium':
|
|
34
|
+
default:
|
|
35
|
+
return 32;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Chevron SVG icon for collapsible variant
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
const ChevronIcon = ({ 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: {
|
|
42
|
+
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
43
|
+
transition: 'transform 200ms ease',
|
|
44
|
+
flexShrink: 0,
|
|
45
|
+
}, "aria-hidden": "true" },
|
|
46
|
+
React.createElement("path", { d: "M4 6L8 10L12 6", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Checkmark SVG for selected color swatches
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
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" },
|
|
51
|
+
React.createElement("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// CSS variable defaults
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
const CSS_VAR_DEFAULTS = {
|
|
56
|
+
'--seekora-facet-bg': 'transparent',
|
|
57
|
+
'--seekora-facet-border': '#dee2e6',
|
|
58
|
+
'--seekora-facet-active-bg': 'rgba(0, 0, 0, 0.05)',
|
|
59
|
+
'--seekora-facet-swatch-size': '32px',
|
|
60
|
+
'--seekora-facet-count-bg': '#e9ecef',
|
|
61
|
+
'--seekora-facet-count-color': '#495057',
|
|
62
|
+
'--seekora-primary': '#3b82f6',
|
|
63
|
+
'--seekora-primary-text': '#ffffff',
|
|
64
|
+
};
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Component
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
export 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', facetRanges, }) => {
|
|
12
69
|
const { theme } = useSearchContext();
|
|
13
70
|
const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
|
|
14
71
|
const facetsTheme = customTheme || {};
|
|
72
|
+
// expandedFacets is used for "Show more/less" in checkbox/color-swatch variants
|
|
73
|
+
// AND for collapse/expand in collapsible variant.
|
|
15
74
|
const [expandedFacets, setExpandedFacets] = useState({});
|
|
75
|
+
const [searchTerms, setSearchTerms] = useState({});
|
|
16
76
|
// Use results from prop if provided, otherwise from state manager
|
|
17
77
|
const results = resultsProp || stateResults;
|
|
78
|
+
// -------------------------------------------------------------------
|
|
18
79
|
// Extract facets from results
|
|
80
|
+
// -------------------------------------------------------------------
|
|
19
81
|
const extractFacets = () => {
|
|
20
82
|
if (facetsProp)
|
|
21
83
|
return facetsProp;
|
|
@@ -53,6 +115,7 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
53
115
|
count: count.count,
|
|
54
116
|
selected: refinements.some(r => r.field === fieldName && r.value === count.value),
|
|
55
117
|
})) : [],
|
|
118
|
+
stats: facet.stats || undefined,
|
|
56
119
|
};
|
|
57
120
|
});
|
|
58
121
|
log.verbose('Facets: Extracted facets', {
|
|
@@ -62,6 +125,9 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
62
125
|
return extracted;
|
|
63
126
|
};
|
|
64
127
|
const facets = extractFacets();
|
|
128
|
+
// -------------------------------------------------------------------
|
|
129
|
+
// Handlers
|
|
130
|
+
// -------------------------------------------------------------------
|
|
65
131
|
const handleFacetToggle = (field, value, selected) => {
|
|
66
132
|
const newSelected = !selected;
|
|
67
133
|
log.verbose('Facets: Facet toggled', { field, value, selected: newSelected });
|
|
@@ -94,49 +160,260 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
94
160
|
[field]: !prev[field],
|
|
95
161
|
}));
|
|
96
162
|
};
|
|
163
|
+
/** For collapsible variant — determine if a facet group is open. */
|
|
164
|
+
const isFacetGroupOpen = (field) => {
|
|
165
|
+
if (field in expandedFacets) {
|
|
166
|
+
return expandedFacets[field];
|
|
167
|
+
}
|
|
168
|
+
// Default based on defaultCollapsed prop
|
|
169
|
+
return !defaultCollapsed;
|
|
170
|
+
};
|
|
171
|
+
const toggleCollapsible = (field) => {
|
|
172
|
+
setExpandedFacets((prev) => ({
|
|
173
|
+
...prev,
|
|
174
|
+
[field]: !(prev[field] ?? !defaultCollapsed),
|
|
175
|
+
}));
|
|
176
|
+
};
|
|
177
|
+
const getSearchTerm = (field) => searchTerms[field] || '';
|
|
178
|
+
const setSearchTerm = (field, term) => {
|
|
179
|
+
setSearchTerms((prev) => ({ ...prev, [field]: term }));
|
|
180
|
+
};
|
|
181
|
+
/** Filter facet items by search term. */
|
|
182
|
+
const filterItems = (items, field) => {
|
|
183
|
+
if (!searchable)
|
|
184
|
+
return items;
|
|
185
|
+
const term = getSearchTerm(field).toLowerCase();
|
|
186
|
+
if (!term)
|
|
187
|
+
return items;
|
|
188
|
+
return items.filter((item) => item.value.toLowerCase().includes(term));
|
|
189
|
+
};
|
|
190
|
+
// -------------------------------------------------------------------
|
|
191
|
+
// Size helpers
|
|
192
|
+
// -------------------------------------------------------------------
|
|
193
|
+
const sizeScale = useMemo(() => {
|
|
194
|
+
switch (size) {
|
|
195
|
+
case 'small':
|
|
196
|
+
return { font: theme.typography.fontSize.small, padding: '0.5rem', gap: '0.5rem' };
|
|
197
|
+
case 'large':
|
|
198
|
+
return { font: theme.typography.fontSize.large, padding: '0.75rem', gap: '0.75rem' };
|
|
199
|
+
case 'medium':
|
|
200
|
+
default:
|
|
201
|
+
return { font: theme.typography.fontSize.medium, padding: theme.spacing.small, gap: theme.spacing.small };
|
|
202
|
+
}
|
|
203
|
+
}, [size, theme]);
|
|
204
|
+
// -------------------------------------------------------------------
|
|
205
|
+
// Count badge renderer
|
|
206
|
+
// -------------------------------------------------------------------
|
|
207
|
+
const renderCountBadge = (count) => {
|
|
208
|
+
if (!showCounts)
|
|
209
|
+
return null;
|
|
210
|
+
return (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
211
|
+
display: 'inline-flex',
|
|
212
|
+
alignItems: 'center',
|
|
213
|
+
justifyContent: 'center',
|
|
214
|
+
minWidth: '1.5em',
|
|
215
|
+
padding: '0.1em 0.5em',
|
|
216
|
+
marginLeft: sizeScale.gap,
|
|
217
|
+
fontSize: theme.typography.fontSize.small,
|
|
218
|
+
fontWeight: theme.typography.fontWeight?.medium ?? 500,
|
|
219
|
+
lineHeight: 1,
|
|
220
|
+
color: 'var(--seekora-facet-count-color, #495057)',
|
|
221
|
+
backgroundColor: 'var(--seekora-facet-count-bg, #e9ecef)',
|
|
222
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.full,
|
|
223
|
+
flexShrink: 0,
|
|
224
|
+
} }, count));
|
|
225
|
+
};
|
|
226
|
+
// -------------------------------------------------------------------
|
|
227
|
+
// Search input renderer
|
|
228
|
+
// -------------------------------------------------------------------
|
|
229
|
+
const renderSearchInput = (facet) => {
|
|
230
|
+
if (!searchable)
|
|
231
|
+
return null;
|
|
232
|
+
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: {
|
|
233
|
+
width: '100%',
|
|
234
|
+
boxSizing: 'border-box',
|
|
235
|
+
padding: sizeScale.padding,
|
|
236
|
+
marginBottom: sizeScale.gap,
|
|
237
|
+
fontSize: theme.typography.fontSize.small,
|
|
238
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
239
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.small,
|
|
240
|
+
outline: 'none',
|
|
241
|
+
color: theme.colors.text,
|
|
242
|
+
backgroundColor: 'var(--seekora-facet-bg, transparent)',
|
|
243
|
+
} }));
|
|
244
|
+
};
|
|
245
|
+
// -------------------------------------------------------------------
|
|
246
|
+
// Checkbox variant item renderer (original behaviour, preserved)
|
|
247
|
+
// -------------------------------------------------------------------
|
|
97
248
|
const defaultRenderFacetItem = (item, facet, index) => {
|
|
98
249
|
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
99
250
|
if (!isExpanded && index >= maxItems) {
|
|
100
251
|
return null;
|
|
101
252
|
}
|
|
102
|
-
|
|
253
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
254
|
+
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: {
|
|
103
255
|
display: 'flex',
|
|
104
256
|
alignItems: 'center',
|
|
105
|
-
padding:
|
|
257
|
+
padding: sizeScale.padding,
|
|
106
258
|
cursor: 'pointer',
|
|
107
259
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
108
|
-
marginBottom:
|
|
109
|
-
backgroundColor:
|
|
260
|
+
marginBottom: sizeScale.gap,
|
|
261
|
+
backgroundColor: isChecked
|
|
262
|
+
? 'var(--seekora-facet-active-bg, ' + theme.colors.hover + ')'
|
|
263
|
+
: 'transparent',
|
|
110
264
|
transition: 'background-color 0.2s ease',
|
|
265
|
+
boxSizing: 'border-box',
|
|
111
266
|
} },
|
|
112
267
|
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: {
|
|
113
|
-
marginRight:
|
|
268
|
+
marginRight: sizeScale.gap,
|
|
114
269
|
cursor: 'pointer',
|
|
115
270
|
}, "aria-label": `Filter by ${item.value}` }),
|
|
116
271
|
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
117
272
|
flex: 1,
|
|
118
|
-
fontSize:
|
|
273
|
+
fontSize: sizeScale.font,
|
|
274
|
+
lineHeight: 1.5,
|
|
119
275
|
color: theme.colors.text,
|
|
120
276
|
} }, item.value),
|
|
121
|
-
|
|
277
|
+
renderCountBadge(item.count)));
|
|
278
|
+
};
|
|
279
|
+
// -------------------------------------------------------------------
|
|
280
|
+
// Color swatch variant item renderer
|
|
281
|
+
// -------------------------------------------------------------------
|
|
282
|
+
const renderColorSwatchItem = (item, facet, index) => {
|
|
283
|
+
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
284
|
+
if (!isExpanded && index >= maxItems) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
288
|
+
const color = colorMap?.[item.value] ?? stringToColor(item.value);
|
|
289
|
+
const pxSize = swatchSize(size);
|
|
290
|
+
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: {
|
|
291
|
+
display: 'inline-flex',
|
|
292
|
+
flexDirection: 'column',
|
|
293
|
+
alignItems: 'center',
|
|
294
|
+
cursor: 'pointer',
|
|
295
|
+
margin: sizeScale.gap,
|
|
296
|
+
} },
|
|
297
|
+
React.createElement("div", { className: clsx(facetsTheme.colorSwatch, isChecked && facetsTheme.colorSwatchSelected), style: {
|
|
298
|
+
width: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
299
|
+
height: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
300
|
+
borderRadius: '50%',
|
|
301
|
+
backgroundColor: color,
|
|
302
|
+
border: isChecked
|
|
303
|
+
? `3px solid var(--seekora-primary, ${theme.colors.primary})`
|
|
304
|
+
: `2px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
305
|
+
display: 'flex',
|
|
306
|
+
alignItems: 'center',
|
|
307
|
+
justifyContent: 'center',
|
|
308
|
+
transition: 'border 0.2s ease, box-shadow 0.2s ease',
|
|
309
|
+
boxShadow: isChecked ? `0 0 0 2px var(--seekora-primary-alpha, ${theme.colors.primary}33)` : 'none',
|
|
310
|
+
position: 'relative',
|
|
311
|
+
} }, isChecked && (React.createElement("span", { className: clsx(facetsTheme.colorSwatchInner), style: {
|
|
312
|
+
display: 'flex',
|
|
313
|
+
alignItems: 'center',
|
|
314
|
+
justifyContent: 'center',
|
|
315
|
+
} },
|
|
316
|
+
React.createElement(CheckmarkIcon, { size: Math.round(pxSize * 0.5) })))),
|
|
317
|
+
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
122
318
|
fontSize: theme.typography.fontSize.small,
|
|
123
|
-
color: theme.colors.
|
|
124
|
-
|
|
125
|
-
|
|
319
|
+
color: theme.colors.text,
|
|
320
|
+
marginTop: '0.25rem',
|
|
321
|
+
textAlign: 'center',
|
|
322
|
+
maxWidth: `${pxSize + 16}px`,
|
|
323
|
+
overflow: 'hidden',
|
|
324
|
+
textOverflow: 'ellipsis',
|
|
325
|
+
whiteSpace: 'nowrap',
|
|
326
|
+
} }, item.value),
|
|
327
|
+
showCounts && (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
328
|
+
fontSize: theme.typography.fontSize.small,
|
|
329
|
+
color: 'var(--seekora-facet-count-color, ' + (theme.colors.textSecondary || theme.colors.text) + ')',
|
|
330
|
+
lineHeight: 1,
|
|
331
|
+
marginTop: '0.125rem',
|
|
332
|
+
} }, item.count))));
|
|
333
|
+
};
|
|
334
|
+
// -------------------------------------------------------------------
|
|
335
|
+
// Item renderer dispatcher
|
|
336
|
+
// -------------------------------------------------------------------
|
|
337
|
+
const renderItem = (item, facet, index) => {
|
|
338
|
+
if (renderFacetItem) {
|
|
339
|
+
return renderFacetItem(item, facet, index);
|
|
340
|
+
}
|
|
341
|
+
switch (variant) {
|
|
342
|
+
case 'color-swatch':
|
|
343
|
+
return renderColorSwatchItem(item, facet, index);
|
|
344
|
+
case 'collapsible':
|
|
345
|
+
case 'checkbox':
|
|
346
|
+
default:
|
|
347
|
+
return defaultRenderFacetItem(item, facet, index);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
// -------------------------------------------------------------------
|
|
351
|
+
// Keyboard handler (shared across variants)
|
|
352
|
+
// -------------------------------------------------------------------
|
|
353
|
+
const handleListKeyDown = (e, visibleItems, facet) => {
|
|
354
|
+
const currentEl = e.currentTarget.querySelector('[aria-selected="true"]');
|
|
355
|
+
const allItems = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
356
|
+
const currentIdx = currentEl ? allItems.indexOf(currentEl) : -1;
|
|
357
|
+
if (e.key === 'ArrowDown') {
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
const next = Math.min(currentIdx + 1, allItems.length - 1);
|
|
360
|
+
allItems[next]?.focus();
|
|
361
|
+
}
|
|
362
|
+
else if (e.key === 'ArrowUp') {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
const prev = Math.max(currentIdx - 1, 0);
|
|
365
|
+
allItems[prev]?.focus();
|
|
366
|
+
}
|
|
367
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
if (currentIdx >= 0 && currentIdx < visibleItems.length) {
|
|
370
|
+
handleFacetToggle(facet.field, visibleItems[currentIdx].value, visibleItems[currentIdx].selected || false);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
// -------------------------------------------------------------------
|
|
375
|
+
// Show more / less buttons (shared)
|
|
376
|
+
// -------------------------------------------------------------------
|
|
377
|
+
const renderShowMoreLess = (facet, filteredItems) => {
|
|
378
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
379
|
+
const hasMore = filteredItems.length > maxItems;
|
|
380
|
+
return (React.createElement(React.Fragment, null,
|
|
381
|
+
showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
382
|
+
marginTop: sizeScale.gap,
|
|
383
|
+
padding: sizeScale.padding,
|
|
384
|
+
border: 'none',
|
|
385
|
+
backgroundColor: 'transparent',
|
|
386
|
+
color: theme.colors.primary,
|
|
387
|
+
cursor: 'pointer',
|
|
388
|
+
fontSize: theme.typography.fontSize.small,
|
|
389
|
+
textDecoration: 'underline',
|
|
126
390
|
} },
|
|
127
|
-
"(",
|
|
128
|
-
|
|
129
|
-
")"))
|
|
391
|
+
"Show more (",
|
|
392
|
+
filteredItems.length - maxItems,
|
|
393
|
+
" more)")),
|
|
394
|
+
isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
395
|
+
marginTop: sizeScale.gap,
|
|
396
|
+
padding: sizeScale.padding,
|
|
397
|
+
border: 'none',
|
|
398
|
+
backgroundColor: 'transparent',
|
|
399
|
+
color: theme.colors.primary,
|
|
400
|
+
cursor: 'pointer',
|
|
401
|
+
fontSize: theme.typography.fontSize.small,
|
|
402
|
+
textDecoration: 'underline',
|
|
403
|
+
} }, "Show less"))));
|
|
130
404
|
};
|
|
131
|
-
|
|
405
|
+
// -------------------------------------------------------------------
|
|
406
|
+
// Default facet group renderer — Checkbox variant
|
|
407
|
+
// -------------------------------------------------------------------
|
|
408
|
+
const renderCheckboxFacet = (facet, _index) => {
|
|
409
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
132
410
|
const isExpanded = expandedFacets[facet.field] || false;
|
|
133
|
-
const visibleItems = isExpanded ?
|
|
134
|
-
const hasMore = facet.items.length > maxItems;
|
|
411
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
135
412
|
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
136
413
|
marginBottom: theme.spacing.large,
|
|
137
414
|
padding: theme.spacing.medium,
|
|
138
|
-
backgroundColor: theme.colors.background,
|
|
139
|
-
border: `1px solid ${theme.colors.border}`,
|
|
415
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
416
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
140
417
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
141
418
|
} },
|
|
142
419
|
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
@@ -146,36 +423,314 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
146
423
|
marginBottom: theme.spacing.medium,
|
|
147
424
|
color: theme.colors.text,
|
|
148
425
|
} }, facet.label || facet.field),
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
426
|
+
renderSearchInput(facet),
|
|
427
|
+
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))),
|
|
428
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
429
|
+
};
|
|
430
|
+
// -------------------------------------------------------------------
|
|
431
|
+
// Color-swatch facet group renderer
|
|
432
|
+
// -------------------------------------------------------------------
|
|
433
|
+
const renderColorSwatchFacet = (facet, _index) => {
|
|
434
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
435
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
436
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
437
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
438
|
+
marginBottom: theme.spacing.large,
|
|
439
|
+
padding: theme.spacing.medium,
|
|
440
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
441
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
442
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
443
|
+
} },
|
|
444
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
445
|
+
fontSize: theme.typography.fontSize.large,
|
|
446
|
+
fontWeight: 'bold',
|
|
447
|
+
margin: 0,
|
|
448
|
+
marginBottom: theme.spacing.medium,
|
|
449
|
+
color: theme.colors.text,
|
|
450
|
+
} }, facet.label || facet.field),
|
|
451
|
+
renderSearchInput(facet),
|
|
452
|
+
React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet), style: {
|
|
453
|
+
display: 'flex',
|
|
454
|
+
flexWrap: 'wrap',
|
|
455
|
+
gap: sizeScale.gap,
|
|
456
|
+
} }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
|
|
457
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
458
|
+
};
|
|
459
|
+
// -------------------------------------------------------------------
|
|
460
|
+
// Collapsible facet group renderer
|
|
461
|
+
// -------------------------------------------------------------------
|
|
462
|
+
const renderCollapsibleFacet = (facet, _index) => {
|
|
463
|
+
const isOpen = isFacetGroupOpen(facet.field);
|
|
464
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
465
|
+
const isItemsExpanded = expandedFacets[facet.field] || false;
|
|
466
|
+
// Note: For collapsible, the expandedFacets state controls the collapse/expand
|
|
467
|
+
// of the group itself. We use a separate concept for Show more/less within items.
|
|
468
|
+
// To avoid collision, Show more/less for collapsible uses the same expandedFacets
|
|
469
|
+
// key prefixed with `_items_`.
|
|
470
|
+
const isShowMoreExpanded = expandedFacets[`_items_${facet.field}`] || false;
|
|
471
|
+
const visibleItems = isShowMoreExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
472
|
+
const hasMore = filteredItems.length > maxItems;
|
|
473
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
474
|
+
marginBottom: theme.spacing.large,
|
|
475
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
476
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
477
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
478
|
+
overflow: 'hidden',
|
|
479
|
+
} },
|
|
480
|
+
React.createElement("button", { type: "button", className: clsx(facetsTheme.collapsibleHeader), onClick: () => toggleCollapsible(facet.field), "aria-expanded": isOpen, "aria-controls": `facet-group-${facet.field}`, style: {
|
|
481
|
+
display: 'flex',
|
|
482
|
+
alignItems: 'center',
|
|
483
|
+
justifyContent: 'space-between',
|
|
484
|
+
width: '100%',
|
|
485
|
+
padding: theme.spacing.medium,
|
|
158
486
|
border: 'none',
|
|
159
487
|
backgroundColor: 'transparent',
|
|
160
|
-
color: theme.colors.primary,
|
|
161
488
|
cursor: 'pointer',
|
|
162
|
-
|
|
163
|
-
textDecoration: 'underline',
|
|
489
|
+
textAlign: 'left',
|
|
164
490
|
} },
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
491
|
+
React.createElement("span", { className: facetsTheme.facetTitle, style: {
|
|
492
|
+
fontSize: theme.typography.fontSize.large,
|
|
493
|
+
fontWeight: 'bold',
|
|
494
|
+
color: theme.colors.text,
|
|
495
|
+
flex: 1,
|
|
496
|
+
} }, facet.label || facet.field),
|
|
497
|
+
React.createElement("span", { className: clsx(facetsTheme.collapsibleIcon) },
|
|
498
|
+
React.createElement(ChevronIcon, { expanded: isOpen, color: theme.colors.textSecondary || theme.colors.text }))),
|
|
499
|
+
isOpen && (React.createElement("div", { id: `facet-group-${facet.field}`, style: {
|
|
500
|
+
padding: `0 ${theme.spacing.medium} ${theme.spacing.medium}`,
|
|
501
|
+
} },
|
|
502
|
+
renderSearchInput(facet),
|
|
503
|
+
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))),
|
|
504
|
+
showMore && hasMore && !isShowMoreExpanded && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
505
|
+
...prev,
|
|
506
|
+
[`_items_${facet.field}`]: true,
|
|
507
|
+
})), style: {
|
|
508
|
+
marginTop: sizeScale.gap,
|
|
509
|
+
padding: sizeScale.padding,
|
|
510
|
+
border: 'none',
|
|
511
|
+
backgroundColor: 'transparent',
|
|
512
|
+
color: theme.colors.primary,
|
|
513
|
+
cursor: 'pointer',
|
|
514
|
+
fontSize: theme.typography.fontSize.small,
|
|
515
|
+
textDecoration: 'underline',
|
|
516
|
+
} },
|
|
517
|
+
"Show more (",
|
|
518
|
+
filteredItems.length - maxItems,
|
|
519
|
+
" more)")),
|
|
520
|
+
isShowMoreExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
521
|
+
...prev,
|
|
522
|
+
[`_items_${facet.field}`]: false,
|
|
523
|
+
})), style: {
|
|
524
|
+
marginTop: sizeScale.gap,
|
|
525
|
+
padding: sizeScale.padding,
|
|
526
|
+
border: 'none',
|
|
527
|
+
backgroundColor: 'transparent',
|
|
528
|
+
color: theme.colors.primary,
|
|
529
|
+
cursor: 'pointer',
|
|
530
|
+
fontSize: theme.typography.fontSize.small,
|
|
531
|
+
textDecoration: 'underline',
|
|
532
|
+
} }, "Show less"))))));
|
|
533
|
+
};
|
|
534
|
+
// -------------------------------------------------------------------
|
|
535
|
+
// Render-type detection for numeric / range facets
|
|
536
|
+
// -------------------------------------------------------------------
|
|
537
|
+
const determineFacetRenderType = (fieldName, stats) => {
|
|
538
|
+
// If explicit range config exists for this field → range buttons
|
|
539
|
+
if (facetRanges?.some((rc) => rc.field === fieldName)) {
|
|
540
|
+
return 'range-buttons';
|
|
541
|
+
}
|
|
542
|
+
// If stats with valid min/max → range slider
|
|
543
|
+
if (stats && typeof stats.min === 'number' && typeof stats.max === 'number' && stats.min !== stats.max) {
|
|
544
|
+
return 'range-slider';
|
|
545
|
+
}
|
|
546
|
+
return 'list';
|
|
547
|
+
};
|
|
548
|
+
// -------------------------------------------------------------------
|
|
549
|
+
// Range button facet renderer
|
|
550
|
+
// -------------------------------------------------------------------
|
|
551
|
+
const renderRangeButtonFacet = (facet, _index) => {
|
|
552
|
+
const rangeConfig = facetRanges?.find((rc) => rc.field === facet.field);
|
|
553
|
+
if (!rangeConfig)
|
|
554
|
+
return null;
|
|
555
|
+
// Build a lookup from item value → count
|
|
556
|
+
const countMap = new Map();
|
|
557
|
+
facet.items.forEach((item) => countMap.set(item.value, item.count));
|
|
558
|
+
// Detect currently active range from refinements
|
|
559
|
+
const activeMin = refinements.find((r) => r.field === facet.field && r.value.startsWith('>='));
|
|
560
|
+
const activeMax = refinements.find((r) => r.field === facet.field && r.value.startsWith('<='));
|
|
561
|
+
const activeFromVal = activeMin ? parseFloat(activeMin.value.slice(2)) : undefined;
|
|
562
|
+
const activeToVal = activeMax ? parseFloat(activeMax.value.slice(2)) : undefined;
|
|
563
|
+
const isRangeActive = (range) => {
|
|
564
|
+
const fromMatch = range.from === undefined
|
|
565
|
+
? activeFromVal === undefined
|
|
566
|
+
: activeFromVal === range.from;
|
|
567
|
+
const toMatch = range.to === undefined
|
|
568
|
+
? activeToVal === undefined
|
|
569
|
+
: activeToVal === range.to;
|
|
570
|
+
return fromMatch && toMatch;
|
|
571
|
+
};
|
|
572
|
+
const handleRangeClick = (range) => {
|
|
573
|
+
// Clear existing range refinements for this field
|
|
574
|
+
refinements
|
|
575
|
+
.filter((r) => r.field === facet.field)
|
|
576
|
+
.forEach((r) => removeRefinement(facet.field, r.value, false));
|
|
577
|
+
if (isRangeActive(range)) {
|
|
578
|
+
// Was active → just clear (already removed above), trigger search
|
|
579
|
+
addRefinement(facet.field, '__noop__', false);
|
|
580
|
+
removeRefinement(facet.field, '__noop__', true);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// Set new range
|
|
584
|
+
if (range.from !== undefined) {
|
|
585
|
+
addRefinement(facet.field, `>=${range.from}`, false);
|
|
586
|
+
}
|
|
587
|
+
if (range.to !== undefined) {
|
|
588
|
+
addRefinement(facet.field, `<=${range.to}`, true);
|
|
589
|
+
}
|
|
590
|
+
else if (range.from !== undefined) {
|
|
591
|
+
// Only "from" set — trigger search
|
|
592
|
+
addRefinement(facet.field, `>=${range.from}`, true);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const hasActiveRange = activeMin !== undefined || activeMax !== undefined;
|
|
596
|
+
const handleClear = () => {
|
|
597
|
+
refinements
|
|
598
|
+
.filter((r) => r.field === facet.field)
|
|
599
|
+
.forEach((r) => removeRefinement(facet.field, r.value, false));
|
|
600
|
+
// Trigger search after clearing
|
|
601
|
+
addRefinement(facet.field, '__noop__', false);
|
|
602
|
+
removeRefinement(facet.field, '__noop__', true);
|
|
603
|
+
};
|
|
604
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
605
|
+
marginBottom: theme.spacing.large,
|
|
606
|
+
padding: theme.spacing.medium,
|
|
607
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
608
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
609
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
610
|
+
} },
|
|
611
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
612
|
+
fontSize: theme.typography.fontSize.large,
|
|
613
|
+
fontWeight: 'bold',
|
|
614
|
+
margin: 0,
|
|
615
|
+
marginBottom: theme.spacing.medium,
|
|
616
|
+
color: theme.colors.text,
|
|
617
|
+
} }, facet.label || facet.field),
|
|
618
|
+
React.createElement("div", { style: {
|
|
619
|
+
display: 'flex',
|
|
620
|
+
flexWrap: 'wrap',
|
|
621
|
+
gap: sizeScale.gap,
|
|
622
|
+
} }, rangeConfig.ranges.map((range) => {
|
|
623
|
+
const count = countMap.get(range.label) ?? 0;
|
|
624
|
+
const active = isRangeActive(range);
|
|
625
|
+
return (React.createElement("button", { key: range.label, type: "button", className: clsx(facetsTheme.rangeButton, active && facetsTheme.rangeButtonActive), disabled: count === 0 && !active, onClick: () => handleRangeClick(range), style: {
|
|
626
|
+
display: 'inline-flex',
|
|
627
|
+
alignItems: 'center',
|
|
628
|
+
gap: '0.35em',
|
|
629
|
+
padding: `${sizeScale.padding} 0.75em`,
|
|
630
|
+
fontSize: sizeScale.font,
|
|
631
|
+
border: `1px solid ${active ? 'var(--seekora-primary, ' + theme.colors.primary + ')' : 'var(--seekora-facet-border, ' + theme.colors.border + ')'}`,
|
|
632
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.full,
|
|
633
|
+
backgroundColor: active ? 'var(--seekora-primary, ' + theme.colors.primary + ')' : 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
634
|
+
color: active ? 'var(--seekora-primary-text, #fff)' : theme.colors.text,
|
|
635
|
+
cursor: count === 0 && !active ? 'not-allowed' : 'pointer',
|
|
636
|
+
opacity: count === 0 && !active ? 0.5 : 1,
|
|
637
|
+
transition: 'all 0.2s ease',
|
|
638
|
+
} },
|
|
639
|
+
range.label,
|
|
640
|
+
showCounts && (React.createElement("span", { className: clsx(facetsTheme.rangeButtonCount), style: {
|
|
641
|
+
fontSize: theme.typography.fontSize.small,
|
|
642
|
+
opacity: 0.8,
|
|
643
|
+
} },
|
|
644
|
+
"(",
|
|
645
|
+
count,
|
|
646
|
+
")"))));
|
|
647
|
+
})),
|
|
648
|
+
hasActiveRange && (React.createElement("button", { type: "button", className: clsx(facetsTheme.rangeClear), onClick: handleClear, style: {
|
|
649
|
+
marginTop: sizeScale.gap,
|
|
650
|
+
padding: sizeScale.padding,
|
|
171
651
|
border: 'none',
|
|
172
652
|
backgroundColor: 'transparent',
|
|
173
653
|
color: theme.colors.primary,
|
|
174
654
|
cursor: 'pointer',
|
|
175
655
|
fontSize: theme.typography.fontSize.small,
|
|
176
656
|
textDecoration: 'underline',
|
|
177
|
-
} }, "
|
|
657
|
+
} }, "Clear"))));
|
|
658
|
+
};
|
|
659
|
+
// -------------------------------------------------------------------
|
|
660
|
+
// Range slider facet renderer
|
|
661
|
+
// -------------------------------------------------------------------
|
|
662
|
+
const renderRangeSliderFacet = (facet, _index) => {
|
|
663
|
+
if (!facet.stats)
|
|
664
|
+
return null;
|
|
665
|
+
const { min: statsMin, max: statsMax } = facet.stats;
|
|
666
|
+
// Auto-detect formatting for common field names
|
|
667
|
+
const fieldLower = facet.field.toLowerCase();
|
|
668
|
+
const isPriceField = fieldLower.includes('price') || fieldLower.includes('cost') || fieldLower.includes('amount');
|
|
669
|
+
const formatValue = isPriceField
|
|
670
|
+
? (v) => `$${v.toFixed(2)}`
|
|
671
|
+
: (v) => v.toString();
|
|
672
|
+
// Determine step: use 0.01 for price, 1 for integer ranges, 0.1 otherwise
|
|
673
|
+
const range = statsMax - statsMin;
|
|
674
|
+
const step = isPriceField ? 0.01 : range > 10 ? 1 : 0.1;
|
|
675
|
+
// Check if there's an active range refinement
|
|
676
|
+
const hasActiveRange = refinements.some((r) => r.field === facet.field && (r.value.startsWith('>=') || r.value.startsWith('<=')));
|
|
677
|
+
const handleClear = () => {
|
|
678
|
+
refinements
|
|
679
|
+
.filter((r) => r.field === facet.field && (r.value.startsWith('>=') || r.value.startsWith('<=')))
|
|
680
|
+
.forEach((r) => removeRefinement(facet.field, r.value, false));
|
|
681
|
+
// Trigger search after clearing
|
|
682
|
+
addRefinement(facet.field, '__noop__', false);
|
|
683
|
+
removeRefinement(facet.field, '__noop__', true);
|
|
684
|
+
};
|
|
685
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
686
|
+
marginBottom: theme.spacing.large,
|
|
687
|
+
padding: theme.spacing.medium,
|
|
688
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
689
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
690
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
691
|
+
} },
|
|
692
|
+
React.createElement("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.spacing.small } },
|
|
693
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
694
|
+
fontSize: theme.typography.fontSize.large,
|
|
695
|
+
fontWeight: 'bold',
|
|
696
|
+
margin: 0,
|
|
697
|
+
color: theme.colors.text,
|
|
698
|
+
} }, facet.label || facet.field),
|
|
699
|
+
hasActiveRange && (React.createElement("button", { type: "button", className: clsx(facetsTheme.rangeClear), onClick: handleClear, style: {
|
|
700
|
+
padding: '0.15em 0.5em',
|
|
701
|
+
border: 'none',
|
|
702
|
+
backgroundColor: 'transparent',
|
|
703
|
+
color: theme.colors.primary,
|
|
704
|
+
cursor: 'pointer',
|
|
705
|
+
fontSize: theme.typography.fontSize.small,
|
|
706
|
+
textDecoration: 'underline',
|
|
707
|
+
} }, "Clear"))),
|
|
708
|
+
React.createElement(RangeSlider, { field: facet.field, min: statsMin, max: statsMax, step: step, formatValue: formatValue, syncWithState: true })));
|
|
709
|
+
};
|
|
710
|
+
// -------------------------------------------------------------------
|
|
711
|
+
// Default facet renderer dispatcher
|
|
712
|
+
// -------------------------------------------------------------------
|
|
713
|
+
const defaultRenderFacet = (facet, index) => {
|
|
714
|
+
const renderType = determineFacetRenderType(facet.field, facet.stats);
|
|
715
|
+
if (renderType === 'range-buttons') {
|
|
716
|
+
return renderRangeButtonFacet(facet, index);
|
|
717
|
+
}
|
|
718
|
+
if (renderType === 'range-slider') {
|
|
719
|
+
return renderRangeSliderFacet(facet, index);
|
|
720
|
+
}
|
|
721
|
+
switch (variant) {
|
|
722
|
+
case 'color-swatch':
|
|
723
|
+
return renderColorSwatchFacet(facet, index);
|
|
724
|
+
case 'collapsible':
|
|
725
|
+
return renderCollapsibleFacet(facet, index);
|
|
726
|
+
case 'checkbox':
|
|
727
|
+
default:
|
|
728
|
+
return renderCheckboxFacet(facet, index);
|
|
729
|
+
}
|
|
178
730
|
};
|
|
731
|
+
// -------------------------------------------------------------------
|
|
732
|
+
// Empty state
|
|
733
|
+
// -------------------------------------------------------------------
|
|
179
734
|
if (facets.length === 0) {
|
|
180
735
|
log.verbose('Facets: No facets to display', {
|
|
181
736
|
hasResults: !!results,
|
|
@@ -187,7 +742,13 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
187
742
|
}
|
|
188
743
|
return null;
|
|
189
744
|
}
|
|
190
|
-
|
|
745
|
+
// -------------------------------------------------------------------
|
|
746
|
+
// Render
|
|
747
|
+
// -------------------------------------------------------------------
|
|
748
|
+
return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: {
|
|
749
|
+
...CSS_VAR_DEFAULTS,
|
|
750
|
+
...style,
|
|
751
|
+
} }, facets.map((facet, index) => {
|
|
191
752
|
return renderFacet
|
|
192
753
|
? renderFacet(facet, index)
|
|
193
754
|
: defaultRenderFacet(facet, index);
|