@seekora-ai/ui-sdk-react 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CurrentRefinements.d.ts +22 -2
- package/dist/components/CurrentRefinements.d.ts.map +1 -1
- package/dist/components/CurrentRefinements.js +199 -47
- package/dist/components/Facets.d.ts +30 -1
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +418 -46
- package/dist/components/HierarchicalMenu.d.ts.map +1 -1
- package/dist/components/HierarchicalMenu.js +112 -4
- package/dist/components/Pagination.d.ts +47 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +166 -28
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +49 -2
- package/dist/components/RichQuerySuggestions.d.ts +7 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/SearchBar.d.ts +16 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +130 -16
- package/dist/components/SearchProvider.d.ts +8 -1
- package/dist/components/SearchProvider.d.ts.map +1 -1
- package/dist/components/SearchProvider.js +16 -4
- package/dist/components/SearchResults.d.ts +10 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +9 -5
- package/dist/components/SortBy.d.ts +44 -4
- package/dist/components/SortBy.d.ts.map +1 -1
- package/dist/components/SortBy.js +154 -29
- package/dist/components/Stats.d.ts +14 -0
- package/dist/components/Stats.d.ts.map +1 -1
- package/dist/components/Stats.js +172 -23
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +2 -4
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +2 -6
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -4
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +2 -4
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +2 -6
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +2 -4
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -4
- package/dist/components/suggestions/utils.d.ts +10 -1
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +36 -0
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
- package/dist/hooks/useClickTracking.d.ts +36 -0
- package/dist/hooks/useClickTracking.d.ts.map +1 -0
- package/dist/hooks/useClickTracking.js +96 -0
- package/dist/hooks/useExperiment.d.ts +25 -0
- package/dist/hooks/useExperiment.d.ts.map +1 -0
- package/dist/hooks/useExperiment.js +146 -0
- package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
- package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
- package/dist/hooks/useKeyboardNavigation.js +113 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestions.js +19 -3
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestionsEnhanced.js +23 -6
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +217 -16
- package/dist/src/index.esm.js +1586 -249
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1585 -248
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,21 +1,80 @@
|
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Generate a deterministic colour from a string (used as fallback for color-swatch). */
|
|
16
|
+
function stringToColor(str) {
|
|
17
|
+
let hash = 0;
|
|
18
|
+
for (let i = 0; i < str.length; i++) {
|
|
19
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
20
|
+
hash |= 0; // Convert to 32-bit int
|
|
21
|
+
}
|
|
22
|
+
const h = Math.abs(hash) % 360;
|
|
23
|
+
return `hsl(${h}, 65%, 55%)`;
|
|
24
|
+
}
|
|
25
|
+
/** Return swatch pixel size from the size prop. */
|
|
26
|
+
function swatchSize(size) {
|
|
27
|
+
switch (size) {
|
|
28
|
+
case 'small':
|
|
29
|
+
return 24;
|
|
30
|
+
case 'large':
|
|
31
|
+
return 40;
|
|
32
|
+
case 'medium':
|
|
33
|
+
default:
|
|
34
|
+
return 32;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Chevron SVG icon for collapsible variant
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
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: {
|
|
41
|
+
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
42
|
+
transition: 'transform 200ms ease',
|
|
43
|
+
flexShrink: 0,
|
|
44
|
+
}, "aria-hidden": "true" },
|
|
45
|
+
React.createElement("path", { d: "M4 6L8 10L12 6", stroke: color, strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Checkmark SVG for selected color swatches
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
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" },
|
|
50
|
+
React.createElement("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })));
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// CSS variable defaults
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const CSS_VAR_DEFAULTS = {
|
|
55
|
+
'--seekora-facet-bg': '#ffffff',
|
|
56
|
+
'--seekora-facet-border': '#dee2e6',
|
|
57
|
+
'--seekora-facet-active-bg': '#f0f7ff',
|
|
58
|
+
'--seekora-facet-swatch-size': '32px',
|
|
59
|
+
'--seekora-facet-count-bg': '#e9ecef',
|
|
60
|
+
'--seekora-facet-count-color': '#495057',
|
|
61
|
+
};
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Component
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
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', }) => {
|
|
12
66
|
const { theme } = useSearchContext();
|
|
13
67
|
const { results: stateResults, refinements, addRefinement, removeRefinement } = useSearchState();
|
|
14
68
|
const facetsTheme = customTheme || {};
|
|
69
|
+
// expandedFacets is used for "Show more/less" in checkbox/color-swatch variants
|
|
70
|
+
// AND for collapse/expand in collapsible variant.
|
|
15
71
|
const [expandedFacets, setExpandedFacets] = useState({});
|
|
72
|
+
const [searchTerms, setSearchTerms] = useState({});
|
|
16
73
|
// Use results from prop if provided, otherwise from state manager
|
|
17
74
|
const results = resultsProp || stateResults;
|
|
75
|
+
// -------------------------------------------------------------------
|
|
18
76
|
// Extract facets from results
|
|
77
|
+
// -------------------------------------------------------------------
|
|
19
78
|
const extractFacets = () => {
|
|
20
79
|
if (facetsProp)
|
|
21
80
|
return facetsProp;
|
|
@@ -62,6 +121,9 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
62
121
|
return extracted;
|
|
63
122
|
};
|
|
64
123
|
const facets = extractFacets();
|
|
124
|
+
// -------------------------------------------------------------------
|
|
125
|
+
// Handlers
|
|
126
|
+
// -------------------------------------------------------------------
|
|
65
127
|
const handleFacetToggle = (field, value, selected) => {
|
|
66
128
|
const newSelected = !selected;
|
|
67
129
|
log.verbose('Facets: Facet toggled', { field, value, selected: newSelected });
|
|
@@ -94,67 +156,225 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
94
156
|
[field]: !prev[field],
|
|
95
157
|
}));
|
|
96
158
|
};
|
|
159
|
+
/** For collapsible variant — determine if a facet group is open. */
|
|
160
|
+
const isFacetGroupOpen = (field) => {
|
|
161
|
+
if (field in expandedFacets) {
|
|
162
|
+
return expandedFacets[field];
|
|
163
|
+
}
|
|
164
|
+
// Default based on defaultCollapsed prop
|
|
165
|
+
return !defaultCollapsed;
|
|
166
|
+
};
|
|
167
|
+
const toggleCollapsible = (field) => {
|
|
168
|
+
setExpandedFacets((prev) => ({
|
|
169
|
+
...prev,
|
|
170
|
+
[field]: !(prev[field] ?? !defaultCollapsed),
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
const getSearchTerm = (field) => searchTerms[field] || '';
|
|
174
|
+
const setSearchTerm = (field, term) => {
|
|
175
|
+
setSearchTerms((prev) => ({ ...prev, [field]: term }));
|
|
176
|
+
};
|
|
177
|
+
/** Filter facet items by search term. */
|
|
178
|
+
const filterItems = (items, field) => {
|
|
179
|
+
if (!searchable)
|
|
180
|
+
return items;
|
|
181
|
+
const term = getSearchTerm(field).toLowerCase();
|
|
182
|
+
if (!term)
|
|
183
|
+
return items;
|
|
184
|
+
return items.filter((item) => item.value.toLowerCase().includes(term));
|
|
185
|
+
};
|
|
186
|
+
// -------------------------------------------------------------------
|
|
187
|
+
// Size helpers
|
|
188
|
+
// -------------------------------------------------------------------
|
|
189
|
+
const sizeScale = useMemo(() => {
|
|
190
|
+
switch (size) {
|
|
191
|
+
case 'small':
|
|
192
|
+
return { font: theme.typography.fontSize.small, padding: '0.25rem', gap: '0.25rem' };
|
|
193
|
+
case 'large':
|
|
194
|
+
return { font: theme.typography.fontSize.large, padding: '0.75rem', gap: '0.75rem' };
|
|
195
|
+
case 'medium':
|
|
196
|
+
default:
|
|
197
|
+
return { font: theme.typography.fontSize.medium, padding: theme.spacing.small, gap: theme.spacing.small };
|
|
198
|
+
}
|
|
199
|
+
}, [size, theme]);
|
|
200
|
+
// -------------------------------------------------------------------
|
|
201
|
+
// Count badge renderer
|
|
202
|
+
// -------------------------------------------------------------------
|
|
203
|
+
const renderCountBadge = (count) => {
|
|
204
|
+
if (!showCounts)
|
|
205
|
+
return null;
|
|
206
|
+
return (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
207
|
+
display: 'inline-flex',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
justifyContent: 'center',
|
|
210
|
+
minWidth: '1.5em',
|
|
211
|
+
padding: '0.1em 0.5em',
|
|
212
|
+
marginLeft: sizeScale.gap,
|
|
213
|
+
fontSize: theme.typography.fontSize.small,
|
|
214
|
+
fontWeight: theme.typography.fontWeight?.medium ?? 500,
|
|
215
|
+
lineHeight: 1,
|
|
216
|
+
color: 'var(--seekora-facet-count-color, #495057)',
|
|
217
|
+
backgroundColor: 'var(--seekora-facet-count-bg, #e9ecef)',
|
|
218
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.full,
|
|
219
|
+
flexShrink: 0,
|
|
220
|
+
} }, count));
|
|
221
|
+
};
|
|
222
|
+
// -------------------------------------------------------------------
|
|
223
|
+
// Search input renderer
|
|
224
|
+
// -------------------------------------------------------------------
|
|
225
|
+
const renderSearchInput = (facet) => {
|
|
226
|
+
if (!searchable)
|
|
227
|
+
return null;
|
|
228
|
+
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: {
|
|
229
|
+
width: '100%',
|
|
230
|
+
boxSizing: 'border-box',
|
|
231
|
+
padding: sizeScale.padding,
|
|
232
|
+
marginBottom: sizeScale.gap,
|
|
233
|
+
fontSize: theme.typography.fontSize.small,
|
|
234
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
235
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.small,
|
|
236
|
+
outline: 'none',
|
|
237
|
+
color: theme.colors.text,
|
|
238
|
+
backgroundColor: 'var(--seekora-facet-bg, transparent)',
|
|
239
|
+
} }));
|
|
240
|
+
};
|
|
241
|
+
// -------------------------------------------------------------------
|
|
242
|
+
// Checkbox variant item renderer (original behaviour, preserved)
|
|
243
|
+
// -------------------------------------------------------------------
|
|
97
244
|
const defaultRenderFacetItem = (item, facet, index) => {
|
|
98
245
|
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
99
246
|
if (!isExpanded && index >= maxItems) {
|
|
100
247
|
return null;
|
|
101
248
|
}
|
|
102
|
-
|
|
249
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
250
|
+
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
251
|
display: 'flex',
|
|
104
252
|
alignItems: 'center',
|
|
105
|
-
padding:
|
|
253
|
+
padding: sizeScale.padding,
|
|
106
254
|
cursor: 'pointer',
|
|
107
255
|
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
108
|
-
marginBottom:
|
|
109
|
-
backgroundColor:
|
|
256
|
+
marginBottom: sizeScale.gap,
|
|
257
|
+
backgroundColor: isChecked
|
|
258
|
+
? 'var(--seekora-facet-active-bg, ' + theme.colors.hover + ')'
|
|
259
|
+
: 'transparent',
|
|
110
260
|
transition: 'background-color 0.2s ease',
|
|
111
261
|
} },
|
|
112
262
|
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:
|
|
263
|
+
marginRight: sizeScale.gap,
|
|
114
264
|
cursor: 'pointer',
|
|
115
265
|
}, "aria-label": `Filter by ${item.value}` }),
|
|
116
266
|
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
117
267
|
flex: 1,
|
|
118
|
-
fontSize:
|
|
268
|
+
fontSize: sizeScale.font,
|
|
119
269
|
color: theme.colors.text,
|
|
120
270
|
} }, item.value),
|
|
121
|
-
|
|
122
|
-
fontSize: theme.typography.fontSize.small,
|
|
123
|
-
color: theme.colors.textSecondary || theme.colors.text,
|
|
124
|
-
opacity: 0.7,
|
|
125
|
-
marginLeft: theme.spacing.small,
|
|
126
|
-
} },
|
|
127
|
-
"(",
|
|
128
|
-
item.count,
|
|
129
|
-
")")));
|
|
271
|
+
renderCountBadge(item.count)));
|
|
130
272
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
273
|
+
// -------------------------------------------------------------------
|
|
274
|
+
// Color swatch variant item renderer
|
|
275
|
+
// -------------------------------------------------------------------
|
|
276
|
+
const renderColorSwatchItem = (item, facet, index) => {
|
|
277
|
+
const isExpanded = expandedFacets[facet.field] || index < maxItems;
|
|
278
|
+
if (!isExpanded && index >= maxItems) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const isChecked = refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected || false;
|
|
282
|
+
const color = colorMap?.[item.value] ?? stringToColor(item.value);
|
|
283
|
+
const pxSize = swatchSize(size);
|
|
284
|
+
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: {
|
|
285
|
+
display: 'inline-flex',
|
|
286
|
+
flexDirection: 'column',
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
cursor: 'pointer',
|
|
289
|
+
margin: sizeScale.gap,
|
|
141
290
|
} },
|
|
142
|
-
React.createElement("
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
291
|
+
React.createElement("div", { className: clsx(facetsTheme.colorSwatch, isChecked && facetsTheme.colorSwatchSelected), style: {
|
|
292
|
+
width: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
293
|
+
height: `var(--seekora-facet-swatch-size, ${pxSize}px)`,
|
|
294
|
+
borderRadius: '50%',
|
|
295
|
+
backgroundColor: color,
|
|
296
|
+
border: isChecked
|
|
297
|
+
? `3px solid ${theme.colors.primary}`
|
|
298
|
+
: `2px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
299
|
+
display: 'flex',
|
|
300
|
+
alignItems: 'center',
|
|
301
|
+
justifyContent: 'center',
|
|
302
|
+
transition: 'border 0.2s ease, box-shadow 0.2s ease',
|
|
303
|
+
boxShadow: isChecked ? `0 0 0 2px ${theme.colors.primary}33` : 'none',
|
|
304
|
+
position: 'relative',
|
|
305
|
+
} }, isChecked && (React.createElement("span", { className: clsx(facetsTheme.colorSwatchInner), style: {
|
|
306
|
+
display: 'flex',
|
|
307
|
+
alignItems: 'center',
|
|
308
|
+
justifyContent: 'center',
|
|
309
|
+
} },
|
|
310
|
+
React.createElement(CheckmarkIcon, { size: Math.round(pxSize * 0.5) })))),
|
|
311
|
+
React.createElement("span", { className: facetsTheme.facetItemLabel, style: {
|
|
312
|
+
fontSize: theme.typography.fontSize.small,
|
|
147
313
|
color: theme.colors.text,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
:
|
|
154
|
-
|
|
314
|
+
marginTop: '0.25rem',
|
|
315
|
+
textAlign: 'center',
|
|
316
|
+
maxWidth: `${pxSize + 16}px`,
|
|
317
|
+
overflow: 'hidden',
|
|
318
|
+
textOverflow: 'ellipsis',
|
|
319
|
+
whiteSpace: 'nowrap',
|
|
320
|
+
} }, item.value),
|
|
321
|
+
showCounts && (React.createElement("span", { className: clsx(facetsTheme.countBadge), style: {
|
|
322
|
+
fontSize: theme.typography.fontSize.small,
|
|
323
|
+
color: 'var(--seekora-facet-count-color, ' + (theme.colors.textSecondary || theme.colors.text) + ')',
|
|
324
|
+
lineHeight: 1,
|
|
325
|
+
marginTop: '0.125rem',
|
|
326
|
+
} }, item.count))));
|
|
327
|
+
};
|
|
328
|
+
// -------------------------------------------------------------------
|
|
329
|
+
// Item renderer dispatcher
|
|
330
|
+
// -------------------------------------------------------------------
|
|
331
|
+
const renderItem = (item, facet, index) => {
|
|
332
|
+
if (renderFacetItem) {
|
|
333
|
+
return renderFacetItem(item, facet, index);
|
|
334
|
+
}
|
|
335
|
+
switch (variant) {
|
|
336
|
+
case 'color-swatch':
|
|
337
|
+
return renderColorSwatchItem(item, facet, index);
|
|
338
|
+
case 'collapsible':
|
|
339
|
+
case 'checkbox':
|
|
340
|
+
default:
|
|
341
|
+
return defaultRenderFacetItem(item, facet, index);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
// -------------------------------------------------------------------
|
|
345
|
+
// Keyboard handler (shared across variants)
|
|
346
|
+
// -------------------------------------------------------------------
|
|
347
|
+
const handleListKeyDown = (e, visibleItems, facet) => {
|
|
348
|
+
const currentEl = e.currentTarget.querySelector('[aria-selected="true"]');
|
|
349
|
+
const allItems = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
350
|
+
const currentIdx = currentEl ? allItems.indexOf(currentEl) : -1;
|
|
351
|
+
if (e.key === 'ArrowDown') {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
const next = Math.min(currentIdx + 1, allItems.length - 1);
|
|
354
|
+
allItems[next]?.focus();
|
|
355
|
+
}
|
|
356
|
+
else if (e.key === 'ArrowUp') {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
const prev = Math.max(currentIdx - 1, 0);
|
|
359
|
+
allItems[prev]?.focus();
|
|
360
|
+
}
|
|
361
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
if (currentIdx >= 0 && currentIdx < visibleItems.length) {
|
|
364
|
+
handleFacetToggle(facet.field, visibleItems[currentIdx].value, visibleItems[currentIdx].selected || false);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
// -------------------------------------------------------------------
|
|
369
|
+
// Show more / less buttons (shared)
|
|
370
|
+
// -------------------------------------------------------------------
|
|
371
|
+
const renderShowMoreLess = (facet, filteredItems) => {
|
|
372
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
373
|
+
const hasMore = filteredItems.length > maxItems;
|
|
374
|
+
return (React.createElement(React.Fragment, null,
|
|
155
375
|
showMore && hasMore && !isExpanded && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
156
|
-
marginTop:
|
|
157
|
-
padding:
|
|
376
|
+
marginTop: sizeScale.gap,
|
|
377
|
+
padding: sizeScale.padding,
|
|
158
378
|
border: 'none',
|
|
159
379
|
backgroundColor: 'transparent',
|
|
160
380
|
color: theme.colors.primary,
|
|
@@ -163,11 +383,11 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
163
383
|
textDecoration: 'underline',
|
|
164
384
|
} },
|
|
165
385
|
"Show more (",
|
|
166
|
-
|
|
386
|
+
filteredItems.length - maxItems,
|
|
167
387
|
" more)")),
|
|
168
388
|
isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
|
|
169
|
-
marginTop:
|
|
170
|
-
padding:
|
|
389
|
+
marginTop: sizeScale.gap,
|
|
390
|
+
padding: sizeScale.padding,
|
|
171
391
|
border: 'none',
|
|
172
392
|
backgroundColor: 'transparent',
|
|
173
393
|
color: theme.colors.primary,
|
|
@@ -176,6 +396,152 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
176
396
|
textDecoration: 'underline',
|
|
177
397
|
} }, "Show less"))));
|
|
178
398
|
};
|
|
399
|
+
// -------------------------------------------------------------------
|
|
400
|
+
// Default facet group renderer — Checkbox variant
|
|
401
|
+
// -------------------------------------------------------------------
|
|
402
|
+
const renderCheckboxFacet = (facet, _index) => {
|
|
403
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
404
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
405
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
406
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
407
|
+
marginBottom: theme.spacing.large,
|
|
408
|
+
padding: theme.spacing.medium,
|
|
409
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
410
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
411
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
412
|
+
} },
|
|
413
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
414
|
+
fontSize: theme.typography.fontSize.large,
|
|
415
|
+
fontWeight: 'bold',
|
|
416
|
+
margin: 0,
|
|
417
|
+
marginBottom: theme.spacing.medium,
|
|
418
|
+
color: theme.colors.text,
|
|
419
|
+
} }, facet.label || facet.field),
|
|
420
|
+
renderSearchInput(facet),
|
|
421
|
+
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))),
|
|
422
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
423
|
+
};
|
|
424
|
+
// -------------------------------------------------------------------
|
|
425
|
+
// Color-swatch facet group renderer
|
|
426
|
+
// -------------------------------------------------------------------
|
|
427
|
+
const renderColorSwatchFacet = (facet, _index) => {
|
|
428
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
429
|
+
const isExpanded = expandedFacets[facet.field] || false;
|
|
430
|
+
const visibleItems = isExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
431
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
432
|
+
marginBottom: theme.spacing.large,
|
|
433
|
+
padding: theme.spacing.medium,
|
|
434
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
435
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
436
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
437
|
+
} },
|
|
438
|
+
React.createElement("h3", { className: facetsTheme.facetTitle, style: {
|
|
439
|
+
fontSize: theme.typography.fontSize.large,
|
|
440
|
+
fontWeight: 'bold',
|
|
441
|
+
margin: 0,
|
|
442
|
+
marginBottom: theme.spacing.medium,
|
|
443
|
+
color: theme.colors.text,
|
|
444
|
+
} }, facet.label || facet.field),
|
|
445
|
+
renderSearchInput(facet),
|
|
446
|
+
React.createElement("div", { className: facetsTheme.facetList, role: "listbox", "aria-label": `${facet.label || facet.field} filters`, tabIndex: 0, onKeyDown: (e) => handleListKeyDown(e, visibleItems, facet), style: {
|
|
447
|
+
display: 'flex',
|
|
448
|
+
flexWrap: 'wrap',
|
|
449
|
+
gap: sizeScale.gap,
|
|
450
|
+
} }, visibleItems.map((item, itemIndex) => renderItem(item, facet, itemIndex))),
|
|
451
|
+
renderShowMoreLess(facet, filteredItems)));
|
|
452
|
+
};
|
|
453
|
+
// -------------------------------------------------------------------
|
|
454
|
+
// Collapsible facet group renderer
|
|
455
|
+
// -------------------------------------------------------------------
|
|
456
|
+
const renderCollapsibleFacet = (facet, _index) => {
|
|
457
|
+
const isOpen = isFacetGroupOpen(facet.field);
|
|
458
|
+
const filteredItems = filterItems(facet.items, facet.field);
|
|
459
|
+
const isItemsExpanded = expandedFacets[facet.field] || false;
|
|
460
|
+
// Note: For collapsible, the expandedFacets state controls the collapse/expand
|
|
461
|
+
// of the group itself. We use a separate concept for Show more/less within items.
|
|
462
|
+
// To avoid collision, Show more/less for collapsible uses the same expandedFacets
|
|
463
|
+
// key prefixed with `_items_`.
|
|
464
|
+
const isShowMoreExpanded = expandedFacets[`_items_${facet.field}`] || false;
|
|
465
|
+
const visibleItems = isShowMoreExpanded ? filteredItems : filteredItems.slice(0, maxItems);
|
|
466
|
+
const hasMore = filteredItems.length > maxItems;
|
|
467
|
+
return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
|
|
468
|
+
marginBottom: theme.spacing.large,
|
|
469
|
+
backgroundColor: 'var(--seekora-facet-bg, ' + theme.colors.background + ')',
|
|
470
|
+
border: `1px solid var(--seekora-facet-border, ${theme.colors.border})`,
|
|
471
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
472
|
+
overflow: 'hidden',
|
|
473
|
+
} },
|
|
474
|
+
React.createElement("button", { type: "button", className: clsx(facetsTheme.collapsibleHeader), onClick: () => toggleCollapsible(facet.field), "aria-expanded": isOpen, "aria-controls": `facet-group-${facet.field}`, style: {
|
|
475
|
+
display: 'flex',
|
|
476
|
+
alignItems: 'center',
|
|
477
|
+
justifyContent: 'space-between',
|
|
478
|
+
width: '100%',
|
|
479
|
+
padding: theme.spacing.medium,
|
|
480
|
+
border: 'none',
|
|
481
|
+
backgroundColor: 'transparent',
|
|
482
|
+
cursor: 'pointer',
|
|
483
|
+
textAlign: 'left',
|
|
484
|
+
} },
|
|
485
|
+
React.createElement("span", { className: facetsTheme.facetTitle, style: {
|
|
486
|
+
fontSize: theme.typography.fontSize.large,
|
|
487
|
+
fontWeight: 'bold',
|
|
488
|
+
color: theme.colors.text,
|
|
489
|
+
flex: 1,
|
|
490
|
+
} }, facet.label || facet.field),
|
|
491
|
+
React.createElement("span", { className: clsx(facetsTheme.collapsibleIcon) },
|
|
492
|
+
React.createElement(ChevronIcon, { expanded: isOpen, color: theme.colors.textSecondary || theme.colors.text }))),
|
|
493
|
+
isOpen && (React.createElement("div", { id: `facet-group-${facet.field}`, style: {
|
|
494
|
+
padding: `0 ${theme.spacing.medium} ${theme.spacing.medium}`,
|
|
495
|
+
} },
|
|
496
|
+
renderSearchInput(facet),
|
|
497
|
+
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))),
|
|
498
|
+
showMore && hasMore && !isShowMoreExpanded && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
499
|
+
...prev,
|
|
500
|
+
[`_items_${facet.field}`]: true,
|
|
501
|
+
})), style: {
|
|
502
|
+
marginTop: sizeScale.gap,
|
|
503
|
+
padding: sizeScale.padding,
|
|
504
|
+
border: 'none',
|
|
505
|
+
backgroundColor: 'transparent',
|
|
506
|
+
color: theme.colors.primary,
|
|
507
|
+
cursor: 'pointer',
|
|
508
|
+
fontSize: theme.typography.fontSize.small,
|
|
509
|
+
textDecoration: 'underline',
|
|
510
|
+
} },
|
|
511
|
+
"Show more (",
|
|
512
|
+
filteredItems.length - maxItems,
|
|
513
|
+
" more)")),
|
|
514
|
+
isShowMoreExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => setExpandedFacets((prev) => ({
|
|
515
|
+
...prev,
|
|
516
|
+
[`_items_${facet.field}`]: false,
|
|
517
|
+
})), style: {
|
|
518
|
+
marginTop: sizeScale.gap,
|
|
519
|
+
padding: sizeScale.padding,
|
|
520
|
+
border: 'none',
|
|
521
|
+
backgroundColor: 'transparent',
|
|
522
|
+
color: theme.colors.primary,
|
|
523
|
+
cursor: 'pointer',
|
|
524
|
+
fontSize: theme.typography.fontSize.small,
|
|
525
|
+
textDecoration: 'underline',
|
|
526
|
+
} }, "Show less"))))));
|
|
527
|
+
};
|
|
528
|
+
// -------------------------------------------------------------------
|
|
529
|
+
// Default facet renderer dispatcher
|
|
530
|
+
// -------------------------------------------------------------------
|
|
531
|
+
const defaultRenderFacet = (facet, index) => {
|
|
532
|
+
switch (variant) {
|
|
533
|
+
case 'color-swatch':
|
|
534
|
+
return renderColorSwatchFacet(facet, index);
|
|
535
|
+
case 'collapsible':
|
|
536
|
+
return renderCollapsibleFacet(facet, index);
|
|
537
|
+
case 'checkbox':
|
|
538
|
+
default:
|
|
539
|
+
return renderCheckboxFacet(facet, index);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
// -------------------------------------------------------------------
|
|
543
|
+
// Empty state
|
|
544
|
+
// -------------------------------------------------------------------
|
|
179
545
|
if (facets.length === 0) {
|
|
180
546
|
log.verbose('Facets: No facets to display', {
|
|
181
547
|
hasResults: !!results,
|
|
@@ -187,7 +553,13 @@ export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange
|
|
|
187
553
|
}
|
|
188
554
|
return null;
|
|
189
555
|
}
|
|
190
|
-
|
|
556
|
+
// -------------------------------------------------------------------
|
|
557
|
+
// Render
|
|
558
|
+
// -------------------------------------------------------------------
|
|
559
|
+
return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: {
|
|
560
|
+
...CSS_VAR_DEFAULTS,
|
|
561
|
+
...style,
|
|
562
|
+
} }, facets.map((facet, index) => {
|
|
191
563
|
return renderFacet
|
|
192
564
|
? renderFacet(facet, index)
|
|
193
565
|
: defaultRenderFacet(facet, index);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HierarchicalMenu.d.ts","sourceRoot":"","sources":["../../src/components/HierarchicalMenu.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"HierarchicalMenu.d.ts","sourceRoot":"","sources":["../../src/components/HierarchicalMenu.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAiD,MAAM,OAAO,CAAC;AAKtE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,yGAAyG;IACzG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5E,wCAAwC;IACxC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,qBAAqB,CAAC;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;IACxC,oDAAoD;IACpD,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAoW5D,CAAC"}
|