@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
|
@@ -0,0 +1,374 @@
|
|
|
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', }) => {
|
|
64
|
+
const { client: contextClient, theme } = 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 });
|
|
83
|
+
// Perform a search with facet_by parameter to get facet counts
|
|
84
|
+
const response = await client.search('*', {
|
|
85
|
+
facet_by: field,
|
|
86
|
+
max_facet_values: 100,
|
|
87
|
+
per_page: 0, // We only need facets, not results
|
|
88
|
+
});
|
|
89
|
+
log.verbose('FacetDropdown: Search response', { response });
|
|
90
|
+
// Extract facet counts from the response
|
|
91
|
+
const facetsData = response?.data?.facets || response?.facets;
|
|
92
|
+
const facets = Array.isArray(facetsData) ? facetsData : [];
|
|
93
|
+
const targetFacet = facets.find((f) => f.field_name === field || f.field === field);
|
|
94
|
+
if (targetFacet && targetFacet.counts) {
|
|
95
|
+
const options = targetFacet.counts.map((count) => ({
|
|
96
|
+
value: count.value,
|
|
97
|
+
count: count.count,
|
|
98
|
+
label: count.value,
|
|
99
|
+
}));
|
|
100
|
+
setFacetOptions(options);
|
|
101
|
+
log.verbose('FacetDropdown: Facet options loaded', { field, count: options.length });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
log.verbose('FacetDropdown: No facet data found', { field, facets });
|
|
105
|
+
setFacetOptions([]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
110
|
+
log.error('FacetDropdown: Error fetching facet values', {
|
|
111
|
+
field,
|
|
112
|
+
error: err.message,
|
|
113
|
+
});
|
|
114
|
+
setFacetOptions([]);
|
|
115
|
+
}
|
|
116
|
+
}, [field, client, providedOptions]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!providedOptions && client) {
|
|
119
|
+
fetchFacetValues();
|
|
120
|
+
}
|
|
121
|
+
}, [fetchFacetValues, providedOptions, client]);
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Calculate dropdown position when opened
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!open || !buttonRef.current)
|
|
127
|
+
return;
|
|
128
|
+
const updatePosition = () => {
|
|
129
|
+
if (!buttonRef.current)
|
|
130
|
+
return;
|
|
131
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
132
|
+
setDropdownPosition({
|
|
133
|
+
top: rect.bottom + window.scrollY + 4,
|
|
134
|
+
left: rect.left + window.scrollX,
|
|
135
|
+
width: rect.width,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
updatePosition();
|
|
139
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
140
|
+
window.addEventListener('resize', updatePosition);
|
|
141
|
+
return () => {
|
|
142
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
143
|
+
window.removeEventListener('resize', updatePosition);
|
|
144
|
+
};
|
|
145
|
+
}, [open]);
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Click outside handler
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!open)
|
|
151
|
+
return;
|
|
152
|
+
const handleClickOutside = (e) => {
|
|
153
|
+
if (dropdownRef.current &&
|
|
154
|
+
!dropdownRef.current.contains(e.target) &&
|
|
155
|
+
buttonRef.current &&
|
|
156
|
+
!buttonRef.current.contains(e.target)) {
|
|
157
|
+
setOpen(false);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const handleEscape = (e) => {
|
|
161
|
+
if (e.key === 'Escape') {
|
|
162
|
+
setOpen(false);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
166
|
+
document.addEventListener('keydown', handleEscape);
|
|
167
|
+
return () => {
|
|
168
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
169
|
+
document.removeEventListener('keydown', handleEscape);
|
|
170
|
+
};
|
|
171
|
+
}, [open]);
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Selection handler
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
const handleSelect = useCallback((value) => {
|
|
176
|
+
log.verbose('FacetDropdown: Value selected', { field, value });
|
|
177
|
+
// Update internal state
|
|
178
|
+
setInternalValue(value);
|
|
179
|
+
setOpen(false);
|
|
180
|
+
// Call onChange callback
|
|
181
|
+
if (onChange) {
|
|
182
|
+
onChange(value);
|
|
183
|
+
}
|
|
184
|
+
// Apply filter if enabled and we have search state
|
|
185
|
+
if (applyFilter && addRefinement && removeRefinement) {
|
|
186
|
+
// Remove any existing refinement for this field
|
|
187
|
+
const existingRefinement = refinements.find(r => r.field === field);
|
|
188
|
+
if (existingRefinement) {
|
|
189
|
+
removeRefinement(field, existingRefinement.value, false);
|
|
190
|
+
}
|
|
191
|
+
// Add new refinement if value is not empty
|
|
192
|
+
if (value) {
|
|
193
|
+
addRefinement(field, value, true); // true = trigger search
|
|
194
|
+
}
|
|
195
|
+
else if (search) {
|
|
196
|
+
// If clearing the filter, trigger search manually
|
|
197
|
+
search();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Navigate to search page if enabled
|
|
201
|
+
if (navigateOnSelect && value && typeof window !== 'undefined') {
|
|
202
|
+
const url = new URL(window.location.origin + searchPageUrl);
|
|
203
|
+
url.searchParams.set(field, value);
|
|
204
|
+
window.location.href = url.toString();
|
|
205
|
+
}
|
|
206
|
+
}, [field, onChange, applyFilter, addRefinement, removeRefinement, refinements, search, navigateOnSelect, searchPageUrl]);
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Render
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
const displayLabel = currentValue || placeholder;
|
|
211
|
+
const hasOptions = facetOptions.length > 0;
|
|
212
|
+
const buttonClass = [
|
|
213
|
+
'facet-dropdown-button',
|
|
214
|
+
customTheme.button,
|
|
215
|
+
className,
|
|
216
|
+
open && customTheme.buttonOpen,
|
|
217
|
+
]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(' ');
|
|
220
|
+
// Render dropdown panel via portal
|
|
221
|
+
const renderDropdown = () => {
|
|
222
|
+
if (!open || !hasOptions || !dropdownPosition || typeof document === 'undefined') {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const dropdown = (React.createElement("div", { ref: dropdownRef, className: customTheme.panel || 'facet-dropdown-panel', role: "listbox", "aria-label": `${field} options`, style: {
|
|
226
|
+
position: 'absolute',
|
|
227
|
+
top: `${dropdownPosition.top}px`,
|
|
228
|
+
left: `${dropdownPosition.left}px`,
|
|
229
|
+
minWidth: `${dropdownPosition.width}px`,
|
|
230
|
+
width: 'max-content',
|
|
231
|
+
maxWidth: 'min(24rem, 90vw)',
|
|
232
|
+
maxHeight: `calc(100vh - 100px)`,
|
|
233
|
+
overflowY: 'auto',
|
|
234
|
+
overflowX: 'hidden',
|
|
235
|
+
backgroundColor: 'var(--facet-dropdown-bg, var(--background))',
|
|
236
|
+
border: '1px solid var(--facet-dropdown-border, var(--border-color))',
|
|
237
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
238
|
+
? theme.borderRadius.medium
|
|
239
|
+
: '0.5rem',
|
|
240
|
+
boxShadow: 'var(--facet-dropdown-shadow, var(--shadow-lg))',
|
|
241
|
+
zIndex: Z_INDEX.dropdown,
|
|
242
|
+
padding: '0.25rem',
|
|
243
|
+
} },
|
|
244
|
+
React.createElement("button", { type: "button", role: "option", "aria-selected": !currentValue, onClick: (e) => {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
handleSelect('');
|
|
248
|
+
}, className: [
|
|
249
|
+
customTheme.option,
|
|
250
|
+
!currentValue && customTheme.optionActive,
|
|
251
|
+
]
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.join(' '), style: {
|
|
254
|
+
width: '100%',
|
|
255
|
+
textAlign: 'left',
|
|
256
|
+
padding: '0.5rem 0.75rem',
|
|
257
|
+
fontSize: 'var(--facet-dropdown-option-font-size, 0.875rem)',
|
|
258
|
+
fontWeight: !currentValue ? 'var(--facet-dropdown-option-font-weight-active, 500)' : 'normal',
|
|
259
|
+
color: 'var(--facet-dropdown-option-text, var(--text-color, currentColor))',
|
|
260
|
+
backgroundColor: !currentValue ? 'var(--facet-dropdown-option-active-bg, var(--hover-color))' : 'transparent',
|
|
261
|
+
border: 'none',
|
|
262
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
263
|
+
? theme.borderRadius.small
|
|
264
|
+
: '0.25rem',
|
|
265
|
+
cursor: 'pointer',
|
|
266
|
+
transition: 'background-color 0.15s ease',
|
|
267
|
+
display: 'flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
justifyContent: 'space-between',
|
|
270
|
+
} },
|
|
271
|
+
React.createElement("span", null, placeholder)),
|
|
272
|
+
facetOptions.map((option) => {
|
|
273
|
+
const isActive = currentValue === option.value;
|
|
274
|
+
return (React.createElement("button", { key: option.value, type: "button", role: "option", "aria-selected": isActive, onClick: (e) => {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
handleSelect(option.value);
|
|
278
|
+
}, className: [
|
|
279
|
+
customTheme.option,
|
|
280
|
+
isActive && customTheme.optionActive,
|
|
281
|
+
]
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join(' '), style: {
|
|
284
|
+
width: '100%',
|
|
285
|
+
textAlign: 'left',
|
|
286
|
+
padding: '0.5rem 0.75rem',
|
|
287
|
+
fontSize: 'var(--facet-dropdown-option-font-size, 0.875rem)',
|
|
288
|
+
fontWeight: isActive ? 'var(--facet-dropdown-option-font-weight-active, 500)' : 'normal',
|
|
289
|
+
color: 'var(--facet-dropdown-option-text, var(--text-color, currentColor))',
|
|
290
|
+
backgroundColor: isActive ? 'var(--facet-dropdown-option-active-bg, var(--hover-color))' : 'transparent',
|
|
291
|
+
border: 'none',
|
|
292
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
293
|
+
? theme.borderRadius.small
|
|
294
|
+
: '0.25rem',
|
|
295
|
+
cursor: 'pointer',
|
|
296
|
+
transition: 'background-color 0.15s ease',
|
|
297
|
+
display: 'flex',
|
|
298
|
+
alignItems: 'center',
|
|
299
|
+
justifyContent: 'space-between',
|
|
300
|
+
} },
|
|
301
|
+
React.createElement("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' } }, option.label || option.value),
|
|
302
|
+
showCounts && (React.createElement("span", { style: {
|
|
303
|
+
fontSize: 'var(--facet-dropdown-count-font-size, 0.75rem)',
|
|
304
|
+
color: 'var(--facet-dropdown-count-text, var(--text-secondary, currentColor))',
|
|
305
|
+
opacity: 0.7,
|
|
306
|
+
marginLeft: '0.5rem',
|
|
307
|
+
flexShrink: 0,
|
|
308
|
+
} },
|
|
309
|
+
"(",
|
|
310
|
+
option.count,
|
|
311
|
+
")"))));
|
|
312
|
+
})));
|
|
313
|
+
return createPortal(dropdown, document.body);
|
|
314
|
+
};
|
|
315
|
+
return (React.createElement("div", { className: "facet-dropdown-container", style: { position: 'relative' } },
|
|
316
|
+
React.createElement("style", { dangerouslySetInnerHTML: {
|
|
317
|
+
__html: `
|
|
318
|
+
.facet-dropdown-panel {
|
|
319
|
+
--scrollbar-track: transparent;
|
|
320
|
+
--scrollbar-thumb: var(--facet-dropdown-scrollbar-thumb, var(--border-color));
|
|
321
|
+
--scrollbar-thumb-hover: var(--facet-dropdown-scrollbar-thumb-hover, var(--text-secondary));
|
|
322
|
+
}
|
|
323
|
+
.facet-dropdown-panel::-webkit-scrollbar {
|
|
324
|
+
width: 6px;
|
|
325
|
+
}
|
|
326
|
+
.facet-dropdown-panel::-webkit-scrollbar-track {
|
|
327
|
+
background: var(--scrollbar-track);
|
|
328
|
+
}
|
|
329
|
+
.facet-dropdown-panel::-webkit-scrollbar-thumb {
|
|
330
|
+
background: var(--scrollbar-thumb);
|
|
331
|
+
border-radius: 3px;
|
|
332
|
+
}
|
|
333
|
+
.facet-dropdown-panel::-webkit-scrollbar-thumb:hover {
|
|
334
|
+
background: var(--scrollbar-thumb-hover);
|
|
335
|
+
}
|
|
336
|
+
.facet-dropdown-panel {
|
|
337
|
+
scrollbar-width: thin;
|
|
338
|
+
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
339
|
+
}
|
|
340
|
+
`
|
|
341
|
+
} }),
|
|
342
|
+
React.createElement("button", { ref: buttonRef, type: "button", onClick: (e) => {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
setOpen(prev => !prev);
|
|
346
|
+
}, className: buttonClass, "aria-label": `Filter by ${field}`, "aria-expanded": open, "aria-haspopup": "listbox", style: {
|
|
347
|
+
display: 'flex',
|
|
348
|
+
alignItems: 'center',
|
|
349
|
+
gap: '0.375rem',
|
|
350
|
+
padding: '0.625rem 0.75rem',
|
|
351
|
+
fontSize: 'var(--facet-dropdown-font-size, 0.875rem)',
|
|
352
|
+
fontWeight: 'var(--facet-dropdown-font-weight, 500)',
|
|
353
|
+
color: 'var(--facet-dropdown-text, var(--text-color, currentColor))',
|
|
354
|
+
backgroundColor: 'var(--facet-dropdown-button-bg, var(--background, transparent))',
|
|
355
|
+
border: '1px solid var(--facet-dropdown-border, var(--border-color))',
|
|
356
|
+
borderRadius: theme?.borderRadius && typeof theme.borderRadius !== 'string'
|
|
357
|
+
? theme.borderRadius.medium
|
|
358
|
+
: '0.5rem',
|
|
359
|
+
cursor: 'pointer',
|
|
360
|
+
transition: 'all 0.2s ease',
|
|
361
|
+
outline: 'none',
|
|
362
|
+
whiteSpace: 'nowrap',
|
|
363
|
+
minWidth: '8rem',
|
|
364
|
+
...(!hasOptions && { opacity: 0.6, cursor: 'not-allowed' }),
|
|
365
|
+
}, disabled: !hasOptions },
|
|
366
|
+
React.createElement("span", { style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' } }, displayLabel),
|
|
367
|
+
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: {
|
|
368
|
+
transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
369
|
+
transition: 'transform 0.2s ease',
|
|
370
|
+
flexShrink: 0,
|
|
371
|
+
}, "aria-hidden": "true" },
|
|
372
|
+
React.createElement("path", { d: "M4 6L8 10L12 6", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }))),
|
|
373
|
+
renderDropdown()));
|
|
374
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
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
7
|
import React from 'react';
|
|
7
8
|
import type { SearchResponse } from '@seekora-ai/search-sdk';
|
|
@@ -10,10 +11,26 @@ export interface FacetItem {
|
|
|
10
11
|
count: number;
|
|
11
12
|
selected?: boolean;
|
|
12
13
|
}
|
|
14
|
+
export interface FacetStats {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
avg?: number;
|
|
18
|
+
sum?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface FacetRangeItem {
|
|
21
|
+
label: string;
|
|
22
|
+
from?: number;
|
|
23
|
+
to?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FacetRangeConfig {
|
|
26
|
+
field: string;
|
|
27
|
+
ranges: FacetRangeItem[];
|
|
28
|
+
}
|
|
13
29
|
export interface Facet {
|
|
14
30
|
field: string;
|
|
15
31
|
label?: string;
|
|
16
32
|
items: FacetItem[];
|
|
33
|
+
stats?: FacetStats;
|
|
17
34
|
}
|
|
18
35
|
export interface FacetsTheme {
|
|
19
36
|
container?: string;
|
|
@@ -26,7 +43,31 @@ export interface FacetsTheme {
|
|
|
26
43
|
facetItemCount?: string;
|
|
27
44
|
facetItemLabel?: string;
|
|
28
45
|
checkbox?: string;
|
|
46
|
+
/** Color swatch circle/square */
|
|
47
|
+
colorSwatch?: string;
|
|
48
|
+
/** Color swatch when selected */
|
|
49
|
+
colorSwatchSelected?: string;
|
|
50
|
+
/** Inner element of color swatch (for checkmark overlay) */
|
|
51
|
+
colorSwatchInner?: string;
|
|
52
|
+
/** Search input within a facet group */
|
|
53
|
+
searchInput?: string;
|
|
54
|
+
/** Collapsible header row */
|
|
55
|
+
collapsibleHeader?: string;
|
|
56
|
+
/** Chevron/expand icon in collapsible header */
|
|
57
|
+
collapsibleIcon?: string;
|
|
58
|
+
/** Count badge pill */
|
|
59
|
+
countBadge?: string;
|
|
60
|
+
/** Range button (pre-configured range option) */
|
|
61
|
+
rangeButton?: string;
|
|
62
|
+
/** Active range button */
|
|
63
|
+
rangeButtonActive?: string;
|
|
64
|
+
/** Count badge on range button */
|
|
65
|
+
rangeButtonCount?: string;
|
|
66
|
+
/** Clear button for range filters */
|
|
67
|
+
rangeClear?: string;
|
|
29
68
|
}
|
|
69
|
+
export type FacetVariant = 'checkbox' | 'color-swatch' | 'collapsible';
|
|
70
|
+
export type FacetSize = 'small' | 'medium' | 'large';
|
|
30
71
|
export interface FacetsProps {
|
|
31
72
|
/** Search results response */
|
|
32
73
|
results?: SearchResponse | null;
|
|
@@ -48,6 +89,20 @@ export interface FacetsProps {
|
|
|
48
89
|
style?: React.CSSProperties;
|
|
49
90
|
/** Custom theme */
|
|
50
91
|
theme?: FacetsTheme;
|
|
92
|
+
/** Display variant: checkbox (default), color-swatch, or collapsible */
|
|
93
|
+
variant?: FacetVariant;
|
|
94
|
+
/** When true, show a search input at the top of each facet group */
|
|
95
|
+
searchable?: boolean;
|
|
96
|
+
/** Show count badges (default: true) */
|
|
97
|
+
showCounts?: boolean;
|
|
98
|
+
/** Maps facet values to CSS colors (used by color-swatch variant) */
|
|
99
|
+
colorMap?: Record<string, string>;
|
|
100
|
+
/** For collapsible variant, whether facets start collapsed (default: false) */
|
|
101
|
+
defaultCollapsed?: boolean;
|
|
102
|
+
/** Size preset: small, medium (default), or large */
|
|
103
|
+
size?: FacetSize;
|
|
104
|
+
/** Pre-configured range button definitions for numeric facets */
|
|
105
|
+
facetRanges?: FacetRangeConfig[];
|
|
51
106
|
}
|
|
52
107
|
export declare const Facets: React.FC<FacetsProps>;
|
|
53
108
|
//# sourceMappingURL=Facets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Facets.d.ts","sourceRoot":"","sources":["../../src/components/Facets.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Facets.d.ts","sourceRoot":"","sources":["../../src/components/Facets.tsx"],"names":[],"mappings":"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"}
|