@seekora-ai/ui-sdk-react 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CurrentRefinements.d.ts +22 -2
- package/dist/components/CurrentRefinements.d.ts.map +1 -1
- package/dist/components/CurrentRefinements.js +259 -47
- package/dist/components/FacetDropdown.d.ts +92 -0
- package/dist/components/FacetDropdown.d.ts.map +1 -0
- package/dist/components/FacetDropdown.js +374 -0
- package/dist/components/Facets.d.ts +56 -1
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +602 -41
- package/dist/components/FederatedDropdown.d.ts.map +1 -1
- package/dist/components/FederatedDropdown.js +45 -31
- package/dist/components/HierarchicalMenu.d.ts.map +1 -1
- package/dist/components/HierarchicalMenu.js +112 -4
- package/dist/components/Pagination.d.ts +47 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +166 -28
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
- package/dist/components/QuerySuggestionsDropdown.js +32 -18
- package/dist/components/RangeInput.d.ts.map +1 -1
- package/dist/components/RangeInput.js +6 -6
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +101 -32
- package/dist/components/RichQuerySuggestions.d.ts +7 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/RichQuerySuggestions.js +40 -26
- package/dist/components/SearchBar.d.ts +16 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +139 -17
- package/dist/components/SearchBarWithSuggestions.js +3 -3
- package/dist/components/SearchLayout.d.ts.map +1 -1
- package/dist/components/SearchLayout.js +10 -1
- package/dist/components/SearchProvider.d.ts +8 -1
- package/dist/components/SearchProvider.d.ts.map +1 -1
- package/dist/components/SearchProvider.js +16 -4
- package/dist/components/SearchResults.d.ts +10 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +46 -30
- package/dist/components/SortBy.d.ts +44 -4
- package/dist/components/SortBy.d.ts.map +1 -1
- package/dist/components/SortBy.js +154 -29
- package/dist/components/Stats.d.ts +14 -0
- package/dist/components/Stats.d.ts.map +1 -1
- package/dist/components/Stats.js +172 -23
- package/dist/components/primitives/ActionButtons.d.ts.map +1 -1
- package/dist/components/primitives/ActionButtons.js +34 -10
- package/dist/components/primitives/BadgeList.d.ts.map +1 -1
- package/dist/components/primitives/BadgeList.js +33 -13
- package/dist/components/primitives/ImageDisplay.d.ts.map +1 -1
- package/dist/components/primitives/ImageDisplay.js +11 -8
- package/dist/components/primitives/ImageZoom.js +26 -26
- package/dist/components/primitives/VariantSelector.js +10 -10
- package/dist/components/primitives/VariantSwatches.js +3 -3
- package/dist/components/product-page/ProductGallery.d.ts +8 -1
- package/dist/components/product-page/ProductGallery.d.ts.map +1 -1
- package/dist/components/product-page/ProductGallery.js +2 -2
- package/dist/components/section-primitives/SectionSearchProvider.d.ts +3 -1
- package/dist/components/section-primitives/SectionSearchProvider.d.ts.map +1 -1
- package/dist/components/section-primitives/SectionSearchProvider.js +3 -2
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +2 -4
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +2 -6
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +2 -4
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +20 -22
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +2 -6
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +39 -41
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +2 -4
- package/dist/components/suggestions/utils.d.ts +10 -1
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +36 -0
- package/dist/components/suggestions-primitives/DropdownPanel.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/DropdownPanel.js +15 -2
- package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemCard.js +21 -8
- package/dist/components/suggestions-primitives/ItemGrid.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ItemGrid.js +9 -3
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCard.js +25 -10
- package/dist/components/suggestions-primitives/ProductCardLayouts.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/ProductCardLayouts.js +24 -12
- package/dist/components/suggestions-primitives/SearchInput.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SearchInput.js +28 -9
- package/dist/components/suggestions-primitives/SuggestionItem.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionItem.js +3 -0
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
- package/dist/hooks/useClickTracking.d.ts +36 -0
- package/dist/hooks/useClickTracking.d.ts.map +1 -0
- package/dist/hooks/useClickTracking.js +96 -0
- package/dist/hooks/useExperiment.d.ts +25 -0
- package/dist/hooks/useExperiment.d.ts.map +1 -0
- package/dist/hooks/useExperiment.js +146 -0
- package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
- package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
- package/dist/hooks/useKeyboardNavigation.js +113 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestions.js +19 -3
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestionsEnhanced.js +23 -6
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +345 -19
- package/dist/src/index.esm.js +2869 -787
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +2868 -785
- package/dist/src/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CurrentRefinements Component
|
|
3
3
|
*
|
|
4
|
-
* Displays currently active filters/refinements with ability to clear them
|
|
4
|
+
* Displays currently active filters/refinements with ability to clear them.
|
|
5
|
+
* Supports StateManager auto-sync, display variants, layout modes, and animations.
|
|
5
6
|
*/
|
|
6
7
|
import React from 'react';
|
|
7
8
|
export interface Refinement {
|
|
@@ -18,9 +19,20 @@ export interface CurrentRefinementsTheme {
|
|
|
18
19
|
value?: string;
|
|
19
20
|
clearButton?: string;
|
|
20
21
|
clearAllButton?: string;
|
|
22
|
+
/** Variant-specific class slots */
|
|
23
|
+
chip?: string;
|
|
24
|
+
tag?: string;
|
|
25
|
+
pill?: string;
|
|
26
|
+
badge?: string;
|
|
27
|
+
group?: string;
|
|
28
|
+
groupLabel?: string;
|
|
21
29
|
}
|
|
30
|
+
/** Display variant for refinement items */
|
|
31
|
+
export type RefinementVariant = 'chips' | 'tags' | 'pills' | 'badges' | 'text-list';
|
|
32
|
+
/** Layout variant for the container */
|
|
33
|
+
export type RefinementLayout = 'horizontal' | 'vertical' | 'grouped';
|
|
22
34
|
export interface CurrentRefinementsProps {
|
|
23
|
-
/** Current refinements */
|
|
35
|
+
/** Current refinements (if not provided, auto-reads from StateManager) */
|
|
24
36
|
refinements?: Refinement[];
|
|
25
37
|
/** Callback when a refinement is cleared */
|
|
26
38
|
onRefinementClear?: (field: string, value: string) => void;
|
|
@@ -30,6 +42,14 @@ export interface CurrentRefinementsProps {
|
|
|
30
42
|
renderRefinement?: (refinement: Refinement, index: number) => React.ReactNode;
|
|
31
43
|
/** Show "Clear all" button */
|
|
32
44
|
showClearAll?: boolean;
|
|
45
|
+
/** Display variant (default: 'chips') */
|
|
46
|
+
variant?: RefinementVariant;
|
|
47
|
+
/** Layout mode (default: 'horizontal') */
|
|
48
|
+
layout?: RefinementLayout;
|
|
49
|
+
/** Per-field color mapping */
|
|
50
|
+
fieldColors?: Record<string, string>;
|
|
51
|
+
/** Custom close icon renderer */
|
|
52
|
+
renderCloseIcon?: () => React.ReactNode;
|
|
33
53
|
/** Custom className */
|
|
34
54
|
className?: string;
|
|
35
55
|
/** Custom styles */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CurrentRefinements.d.ts","sourceRoot":"","sources":["../../src/components/CurrentRefinements.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"CurrentRefinements.d.ts","sourceRoot":"","sources":["../../src/components/CurrentRefinements.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AAWpE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEpF,uCAAuC;AACvC,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,UAAU,GAAG,SAAS,CAAC;AAErE,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,4CAA4C;IAC5C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9E,8BAA8B;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yCAAyC;IACzC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,0CAA0C;IAC1C,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iCAAiC;IACjC,eAAe,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACxC,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC;AA0FD,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAuShE,CAAC"}
|
|
@@ -1,75 +1,286 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CurrentRefinements Component
|
|
3
3
|
*
|
|
4
|
-
* Displays currently active filters/refinements with ability to clear them
|
|
4
|
+
* Displays currently active filters/refinements with ability to clear them.
|
|
5
|
+
* Supports StateManager auto-sync, display variants, layout modes, and animations.
|
|
5
6
|
*/
|
|
6
|
-
import React from 'react';
|
|
7
|
+
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
7
8
|
import { useSearchContext } from './SearchProvider';
|
|
9
|
+
import { useSearchState } from '../hooks/useSearchState';
|
|
8
10
|
import { clsx } from 'clsx';
|
|
9
|
-
|
|
11
|
+
const TRANSITIONS = {
|
|
12
|
+
fast: '150ms ease-in-out',
|
|
13
|
+
normal: '200ms ease-in-out',
|
|
14
|
+
slow: '300ms ease-in-out',
|
|
15
|
+
};
|
|
16
|
+
/** Get variant-specific styles */
|
|
17
|
+
const getVariantStyles = (variant, themeColors, themeSpacing, themeBorderRadius, fieldColor) => {
|
|
18
|
+
const baseBg = fieldColor || `var(--seekora-refinement-bg, ${themeColors.hover})`;
|
|
19
|
+
const baseColor = `var(--seekora-refinement-color, ${themeColors.text})`;
|
|
20
|
+
const baseBorder = `var(--seekora-refinement-border, ${themeColors.border})`;
|
|
21
|
+
switch (variant) {
|
|
22
|
+
case 'tags':
|
|
23
|
+
return {
|
|
24
|
+
display: 'inline-flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
padding: `2px ${themeSpacing.medium}`,
|
|
27
|
+
backgroundColor: baseBg,
|
|
28
|
+
border: `1px solid ${baseBorder}`,
|
|
29
|
+
borderRadius: `var(--seekora-refinement-radius, 4px)`,
|
|
30
|
+
fontSize: '12px',
|
|
31
|
+
color: baseColor,
|
|
32
|
+
fontWeight: 500,
|
|
33
|
+
};
|
|
34
|
+
case 'pills':
|
|
35
|
+
return {
|
|
36
|
+
display: 'inline-flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
padding: `${themeSpacing.small} ${themeSpacing.medium}`,
|
|
39
|
+
backgroundColor: baseBg,
|
|
40
|
+
border: 'none',
|
|
41
|
+
borderRadius: `var(--seekora-refinement-radius, 9999px)`,
|
|
42
|
+
fontSize: '13px',
|
|
43
|
+
color: baseColor,
|
|
44
|
+
};
|
|
45
|
+
case 'badges':
|
|
46
|
+
return {
|
|
47
|
+
display: 'inline-flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
padding: `3px ${themeSpacing.small}`,
|
|
50
|
+
backgroundColor: themeColors.primary,
|
|
51
|
+
border: 'none',
|
|
52
|
+
borderRadius: `var(--seekora-refinement-radius, 4px)`,
|
|
53
|
+
fontSize: '11px',
|
|
54
|
+
color: '#fff',
|
|
55
|
+
fontWeight: 600,
|
|
56
|
+
textTransform: 'uppercase',
|
|
57
|
+
letterSpacing: '0.5px',
|
|
58
|
+
};
|
|
59
|
+
case 'text-list':
|
|
60
|
+
return {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
padding: `${themeSpacing.small} 0`,
|
|
64
|
+
backgroundColor: 'transparent',
|
|
65
|
+
border: 'none',
|
|
66
|
+
borderBottom: `1px solid ${baseBorder}`,
|
|
67
|
+
borderRadius: '0',
|
|
68
|
+
fontSize: '14px',
|
|
69
|
+
color: baseColor,
|
|
70
|
+
};
|
|
71
|
+
case 'chips':
|
|
72
|
+
default:
|
|
73
|
+
return {
|
|
74
|
+
display: 'inline-flex',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
padding: `${themeSpacing.small} ${themeSpacing.medium}`,
|
|
77
|
+
backgroundColor: baseBg,
|
|
78
|
+
border: `1px solid ${baseBorder}`,
|
|
79
|
+
borderRadius: `var(--seekora-refinement-radius, ${typeof themeBorderRadius === 'string' ? themeBorderRadius : themeBorderRadius.medium})`,
|
|
80
|
+
fontSize: '13px',
|
|
81
|
+
color: baseColor,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** Get variant-specific class name from theme */
|
|
86
|
+
const getVariantClass = (variant, refinementsTheme) => {
|
|
87
|
+
switch (variant) {
|
|
88
|
+
case 'tags': return refinementsTheme.tag;
|
|
89
|
+
case 'pills': return refinementsTheme.pill;
|
|
90
|
+
case 'badges': return refinementsTheme.badge;
|
|
91
|
+
case 'chips': return refinementsTheme.chip;
|
|
92
|
+
default: return refinementsTheme.item;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
export const CurrentRefinements = ({ refinements: refinementsProp, onRefinementClear, onClearAll, renderRefinement, showClearAll = true, variant = 'chips', layout = 'horizontal', fieldColors, renderCloseIcon, className, style, theme: customTheme, }) => {
|
|
10
96
|
const { theme } = useSearchContext();
|
|
97
|
+
const { refinements: stateRefinements, removeRefinement, clearRefinements } = useSearchState();
|
|
11
98
|
const refinementsTheme = customTheme || {};
|
|
99
|
+
// Format a numeric value for display (auto-detect price fields)
|
|
100
|
+
const formatRangeValue = (field, val) => {
|
|
101
|
+
const f = field.toLowerCase();
|
|
102
|
+
if (f.includes('price') || f.includes('cost') || f.includes('amount')) {
|
|
103
|
+
return `$${val.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
104
|
+
}
|
|
105
|
+
return val.toLocaleString();
|
|
106
|
+
};
|
|
107
|
+
// Create a stable serialized key from stateRefinements to detect actual changes
|
|
108
|
+
// (needed because state manager mutates the array in place)
|
|
109
|
+
const stateRefinementsKey = useMemo(() => stateRefinements.map(r => `${r.field}:${r.value}`).sort().join('|'), [stateRefinements.map(r => `${r.field}:${r.value}`).sort().join('|')]);
|
|
110
|
+
// Use props if provided, otherwise auto-read from StateManager
|
|
111
|
+
// Combines >= / <= range refinements into single formatted pills
|
|
112
|
+
const refinements = useMemo(() => {
|
|
113
|
+
if (refinementsProp !== undefined) {
|
|
114
|
+
return refinementsProp;
|
|
115
|
+
}
|
|
116
|
+
const rangeMap = new Map();
|
|
117
|
+
const nonRange = [];
|
|
118
|
+
stateRefinements.forEach(r => {
|
|
119
|
+
const minMatch = r.value.match(/^>=(\d+(?:\.\d+)?)$/);
|
|
120
|
+
const maxMatch = r.value.match(/^<=(\d+(?:\.\d+)?)$/);
|
|
121
|
+
if (minMatch || maxMatch) {
|
|
122
|
+
if (!rangeMap.has(r.field))
|
|
123
|
+
rangeMap.set(r.field, {});
|
|
124
|
+
const entry = rangeMap.get(r.field);
|
|
125
|
+
if (minMatch)
|
|
126
|
+
entry.minVal = parseFloat(minMatch[1]);
|
|
127
|
+
if (maxMatch)
|
|
128
|
+
entry.maxVal = parseFloat(maxMatch[1]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
nonRange.push({ field: r.field, value: r.value });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
rangeMap.forEach((range, field) => {
|
|
135
|
+
let displayValue;
|
|
136
|
+
if (range.minVal !== undefined && range.maxVal !== undefined) {
|
|
137
|
+
displayValue = `${formatRangeValue(field, range.minVal)} – ${formatRangeValue(field, range.maxVal)}`;
|
|
138
|
+
}
|
|
139
|
+
else if (range.minVal !== undefined) {
|
|
140
|
+
displayValue = `≥ ${formatRangeValue(field, range.minVal)}`;
|
|
141
|
+
}
|
|
142
|
+
else if (range.maxVal !== undefined) {
|
|
143
|
+
displayValue = `≤ ${formatRangeValue(field, range.maxVal)}`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
nonRange.push({ field, value: '__range__', displayValue });
|
|
149
|
+
});
|
|
150
|
+
return nonRange;
|
|
151
|
+
}, [refinementsProp, stateRefinementsKey]);
|
|
152
|
+
// Track items for entry/exit animations
|
|
153
|
+
const [visibleItems, setVisibleItems] = useState(new Set());
|
|
154
|
+
const [exitingItems, setExitingItems] = useState(new Set());
|
|
155
|
+
const prevRefinementsRef = useRef([]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const currentKeys = new Set(refinements.map(r => `${r.field}:${r.value}`));
|
|
158
|
+
const prevKeys = new Set(prevRefinementsRef.current.map(r => `${r.field}:${r.value}`));
|
|
159
|
+
// Detect removed items for exit animation
|
|
160
|
+
const removed = new Set();
|
|
161
|
+
prevKeys.forEach(key => {
|
|
162
|
+
if (!currentKeys.has(key))
|
|
163
|
+
removed.add(key);
|
|
164
|
+
});
|
|
165
|
+
if (removed.size > 0) {
|
|
166
|
+
setExitingItems(removed);
|
|
167
|
+
// Remove after animation completes
|
|
168
|
+
setTimeout(() => setExitingItems(new Set()), 200);
|
|
169
|
+
}
|
|
170
|
+
// Mark new items for entry animation
|
|
171
|
+
setVisibleItems(currentKeys);
|
|
172
|
+
prevRefinementsRef.current = [...refinements];
|
|
173
|
+
}, [refinements]);
|
|
12
174
|
const handleClear = (field, value) => {
|
|
175
|
+
if (refinementsProp === undefined) {
|
|
176
|
+
if (value === '__range__') {
|
|
177
|
+
// Clear all range refinements for this field
|
|
178
|
+
const rangeRefs = stateRefinements.filter(r => r.field === field && (/^>=/.test(r.value) || /^<=/.test(r.value)));
|
|
179
|
+
rangeRefs.forEach((r, i) => removeRefinement(field, r.value, i === rangeRefs.length - 1));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
removeRefinement(field, value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
13
185
|
if (onRefinementClear) {
|
|
14
186
|
onRefinementClear(field, value);
|
|
15
187
|
}
|
|
16
188
|
};
|
|
17
189
|
const handleClearAll = () => {
|
|
190
|
+
// If synced with StateManager, auto-clear all via StateManager
|
|
191
|
+
if (refinementsProp === undefined) {
|
|
192
|
+
clearRefinements();
|
|
193
|
+
}
|
|
18
194
|
if (onClearAll) {
|
|
19
195
|
onClearAll();
|
|
20
196
|
}
|
|
21
197
|
};
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
198
|
+
const defaultCloseIcon = () => (React.createElement("span", { "aria-hidden": "true", style: { lineHeight: 1 } }, "\u00D7"));
|
|
199
|
+
const defaultRenderRefinement = (refinement, index) => {
|
|
200
|
+
const key = `${refinement.field}:${refinement.value}`;
|
|
201
|
+
const fieldColor = fieldColors?.[refinement.field];
|
|
202
|
+
const isEntering = visibleItems.has(key) && !prevRefinementsRef.current.some(r => `${r.field}:${r.value}` === key);
|
|
203
|
+
const variantStyles = getVariantStyles(variant, theme.colors, theme.spacing, theme.borderRadius, fieldColor);
|
|
204
|
+
const variantClass = getVariantClass(variant, refinementsTheme);
|
|
205
|
+
return (React.createElement("div", { key: `${key}-${index}`, className: clsx(refinementsTheme.item, variantClass), role: "listitem", style: {
|
|
206
|
+
...variantStyles,
|
|
207
|
+
margin: layout === 'vertical'
|
|
208
|
+
? `0 0 ${theme.spacing.small} 0`
|
|
209
|
+
: `0 ${theme.spacing.small} ${theme.spacing.small} 0`,
|
|
210
|
+
transition: `all ${TRANSITIONS.normal}`,
|
|
211
|
+
opacity: exitingItems.has(key) ? 0 : 1,
|
|
212
|
+
transform: exitingItems.has(key) ? 'scale(0.8)' : 'scale(1)',
|
|
213
|
+
animation: isEntering ? 'seekoraChipIn 200ms ease-out' : undefined,
|
|
36
214
|
} },
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
215
|
+
variant !== 'badges' && (React.createElement("span", { className: refinementsTheme.label, style: {
|
|
216
|
+
marginRight: theme.spacing.small,
|
|
217
|
+
fontWeight: '500',
|
|
218
|
+
opacity: 0.7,
|
|
219
|
+
} },
|
|
220
|
+
refinement.label || refinement.field,
|
|
221
|
+
":")),
|
|
222
|
+
React.createElement("span", { className: refinementsTheme.value }, refinement.displayValue || refinement.value),
|
|
223
|
+
React.createElement("button", { type: "button", onClick: () => handleClear(refinement.field, refinement.value), className: refinementsTheme.clearButton, style: {
|
|
224
|
+
border: 'none',
|
|
225
|
+
backgroundColor: 'transparent',
|
|
226
|
+
color: 'inherit',
|
|
227
|
+
cursor: 'pointer',
|
|
228
|
+
fontSize: '14px',
|
|
229
|
+
padding: '0 0 0 6px',
|
|
230
|
+
display: 'flex',
|
|
231
|
+
alignItems: 'center',
|
|
232
|
+
justifyContent: 'center',
|
|
233
|
+
borderRadius: '50%',
|
|
234
|
+
transition: `opacity ${TRANSITIONS.fast}`,
|
|
235
|
+
opacity: 0.6,
|
|
236
|
+
}, "aria-label": `Clear ${refinement.label || refinement.field}: ${refinement.value}`, onMouseEnter: e => (e.currentTarget.style.opacity = '1'), onMouseLeave: e => (e.currentTarget.style.opacity = '0.6') }, renderCloseIcon ? renderCloseIcon() : defaultCloseIcon())));
|
|
237
|
+
};
|
|
59
238
|
if (refinements.length === 0) {
|
|
60
239
|
return null;
|
|
61
240
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
241
|
+
// Group refinements by field for grouped layout
|
|
242
|
+
const groupedRefinements = layout === 'grouped'
|
|
243
|
+
? refinements.reduce((acc, r) => {
|
|
244
|
+
if (!acc[r.field])
|
|
245
|
+
acc[r.field] = [];
|
|
246
|
+
acc[r.field].push(r);
|
|
247
|
+
return acc;
|
|
248
|
+
}, {})
|
|
249
|
+
: null;
|
|
250
|
+
const containerStyles = {
|
|
251
|
+
...style,
|
|
252
|
+
};
|
|
253
|
+
const listStyles = {
|
|
254
|
+
display: 'flex',
|
|
255
|
+
flexWrap: layout === 'vertical' ? 'nowrap' : 'wrap',
|
|
256
|
+
flexDirection: layout === 'vertical' ? 'column' : 'row',
|
|
257
|
+
alignItems: layout === 'vertical' ? 'flex-start' : 'center',
|
|
258
|
+
marginBottom: showClearAll ? theme.spacing.medium : 0,
|
|
259
|
+
};
|
|
260
|
+
return (React.createElement("div", { className: clsx(refinementsTheme.container, className), style: containerStyles },
|
|
261
|
+
React.createElement("style", null, `
|
|
262
|
+
@keyframes seekoraChipIn {
|
|
263
|
+
from { opacity: 0; transform: scale(0.8); }
|
|
264
|
+
to { opacity: 1; transform: scale(1); }
|
|
265
|
+
}
|
|
266
|
+
`),
|
|
267
|
+
layout === 'grouped' && groupedRefinements ? (Object.entries(groupedRefinements).map(([field, items]) => (React.createElement("div", { key: field, className: refinementsTheme.group, style: { marginBottom: theme.spacing.medium } },
|
|
268
|
+
React.createElement("div", { className: refinementsTheme.groupLabel, style: {
|
|
269
|
+
fontSize: theme.typography.fontSize.small,
|
|
270
|
+
fontWeight: 600,
|
|
271
|
+
color: theme.colors.text,
|
|
272
|
+
marginBottom: theme.spacing.small,
|
|
273
|
+
textTransform: 'capitalize',
|
|
274
|
+
} }, items[0]?.label || field),
|
|
275
|
+
React.createElement("div", { role: "list", style: listStyles }, items.map((refinement, index) => {
|
|
276
|
+
return renderRefinement
|
|
277
|
+
? renderRefinement(refinement, index)
|
|
278
|
+
: defaultRenderRefinement(refinement, index);
|
|
279
|
+
})))))) : (React.createElement("div", { role: "list", className: refinementsTheme.list, style: listStyles }, refinements.map((refinement, index) => {
|
|
69
280
|
return renderRefinement
|
|
70
281
|
? renderRefinement(refinement, index)
|
|
71
282
|
: defaultRenderRefinement(refinement, index);
|
|
72
|
-
})),
|
|
283
|
+
}))),
|
|
73
284
|
showClearAll && refinements.length > 1 && (React.createElement("button", { type: "button", onClick: handleClearAll, className: refinementsTheme.clearAllButton, style: {
|
|
74
285
|
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
|
|
75
286
|
border: `1px solid ${theme.colors.border}`,
|
|
@@ -79,5 +290,6 @@ export const CurrentRefinements = ({ refinements = [], onRefinementClear, onClea
|
|
|
79
290
|
cursor: 'pointer',
|
|
80
291
|
fontSize: theme.typography.fontSize.small,
|
|
81
292
|
textDecoration: 'underline',
|
|
293
|
+
transition: `background-color ${TRANSITIONS.fast}`,
|
|
82
294
|
} }, "Clear all filters"))));
|
|
83
295
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FacetDropdown Component
|
|
3
|
+
*
|
|
4
|
+
* A dropdown component that displays facet values from a specific field
|
|
5
|
+
* and allows users to filter search results by selecting values.
|
|
6
|
+
*
|
|
7
|
+
* Can be used standalone (fetches its own data) or integrated with SearchProvider.
|
|
8
|
+
*
|
|
9
|
+
* @cssVariables
|
|
10
|
+
* Customize the dropdown appearance using CSS variables:
|
|
11
|
+
*
|
|
12
|
+
* Button:
|
|
13
|
+
* - `--facet-dropdown-button-bg`: Button background color
|
|
14
|
+
* - `--facet-dropdown-text`: Button text color
|
|
15
|
+
* - `--facet-dropdown-border`: Button and panel border color
|
|
16
|
+
* - `--facet-dropdown-font-size`: Button font size (default: 0.875rem)
|
|
17
|
+
* - `--facet-dropdown-font-weight`: Button font weight (default: 500)
|
|
18
|
+
*
|
|
19
|
+
* Dropdown Panel:
|
|
20
|
+
* - `--facet-dropdown-bg`: Panel background color
|
|
21
|
+
* - `--facet-dropdown-shadow`: Panel box shadow
|
|
22
|
+
*
|
|
23
|
+
* Options:
|
|
24
|
+
* - `--facet-dropdown-option-text`: Option text color
|
|
25
|
+
* - `--facet-dropdown-option-font-size`: Option font size (default: 0.875rem)
|
|
26
|
+
* - `--facet-dropdown-option-font-weight-active`: Active option font weight (default: 500)
|
|
27
|
+
* - `--facet-dropdown-option-active-bg`: Active option background color
|
|
28
|
+
*
|
|
29
|
+
* Count Badge:
|
|
30
|
+
* - `--facet-dropdown-count-text`: Count text color
|
|
31
|
+
* - `--facet-dropdown-count-font-size`: Count font size (default: 0.75rem)
|
|
32
|
+
*
|
|
33
|
+
* Scrollbar:
|
|
34
|
+
* - `--facet-dropdown-scrollbar-thumb`: Scrollbar thumb color
|
|
35
|
+
* - `--facet-dropdown-scrollbar-thumb-hover`: Scrollbar thumb hover color
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <FacetDropdown
|
|
40
|
+
* field="category"
|
|
41
|
+
* placeholder="All Categories"
|
|
42
|
+
* applyFilter={true}
|
|
43
|
+
* showCounts={true}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import React from 'react';
|
|
48
|
+
import type { SeekoraClient } from '@seekora-ai/search-sdk';
|
|
49
|
+
export interface FacetDropdownOption {
|
|
50
|
+
value: string;
|
|
51
|
+
count: number;
|
|
52
|
+
label?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface FacetDropdownTheme {
|
|
55
|
+
button?: string;
|
|
56
|
+
buttonOpen?: string;
|
|
57
|
+
panel?: string;
|
|
58
|
+
option?: string;
|
|
59
|
+
optionActive?: string;
|
|
60
|
+
optionHover?: string;
|
|
61
|
+
caretIcon?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface FacetDropdownProps {
|
|
64
|
+
/** The facet field to display (e.g., "category", "brand", "product_type") */
|
|
65
|
+
field: string;
|
|
66
|
+
/** Placeholder text when no value is selected */
|
|
67
|
+
placeholder?: string;
|
|
68
|
+
/** Custom className for the button */
|
|
69
|
+
className?: string;
|
|
70
|
+
/** Custom theme overrides */
|
|
71
|
+
theme?: FacetDropdownTheme;
|
|
72
|
+
/** Called when a value is selected */
|
|
73
|
+
onChange?: (value: string) => void;
|
|
74
|
+
/** Current selected value (controlled mode) */
|
|
75
|
+
value?: string;
|
|
76
|
+
/** Maximum number of options to show before scrolling */
|
|
77
|
+
maxOptions?: number;
|
|
78
|
+
/** Whether to show facet counts */
|
|
79
|
+
showCounts?: boolean;
|
|
80
|
+
/** Custom options (if you want to provide static options instead of fetching) */
|
|
81
|
+
options?: FacetDropdownOption[];
|
|
82
|
+
/** Standalone mode: provide your own search client */
|
|
83
|
+
client?: SeekoraClient;
|
|
84
|
+
/** Apply filter immediately on selection (default: true) */
|
|
85
|
+
applyFilter?: boolean;
|
|
86
|
+
/** Navigate to search page on selection (default: false) */
|
|
87
|
+
navigateOnSelect?: boolean;
|
|
88
|
+
/** Base URL for navigation (e.g., "/search") */
|
|
89
|
+
searchPageUrl?: string;
|
|
90
|
+
}
|
|
91
|
+
export declare const FacetDropdown: React.FC<FacetDropdownProps>;
|
|
92
|
+
//# sourceMappingURL=FacetDropdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacetDropdown.d.ts","sourceRoot":"","sources":["../../src/components/FacetDropdown.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAe5D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iFAAiF;IACjF,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC,sDAAsD;IACtD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6atD,CAAC"}
|