@seekora-ai/ui-sdk-react 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/components/CurrentRefinements.d.ts +22 -2
  2. package/dist/components/CurrentRefinements.d.ts.map +1 -1
  3. package/dist/components/CurrentRefinements.js +199 -47
  4. package/dist/components/Facets.d.ts +30 -1
  5. package/dist/components/Facets.d.ts.map +1 -1
  6. package/dist/components/Facets.js +418 -46
  7. package/dist/components/HierarchicalMenu.d.ts.map +1 -1
  8. package/dist/components/HierarchicalMenu.js +112 -4
  9. package/dist/components/InfiniteHits.d.ts +2 -0
  10. package/dist/components/InfiniteHits.d.ts.map +1 -1
  11. package/dist/components/InfiniteHits.js +6 -3
  12. package/dist/components/Pagination.d.ts +47 -1
  13. package/dist/components/Pagination.d.ts.map +1 -1
  14. package/dist/components/Pagination.js +166 -28
  15. package/dist/components/QuerySuggestions.d.ts +2 -0
  16. package/dist/components/QuerySuggestions.d.ts.map +1 -1
  17. package/dist/components/QuerySuggestions.js +4 -3
  18. package/dist/components/QuerySuggestionsDropdown.d.ts +1 -1
  19. package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
  20. package/dist/components/QuerySuggestionsDropdown.js +4 -4
  21. package/dist/components/RangeSlider.d.ts.map +1 -1
  22. package/dist/components/RangeSlider.js +49 -2
  23. package/dist/components/Recommendations.d.ts +6 -0
  24. package/dist/components/Recommendations.d.ts.map +1 -1
  25. package/dist/components/Recommendations.js +12 -6
  26. package/dist/components/RichQuerySuggestions.d.ts +11 -0
  27. package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
  28. package/dist/components/RichQuerySuggestions.js +2 -3
  29. package/dist/components/SearchBar.d.ts +18 -0
  30. package/dist/components/SearchBar.d.ts.map +1 -1
  31. package/dist/components/SearchBar.js +134 -24
  32. package/dist/components/SearchProvider.d.ts +8 -1
  33. package/dist/components/SearchProvider.d.ts.map +1 -1
  34. package/dist/components/SearchProvider.js +16 -4
  35. package/dist/components/SearchResults.d.ts +12 -0
  36. package/dist/components/SearchResults.d.ts.map +1 -1
  37. package/dist/components/SearchResults.js +11 -5
  38. package/dist/components/SortBy.d.ts +44 -4
  39. package/dist/components/SortBy.d.ts.map +1 -1
  40. package/dist/components/SortBy.js +154 -29
  41. package/dist/components/Stats.d.ts +14 -0
  42. package/dist/components/Stats.d.ts.map +1 -1
  43. package/dist/components/Stats.js +172 -23
  44. package/dist/components/section-primitives/SectionItemGrid.d.ts +3 -1
  45. package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
  46. package/dist/components/section-primitives/SectionItemGrid.js +3 -2
  47. package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
  48. package/dist/components/suggestions/AmazonDropdown.js +4 -6
  49. package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
  50. package/dist/components/suggestions/GoogleDropdown.js +4 -8
  51. package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
  52. package/dist/components/suggestions/MinimalDropdown.js +4 -6
  53. package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
  54. package/dist/components/suggestions/MobileSheetDropdown.js +4 -6
  55. package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
  56. package/dist/components/suggestions/PinterestDropdown.js +4 -8
  57. package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
  58. package/dist/components/suggestions/ShopifyDropdown.js +4 -6
  59. package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
  60. package/dist/components/suggestions/SpotlightDropdown.js +4 -6
  61. package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
  62. package/dist/components/suggestions/SuggestionSearchBar.js +1 -0
  63. package/dist/components/suggestions/types.d.ts +2 -0
  64. package/dist/components/suggestions/types.d.ts.map +1 -1
  65. package/dist/components/suggestions/utils.d.ts +10 -1
  66. package/dist/components/suggestions/utils.d.ts.map +1 -1
  67. package/dist/components/suggestions/utils.js +36 -0
  68. package/dist/components/suggestions-primitives/SuggestionList.d.ts +8 -1
  69. package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
  70. package/dist/components/suggestions-primitives/SuggestionList.js +7 -4
  71. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -1
  72. package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +0 -2
  73. package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
  74. package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
  75. package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
  76. package/dist/docsearch/components/Results.d.ts +3 -1
  77. package/dist/docsearch/components/Results.d.ts.map +1 -1
  78. package/dist/docsearch/components/Results.js +6 -2
  79. package/dist/hooks/useClickTracking.d.ts +36 -0
  80. package/dist/hooks/useClickTracking.d.ts.map +1 -0
  81. package/dist/hooks/useClickTracking.js +96 -0
  82. package/dist/hooks/useExperiment.d.ts +25 -0
  83. package/dist/hooks/useExperiment.d.ts.map +1 -0
  84. package/dist/hooks/useExperiment.js +146 -0
  85. package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
  86. package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
  87. package/dist/hooks/useKeyboardNavigation.js +113 -0
  88. package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
  89. package/dist/hooks/useQuerySuggestions.js +19 -3
  90. package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
  91. package/dist/hooks/useQuerySuggestionsEnhanced.js +25 -7
  92. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  93. package/dist/hooks/useSuggestionsAnalytics.js +6 -1
  94. package/dist/index.d.ts +4 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.umd.js +1 -1
  97. package/dist/src/index.d.ts +249 -19
  98. package/dist/src/index.esm.js +1659 -305
  99. package/dist/src/index.esm.js.map +1 -1
  100. package/dist/src/index.js +1658 -304
  101. package/dist/src/index.js.map +1 -1
  102. 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
- export const Facets = ({ results: resultsProp, facets: facetsProp, onFacetChange, renderFacet, renderFacetItem, maxItems = 10, showMore = true, className, style, theme: customTheme, }) => {
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
- return (React.createElement("div", { key: `${facet.field}-${item.value}`, className: clsx(facetsTheme.facetItem, (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) && facetsTheme.facetItemActive), onClick: () => handleFacetToggle(facet.field, item.value, item.selected || false), style: {
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: theme.spacing.small,
253
+ padding: sizeScale.padding,
106
254
  cursor: 'pointer',
107
255
  borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
108
- marginBottom: theme.spacing.small,
109
- backgroundColor: (refinements.some(r => r.field === facet.field && r.value === item.value) || item.selected) ? theme.colors.hover : 'transparent',
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: theme.spacing.small,
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: theme.typography.fontSize.medium,
268
+ fontSize: sizeScale.font,
119
269
  color: theme.colors.text,
120
270
  } }, item.value),
121
- React.createElement("span", { className: facetsTheme.facetItemCount, style: {
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
- const defaultRenderFacet = (facet, index) => {
132
- const isExpanded = expandedFacets[facet.field] || false;
133
- const visibleItems = isExpanded ? facet.items : facet.items.slice(0, maxItems);
134
- const hasMore = facet.items.length > maxItems;
135
- return (React.createElement("div", { key: facet.field, className: facetsTheme.facet, style: {
136
- marginBottom: theme.spacing.large,
137
- padding: theme.spacing.medium,
138
- backgroundColor: theme.colors.background,
139
- border: `1px solid ${theme.colors.border}`,
140
- borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
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("h3", { className: facetsTheme.facetTitle, style: {
143
- fontSize: theme.typography.fontSize.large,
144
- fontWeight: 'bold',
145
- margin: 0,
146
- marginBottom: theme.spacing.medium,
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
- } }, facet.label || facet.field),
149
- React.createElement("div", { className: facetsTheme.facetList }, visibleItems.map((item, itemIndex) => {
150
- const actualIndex = isExpanded ? itemIndex : itemIndex;
151
- return renderFacetItem
152
- ? renderFacetItem(item, facet, actualIndex)
153
- : defaultRenderFacetItem(item, facet, actualIndex);
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: theme.spacing.small,
157
- padding: theme.spacing.small,
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
- facet.items.length - maxItems,
386
+ filteredItems.length - maxItems,
167
387
  " more)")),
168
388
  isExpanded && hasMore && (React.createElement("button", { type: "button", onClick: () => toggleFacetExpansion(facet.field), style: {
169
- marginTop: theme.spacing.small,
170
- padding: theme.spacing.small,
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
- return (React.createElement("div", { className: clsx(facetsTheme.container, className), style: style }, facets.map((facet, index) => {
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,KAAyC,MAAM,OAAO,CAAC;AAK9D,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,CA2P5D,CAAC"}
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"}