@seekora-ai/ui-sdk-react 1.0.0
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/Breadcrumb.d.ts +43 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +119 -0
- package/dist/components/ClearRefinements.d.ts +42 -0
- package/dist/components/ClearRefinements.d.ts.map +1 -0
- package/dist/components/ClearRefinements.js +80 -0
- package/dist/components/CurrentRefinements.d.ts +41 -0
- package/dist/components/CurrentRefinements.d.ts.map +1 -0
- package/dist/components/CurrentRefinements.js +83 -0
- package/dist/components/Facets.d.ts +53 -0
- package/dist/components/Facets.d.ts.map +1 -0
- package/dist/components/Facets.js +195 -0
- package/dist/components/FederatedDropdown.d.ts +92 -0
- package/dist/components/FederatedDropdown.d.ts.map +1 -0
- package/dist/components/FederatedDropdown.js +510 -0
- package/dist/components/HierarchicalMenu.d.ts +55 -0
- package/dist/components/HierarchicalMenu.d.ts.map +1 -0
- package/dist/components/HierarchicalMenu.js +168 -0
- package/dist/components/Highlight.d.ts +51 -0
- package/dist/components/Highlight.d.ts.map +1 -0
- package/dist/components/Highlight.js +155 -0
- package/dist/components/HitsPerPage.d.ts +41 -0
- package/dist/components/HitsPerPage.d.ts.map +1 -0
- package/dist/components/HitsPerPage.js +72 -0
- package/dist/components/InfiniteHits.d.ts +56 -0
- package/dist/components/InfiniteHits.d.ts.map +1 -0
- package/dist/components/InfiniteHits.js +181 -0
- package/dist/components/MobileFilters.d.ts +71 -0
- package/dist/components/MobileFilters.d.ts.map +1 -0
- package/dist/components/MobileFilters.js +242 -0
- package/dist/components/Pagination.d.ts +44 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +142 -0
- package/dist/components/QuerySuggestions.d.ts +38 -0
- package/dist/components/QuerySuggestions.d.ts.map +1 -0
- package/dist/components/QuerySuggestions.js +86 -0
- package/dist/components/QuerySuggestionsDropdown.d.ts +86 -0
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -0
- package/dist/components/QuerySuggestionsDropdown.js +395 -0
- package/dist/components/RangeInput.d.ts +58 -0
- package/dist/components/RangeInput.d.ts.map +1 -0
- package/dist/components/RangeInput.js +203 -0
- package/dist/components/RangeSlider.d.ts +51 -0
- package/dist/components/RangeSlider.d.ts.map +1 -0
- package/dist/components/RangeSlider.js +193 -0
- package/dist/components/Recommendations.d.ts +90 -0
- package/dist/components/Recommendations.d.ts.map +1 -0
- package/dist/components/Recommendations.js +270 -0
- package/dist/components/RichQuerySuggestions.d.ts +77 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -0
- package/dist/components/RichQuerySuggestions.js +492 -0
- package/dist/components/SearchBar.d.ts +40 -0
- package/dist/components/SearchBar.d.ts.map +1 -0
- package/dist/components/SearchBar.js +217 -0
- package/dist/components/SearchBarWithSuggestions.d.ts +99 -0
- package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -0
- package/dist/components/SearchBarWithSuggestions.js +275 -0
- package/dist/components/SearchLayout.d.ts +35 -0
- package/dist/components/SearchLayout.d.ts.map +1 -0
- package/dist/components/SearchLayout.js +56 -0
- package/dist/components/SearchProvider.d.ts +28 -0
- package/dist/components/SearchProvider.d.ts.map +1 -0
- package/dist/components/SearchProvider.js +43 -0
- package/dist/components/SearchResults.d.ts +51 -0
- package/dist/components/SearchResults.d.ts.map +1 -0
- package/dist/components/SearchResults.js +485 -0
- package/dist/components/SortBy.d.ts +44 -0
- package/dist/components/SortBy.d.ts.map +1 -0
- package/dist/components/SortBy.js +61 -0
- package/dist/components/Stats.d.ts +37 -0
- package/dist/components/Stats.d.ts.map +1 -0
- package/dist/components/Stats.js +52 -0
- package/dist/components/suggestions/AmazonDropdown.d.ts +30 -0
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/AmazonDropdown.js +529 -0
- package/dist/components/suggestions/GoogleDropdown.d.ts +31 -0
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/GoogleDropdown.js +370 -0
- package/dist/components/suggestions/MinimalDropdown.d.ts +24 -0
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/MinimalDropdown.js +314 -0
- package/dist/components/suggestions/MobileSheetDropdown.d.ts +31 -0
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/MobileSheetDropdown.js +485 -0
- package/dist/components/suggestions/PinterestDropdown.d.ts +29 -0
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/PinterestDropdown.js +450 -0
- package/dist/components/suggestions/ShopifyDropdown.d.ts +27 -0
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/ShopifyDropdown.js +451 -0
- package/dist/components/suggestions/SpotlightDropdown.d.ts +33 -0
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -0
- package/dist/components/suggestions/SpotlightDropdown.js +547 -0
- package/dist/components/suggestions/SuggestionSearchBar.d.ts +123 -0
- package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -0
- package/dist/components/suggestions/SuggestionSearchBar.js +652 -0
- package/dist/components/suggestions/index.d.ts +37 -0
- package/dist/components/suggestions/index.d.ts.map +1 -0
- package/dist/components/suggestions/index.js +59 -0
- package/dist/components/suggestions/styles/index.d.ts +11 -0
- package/dist/components/suggestions/styles/index.d.ts.map +1 -0
- package/dist/components/suggestions/styles/index.js +289 -0
- package/dist/components/suggestions/styles/responsive.d.ts +107 -0
- package/dist/components/suggestions/styles/responsive.d.ts.map +1 -0
- package/dist/components/suggestions/styles/responsive.js +237 -0
- package/dist/components/suggestions/types.d.ts +489 -0
- package/dist/components/suggestions/types.d.ts.map +1 -0
- package/dist/components/suggestions/types.js +6 -0
- package/dist/components/suggestions/utils.d.ts +213 -0
- package/dist/components/suggestions/utils.d.ts.map +1 -0
- package/dist/components/suggestions/utils.js +514 -0
- package/dist/hooks/useAnalytics.d.ts +20 -0
- package/dist/hooks/useAnalytics.d.ts.map +1 -0
- package/dist/hooks/useAnalytics.js +62 -0
- package/dist/hooks/useNaturalLanguageFilters.d.ts +48 -0
- package/dist/hooks/useNaturalLanguageFilters.d.ts.map +1 -0
- package/dist/hooks/useNaturalLanguageFilters.js +221 -0
- package/dist/hooks/useQuerySuggestions.d.ts +21 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -0
- package/dist/hooks/useQuerySuggestions.js +68 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts +114 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.js +376 -0
- package/dist/hooks/useSearchState.d.ts +35 -0
- package/dist/hooks/useSearchState.d.ts.map +1 -0
- package/dist/hooks/useSearchState.js +68 -0
- package/dist/hooks/useSeekoraSearch.d.ts +20 -0
- package/dist/hooks/useSeekoraSearch.d.ts.map +1 -0
- package/dist/hooks/useSeekoraSearch.js +63 -0
- package/dist/hooks/useSmartSuggestions.d.ts +55 -0
- package/dist/hooks/useSmartSuggestions.d.ts.map +1 -0
- package/dist/hooks/useSmartSuggestions.js +236 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts +91 -0
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -0
- package/dist/hooks/useSuggestionsAnalytics.js +226 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.umd.js +1 -0
- package/dist/src/index.d.ts +2849 -0
- package/dist/src/index.esm.js +11679 -0
- package/dist/src/index.esm.js.map +1 -0
- package/dist/src/index.js +11761 -0
- package/dist/src/index.js.map +1 -0
- package/dist/themes/createTheme.d.ts +8 -0
- package/dist/themes/createTheme.d.ts.map +1 -0
- package/dist/themes/createTheme.js +10 -0
- package/dist/themes/dark.d.ts +6 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +34 -0
- package/dist/themes/default.d.ts +6 -0
- package/dist/themes/default.d.ts.map +1 -0
- package/dist/themes/default.js +71 -0
- package/dist/themes/mergeThemes.d.ts +7 -0
- package/dist/themes/mergeThemes.d.ts.map +1 -0
- package/dist/themes/mergeThemes.js +6 -0
- package/dist/themes/minimal.d.ts +6 -0
- package/dist/themes/minimal.d.ts.map +1 -0
- package/dist/themes/minimal.js +34 -0
- package/dist/themes/suggestions.d.ts +216 -0
- package/dist/themes/suggestions.d.ts.map +1 -0
- package/dist/themes/suggestions.js +546 -0
- package/dist/themes/types.d.ts +7 -0
- package/dist/themes/types.d.ts.map +1 -0
- package/dist/themes/types.js +6 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/package.json +65 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchBar Component
|
|
3
|
+
*
|
|
4
|
+
* Interactive search input component with query suggestions support
|
|
5
|
+
*/
|
|
6
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
7
|
+
import { useSearchContext } from './SearchProvider';
|
|
8
|
+
import { useSearchState } from '../hooks/useSearchState';
|
|
9
|
+
import { useQuerySuggestions } from '../hooks/useQuerySuggestions';
|
|
10
|
+
import { log } from '@seekora-ai/ui-sdk-core';
|
|
11
|
+
import { clsx } from 'clsx';
|
|
12
|
+
export const SearchBar = ({ placeholder = 'Search...', showSuggestions = true, debounceMs = 300, minQueryLength = 2, maxSuggestions = 10, onSearch, onQueryChange, onSuggestionSelect, onSearchStateChange, searchOptions, className, style, theme: customTheme, renderSuggestion, renderLoading, }) => {
|
|
13
|
+
const { client, theme, enableAnalytics, autoTrackSearch } = useSearchContext();
|
|
14
|
+
const { query, setQuery, search: triggerSearch, results, loading: searchLoading, error: searchError } = useSearchState();
|
|
15
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
16
|
+
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
17
|
+
const inputRef = useRef(null);
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const isSearchingRef = useRef(false); // Flag to prevent blur handler from interfering
|
|
20
|
+
const { suggestions, loading: suggestionsLoading } = useQuerySuggestions({
|
|
21
|
+
client,
|
|
22
|
+
query,
|
|
23
|
+
enabled: showSuggestions && query.length >= minQueryLength,
|
|
24
|
+
debounceMs,
|
|
25
|
+
maxSuggestions,
|
|
26
|
+
});
|
|
27
|
+
const displayedSuggestions = suggestions.slice(0, maxSuggestions);
|
|
28
|
+
// Expose search state to parent
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (onSearchStateChange) {
|
|
31
|
+
onSearchStateChange({
|
|
32
|
+
results,
|
|
33
|
+
loading: searchLoading,
|
|
34
|
+
error: searchError,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}, [results, searchLoading, searchError, onSearchStateChange]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (results && onSearch) {
|
|
40
|
+
onSearch(query, results);
|
|
41
|
+
}
|
|
42
|
+
}, [results, query, onSearch]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (onQueryChange) {
|
|
45
|
+
onQueryChange(query);
|
|
46
|
+
}
|
|
47
|
+
}, [query, onQueryChange]);
|
|
48
|
+
const handleSearch = useCallback(async (searchQuery) => {
|
|
49
|
+
// Allow empty queries - use empty string for search
|
|
50
|
+
const query = searchQuery.trim();
|
|
51
|
+
log.info('SearchBar: Triggering search', { query, originalQuery: searchQuery, isEmpty: !query });
|
|
52
|
+
// Set flag to prevent blur handler from interfering
|
|
53
|
+
isSearchingRef.current = true;
|
|
54
|
+
// Close suggestions first
|
|
55
|
+
setIsFocused(false);
|
|
56
|
+
setSelectedIndex(-1);
|
|
57
|
+
// Update query in state manager (don't trigger search yet - we'll do it explicitly)
|
|
58
|
+
setQuery(query, false);
|
|
59
|
+
// Explicitly trigger search immediately (bypass debounce for Enter key)
|
|
60
|
+
log.info('SearchBar: Calling search() directly');
|
|
61
|
+
await triggerSearch();
|
|
62
|
+
// Blur after a small delay to ensure search is triggered
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
inputRef.current?.blur();
|
|
65
|
+
// Reset flag after blur
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
isSearchingRef.current = false;
|
|
68
|
+
}, 50);
|
|
69
|
+
}, 100);
|
|
70
|
+
}, [setQuery, triggerSearch]);
|
|
71
|
+
const handleSuggestionSelect = useCallback((suggestion) => {
|
|
72
|
+
log.verbose('SearchBar: Suggestion selected', { suggestion });
|
|
73
|
+
setSelectedIndex(-1);
|
|
74
|
+
setIsFocused(false);
|
|
75
|
+
inputRef.current?.blur();
|
|
76
|
+
if (onSuggestionSelect) {
|
|
77
|
+
onSuggestionSelect(suggestion);
|
|
78
|
+
}
|
|
79
|
+
// Update query and trigger search
|
|
80
|
+
setQuery(suggestion);
|
|
81
|
+
}, [onSuggestionSelect, setQuery]);
|
|
82
|
+
const handleInputChange = useCallback((e) => {
|
|
83
|
+
const value = e.target.value;
|
|
84
|
+
// Update query in state manager but don't trigger search immediately
|
|
85
|
+
// Search will be triggered on Enter or suggestion select
|
|
86
|
+
setQuery(value, false); // false = don't trigger search immediately
|
|
87
|
+
setSelectedIndex(-1);
|
|
88
|
+
}, [setQuery]);
|
|
89
|
+
const handleKeyDown = useCallback((e) => {
|
|
90
|
+
switch (e.key) {
|
|
91
|
+
case 'ArrowDown':
|
|
92
|
+
if (showSuggestions && displayedSuggestions.length > 0) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setSelectedIndex((prev) => prev < displayedSuggestions.length - 1 ? prev + 1 : prev);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 'ArrowUp':
|
|
98
|
+
if (showSuggestions && displayedSuggestions.length > 0) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
case 'Enter':
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
// Only select suggestion if one is explicitly selected (selectedIndex >= 0)
|
|
107
|
+
// Otherwise, search whatever is in the text box
|
|
108
|
+
if (showSuggestions && displayedSuggestions.length > 0 && selectedIndex >= 0 && selectedIndex < displayedSuggestions.length) {
|
|
109
|
+
// Select suggestion if one is highlighted
|
|
110
|
+
handleSuggestionSelect(displayedSuggestions[selectedIndex].query);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Search whatever is in the text box (even if suggestions are shown)
|
|
114
|
+
// Allow empty search - will use "*" for wildcard
|
|
115
|
+
const currentValue = inputRef.current?.value || query;
|
|
116
|
+
handleSearch(currentValue);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case 'Escape':
|
|
120
|
+
setIsFocused(false);
|
|
121
|
+
inputRef.current?.blur();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}, [displayedSuggestions, selectedIndex, query, showSuggestions, handleSearch, handleSuggestionSelect]);
|
|
125
|
+
const handleFocus = useCallback(() => {
|
|
126
|
+
setIsFocused(true);
|
|
127
|
+
}, []);
|
|
128
|
+
const handleBlur = useCallback((e) => {
|
|
129
|
+
// Don't interfere if we're explicitly triggering a search
|
|
130
|
+
if (isSearchingRef.current) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Delay to allow click events on suggestions to fire
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (!containerRef.current?.contains(document.activeElement)) {
|
|
136
|
+
setIsFocused(false);
|
|
137
|
+
setSelectedIndex(-1);
|
|
138
|
+
}
|
|
139
|
+
}, 200);
|
|
140
|
+
}, []);
|
|
141
|
+
const defaultRenderSuggestion = (suggestion, index) => (React.createElement("div", { key: index }, suggestion));
|
|
142
|
+
const defaultRenderLoading = () => (React.createElement("div", { style: { padding: theme.spacing.medium, textAlign: 'center' } }, "Loading suggestions..."));
|
|
143
|
+
const searchBarTheme = customTheme || {};
|
|
144
|
+
const isLoading = suggestionsLoading || searchLoading;
|
|
145
|
+
// Only show suggestions list if:
|
|
146
|
+
// 1. Input is focused
|
|
147
|
+
// 2. Suggestions are enabled
|
|
148
|
+
// 3. Query is long enough
|
|
149
|
+
// 4. AND (we have suggestions to show OR we're currently loading suggestions)
|
|
150
|
+
const hasSuggestions = displayedSuggestions.length > 0;
|
|
151
|
+
const showSuggestionsList = isFocused && showSuggestions && query.length >= minQueryLength && (hasSuggestions || isLoading);
|
|
152
|
+
// Get processing time from results
|
|
153
|
+
const res = results;
|
|
154
|
+
const processingTime = res?.processingTimeMS
|
|
155
|
+
|| res?.data?.processingTimeMS
|
|
156
|
+
|| res?.data?.data?.processingTimeMS;
|
|
157
|
+
return (React.createElement("div", { ref: containerRef, className: clsx(searchBarTheme.container, className), style: {
|
|
158
|
+
position: 'relative',
|
|
159
|
+
display: 'flex',
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
...style,
|
|
162
|
+
} },
|
|
163
|
+
React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, className: clsx(searchBarTheme.input, isFocused && searchBarTheme.inputFocused), style: {
|
|
164
|
+
flex: 1,
|
|
165
|
+
padding: theme.spacing.medium,
|
|
166
|
+
fontSize: theme.typography.fontSize.medium,
|
|
167
|
+
fontFamily: theme.typography.fontFamily,
|
|
168
|
+
backgroundColor: theme.colors.background,
|
|
169
|
+
color: theme.colors.text,
|
|
170
|
+
borderWidth: '1px',
|
|
171
|
+
borderStyle: 'solid',
|
|
172
|
+
borderColor: isFocused ? theme.colors.focus : theme.colors.border,
|
|
173
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
174
|
+
outline: 'none',
|
|
175
|
+
...(isFocused && {
|
|
176
|
+
boxShadow: theme.shadows.small,
|
|
177
|
+
}),
|
|
178
|
+
} }),
|
|
179
|
+
processingTime !== undefined && (React.createElement("span", { style: {
|
|
180
|
+
marginLeft: theme.spacing.small,
|
|
181
|
+
fontSize: theme.typography.fontSize.small,
|
|
182
|
+
color: theme.colors.text,
|
|
183
|
+
opacity: 0.7,
|
|
184
|
+
whiteSpace: 'nowrap',
|
|
185
|
+
} },
|
|
186
|
+
processingTime,
|
|
187
|
+
"ms")),
|
|
188
|
+
showSuggestionsList && (React.createElement("div", { className: searchBarTheme.suggestionsContainer, style: {
|
|
189
|
+
position: 'absolute',
|
|
190
|
+
top: '100%',
|
|
191
|
+
left: 0,
|
|
192
|
+
right: 0,
|
|
193
|
+
marginTop: theme.spacing.small,
|
|
194
|
+
backgroundColor: theme.colors.background,
|
|
195
|
+
border: `1px solid ${theme.colors.border}`,
|
|
196
|
+
borderRadius: typeof theme.borderRadius === 'string' ? theme.borderRadius : theme.borderRadius.medium,
|
|
197
|
+
boxShadow: theme.shadows.medium,
|
|
198
|
+
maxHeight: '400px',
|
|
199
|
+
overflowY: 'auto',
|
|
200
|
+
zIndex: 1000,
|
|
201
|
+
} },
|
|
202
|
+
isLoading && (renderLoading ? renderLoading() : defaultRenderLoading()),
|
|
203
|
+
!isLoading && displayedSuggestions.length > 0 && (React.createElement(React.Fragment, null, displayedSuggestions.map((suggestion, index) => {
|
|
204
|
+
const isSelected = index === selectedIndex;
|
|
205
|
+
const renderFn = renderSuggestion || defaultRenderSuggestion;
|
|
206
|
+
return (React.createElement("div", { key: index, className: clsx(searchBarTheme.suggestionItem, isSelected && searchBarTheme.suggestionItemActive), onClick: () => handleSuggestionSelect(suggestion.query), onMouseEnter: () => setSelectedIndex(index), style: {
|
|
207
|
+
padding: theme.spacing.medium,
|
|
208
|
+
cursor: 'pointer',
|
|
209
|
+
backgroundColor: isSelected
|
|
210
|
+
? theme.colors.hover
|
|
211
|
+
: 'transparent',
|
|
212
|
+
color: theme.colors.text,
|
|
213
|
+
transition: theme.transitions?.fast || '150ms ease-in-out',
|
|
214
|
+
} }, renderFn(suggestion.query, index)));
|
|
215
|
+
}))),
|
|
216
|
+
!isLoading && displayedSuggestions.length === 0 && query.length >= minQueryLength && (React.createElement("div", { style: { padding: theme.spacing.medium, textAlign: 'center', color: theme.colors.textSecondary } }, "No suggestions found"))))));
|
|
217
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchBarWithSuggestions Component
|
|
3
|
+
*
|
|
4
|
+
* Integrated search bar with query suggestions dropdown.
|
|
5
|
+
* Combines SearchBar with any of the suggestion dropdown variants.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import type { SuggestionItem, ProductItem, QuerySuggestionsVariant, QuerySuggestionsClassNames } from '@seekora-ai/ui-sdk-types';
|
|
9
|
+
export interface SearchBarWithSuggestionsProps {
|
|
10
|
+
/** Dropdown variant to use */
|
|
11
|
+
variant?: QuerySuggestionsVariant;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Initial query value */
|
|
15
|
+
initialQuery?: string;
|
|
16
|
+
/** Controlled query value */
|
|
17
|
+
value?: string;
|
|
18
|
+
/** Callback when query changes */
|
|
19
|
+
onQueryChange?: (query: string) => void;
|
|
20
|
+
/** Callback when search is submitted */
|
|
21
|
+
onSearch?: (query: string) => void;
|
|
22
|
+
/** Callback when suggestion is selected */
|
|
23
|
+
onSuggestionSelect?: (suggestion: SuggestionItem) => void;
|
|
24
|
+
/** Callback when product is clicked */
|
|
25
|
+
onProductClick?: (product: ProductItem) => void;
|
|
26
|
+
/** Show search button */
|
|
27
|
+
showSearchButton?: boolean;
|
|
28
|
+
/** Search button text */
|
|
29
|
+
searchButtonText?: string;
|
|
30
|
+
/** Show clear button */
|
|
31
|
+
showClearButton?: boolean;
|
|
32
|
+
/** Auto focus on mount */
|
|
33
|
+
autoFocus?: boolean;
|
|
34
|
+
/** Min query length for suggestions */
|
|
35
|
+
minQueryLength?: number;
|
|
36
|
+
/** Max suggestions to show */
|
|
37
|
+
maxSuggestions?: number;
|
|
38
|
+
/** Debounce delay (ms) */
|
|
39
|
+
debounceMs?: number;
|
|
40
|
+
/** Show recent searches */
|
|
41
|
+
showRecentSearches?: boolean;
|
|
42
|
+
/** Show trending when empty */
|
|
43
|
+
showTrendingOnEmpty?: boolean;
|
|
44
|
+
/** Include dropdown recommendations */
|
|
45
|
+
includeDropdownRecommendations?: boolean;
|
|
46
|
+
/** Filtered tabs config */
|
|
47
|
+
filteredTabs?: Array<{
|
|
48
|
+
id?: string;
|
|
49
|
+
label: string;
|
|
50
|
+
filter: string;
|
|
51
|
+
}>;
|
|
52
|
+
/** Enable analytics tracking */
|
|
53
|
+
enableAnalytics?: boolean;
|
|
54
|
+
/** Analytics tags */
|
|
55
|
+
analyticsTags?: string[];
|
|
56
|
+
/** Dropdown width */
|
|
57
|
+
dropdownWidth?: string | number;
|
|
58
|
+
/** Dropdown max height */
|
|
59
|
+
dropdownMaxHeight?: string | number;
|
|
60
|
+
/** Custom class names */
|
|
61
|
+
classNames?: {
|
|
62
|
+
wrapper?: string;
|
|
63
|
+
input?: string;
|
|
64
|
+
button?: string;
|
|
65
|
+
clearButton?: string;
|
|
66
|
+
dropdown?: string;
|
|
67
|
+
} & QuerySuggestionsClassNames;
|
|
68
|
+
/** Custom styles */
|
|
69
|
+
style?: React.CSSProperties;
|
|
70
|
+
/** Input style */
|
|
71
|
+
inputStyle?: React.CSSProperties;
|
|
72
|
+
/** ARIA label */
|
|
73
|
+
ariaLabel?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface SearchBarWithSuggestionsRef {
|
|
76
|
+
/** Focus the input */
|
|
77
|
+
focus: () => void;
|
|
78
|
+
/** Blur the input */
|
|
79
|
+
blur: () => void;
|
|
80
|
+
/** Clear the input */
|
|
81
|
+
clear: () => void;
|
|
82
|
+
/** Get current query */
|
|
83
|
+
getQuery: () => string;
|
|
84
|
+
/** Set query value */
|
|
85
|
+
setQuery: (query: string) => void;
|
|
86
|
+
/** Open dropdown */
|
|
87
|
+
openDropdown: () => void;
|
|
88
|
+
/** Close dropdown */
|
|
89
|
+
closeDropdown: () => void;
|
|
90
|
+
/** Navigate to next suggestion */
|
|
91
|
+
navigateNext: () => void;
|
|
92
|
+
/** Navigate to previous suggestion */
|
|
93
|
+
navigatePrevious: () => void;
|
|
94
|
+
/** Select current suggestion */
|
|
95
|
+
selectCurrent: () => void;
|
|
96
|
+
}
|
|
97
|
+
export declare const SearchBarWithSuggestions: React.ForwardRefExoticComponent<SearchBarWithSuggestionsProps & React.RefAttributes<SearchBarWithSuggestionsRef>>;
|
|
98
|
+
export default SearchBarWithSuggestions;
|
|
99
|
+
//# sourceMappingURL=SearchBarWithSuggestions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchBarWithSuggestions.d.ts","sourceRoot":"","sources":["../../src/components/SearchBarWithSuggestions.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchBarWithSuggestions Component
|
|
3
|
+
*
|
|
4
|
+
* Integrated search bar with query suggestions dropdown.
|
|
5
|
+
* Combines SearchBar with any of the suggestion dropdown variants.
|
|
6
|
+
*/
|
|
7
|
+
import React, { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle, } from 'react';
|
|
8
|
+
import { useSearchContext } from './SearchProvider';
|
|
9
|
+
import { QuerySuggestionsDropdown } from './QuerySuggestionsDropdown';
|
|
10
|
+
import { RichQuerySuggestions } from './RichQuerySuggestions';
|
|
11
|
+
import { FederatedDropdown } from './FederatedDropdown';
|
|
12
|
+
import { useSuggestionsAnalytics } from '../hooks/useSuggestionsAnalytics';
|
|
13
|
+
import { clsx } from 'clsx';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Styles
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const styles = {
|
|
18
|
+
wrapper: {
|
|
19
|
+
position: 'relative',
|
|
20
|
+
width: '100%',
|
|
21
|
+
},
|
|
22
|
+
inputWrapper: {
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
backgroundColor: 'var(--seekora-bg-surface, #ffffff)',
|
|
26
|
+
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
27
|
+
borderRadius: 'var(--seekora-border-radius-lg, 8px)',
|
|
28
|
+
overflow: 'hidden',
|
|
29
|
+
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
|
30
|
+
},
|
|
31
|
+
inputWrapperFocused: {
|
|
32
|
+
borderColor: 'var(--seekora-primary, #3b82f6)',
|
|
33
|
+
boxShadow: '0 0 0 3px var(--seekora-primary-light, rgba(59, 130, 246, 0.1))',
|
|
34
|
+
},
|
|
35
|
+
input: {
|
|
36
|
+
flex: 1,
|
|
37
|
+
padding: '12px 16px',
|
|
38
|
+
fontSize: '16px',
|
|
39
|
+
border: 'none',
|
|
40
|
+
outline: 'none',
|
|
41
|
+
backgroundColor: 'transparent',
|
|
42
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
43
|
+
fontFamily: 'inherit',
|
|
44
|
+
},
|
|
45
|
+
searchIcon: {
|
|
46
|
+
width: '20px',
|
|
47
|
+
height: '20px',
|
|
48
|
+
color: 'var(--seekora-text-secondary, #9ca3af)',
|
|
49
|
+
marginLeft: '12px',
|
|
50
|
+
flexShrink: 0,
|
|
51
|
+
},
|
|
52
|
+
clearButton: {
|
|
53
|
+
display: 'flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
justifyContent: 'center',
|
|
56
|
+
width: '32px',
|
|
57
|
+
height: '32px',
|
|
58
|
+
marginRight: '4px',
|
|
59
|
+
padding: 0,
|
|
60
|
+
border: 'none',
|
|
61
|
+
backgroundColor: 'transparent',
|
|
62
|
+
color: 'var(--seekora-text-secondary, #9ca3af)',
|
|
63
|
+
borderRadius: '50%',
|
|
64
|
+
cursor: 'pointer',
|
|
65
|
+
transition: 'background-color 150ms ease, color 150ms ease',
|
|
66
|
+
},
|
|
67
|
+
searchButton: {
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
gap: '8px',
|
|
72
|
+
padding: '12px 20px',
|
|
73
|
+
margin: '4px',
|
|
74
|
+
border: 'none',
|
|
75
|
+
backgroundColor: 'var(--seekora-primary, #3b82f6)',
|
|
76
|
+
color: 'white',
|
|
77
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
78
|
+
fontSize: '14px',
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
cursor: 'pointer',
|
|
81
|
+
transition: 'background-color 150ms ease',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Icons
|
|
86
|
+
// ============================================================================
|
|
87
|
+
const SearchIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", style: styles.searchIcon },
|
|
88
|
+
React.createElement("path", { fillRule: "evenodd", d: "M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z", clipRule: "evenodd" })));
|
|
89
|
+
const ClearIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", style: { width: 16, height: 16 } },
|
|
90
|
+
React.createElement("path", { fillRule: "evenodd", d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z", clipRule: "evenodd" })));
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Component
|
|
93
|
+
// ============================================================================
|
|
94
|
+
export const SearchBarWithSuggestions = forwardRef(function SearchBarWithSuggestions(props, ref) {
|
|
95
|
+
const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
|
|
96
|
+
const { client } = useSearchContext();
|
|
97
|
+
const inputRef = useRef(null);
|
|
98
|
+
const dropdownRef = useRef(null);
|
|
99
|
+
const [query, setQuery] = useState(value ?? initialQuery);
|
|
100
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
101
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
102
|
+
// Analytics
|
|
103
|
+
const analytics = useSuggestionsAnalytics({
|
|
104
|
+
client,
|
|
105
|
+
enabled: enableAnalytics,
|
|
106
|
+
analyticsTags,
|
|
107
|
+
});
|
|
108
|
+
// Sync controlled value
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (value !== undefined && value !== query) {
|
|
111
|
+
setQuery(value);
|
|
112
|
+
}
|
|
113
|
+
}, [value]);
|
|
114
|
+
// Handle query change
|
|
115
|
+
const handleQueryChange = useCallback((e) => {
|
|
116
|
+
const newQuery = e.target.value;
|
|
117
|
+
setQuery(newQuery);
|
|
118
|
+
onQueryChange?.(newQuery);
|
|
119
|
+
// Open dropdown when typing
|
|
120
|
+
if (newQuery.length >= minQueryLength || (showTrendingOnEmpty && newQuery.length === 0)) {
|
|
121
|
+
setIsDropdownOpen(true);
|
|
122
|
+
}
|
|
123
|
+
}, [onQueryChange, minQueryLength, showTrendingOnEmpty]);
|
|
124
|
+
// Handle search submit
|
|
125
|
+
const handleSubmit = useCallback((e) => {
|
|
126
|
+
e?.preventDefault();
|
|
127
|
+
if (query.trim()) {
|
|
128
|
+
onSearch?.(query.trim());
|
|
129
|
+
analytics.trackSearchSubmit(query.trim(), false);
|
|
130
|
+
setIsDropdownOpen(false);
|
|
131
|
+
}
|
|
132
|
+
}, [query, onSearch, analytics]);
|
|
133
|
+
// Handle suggestion select
|
|
134
|
+
const handleSuggestionSelect = useCallback((suggestion) => {
|
|
135
|
+
setQuery(suggestion.query);
|
|
136
|
+
onQueryChange?.(suggestion.query);
|
|
137
|
+
onSuggestionSelect?.(suggestion);
|
|
138
|
+
analytics.trackSuggestionClick({
|
|
139
|
+
suggestion,
|
|
140
|
+
position: 0, // Would need to track actual position
|
|
141
|
+
query,
|
|
142
|
+
});
|
|
143
|
+
setIsDropdownOpen(false);
|
|
144
|
+
// Optionally trigger search
|
|
145
|
+
onSearch?.(suggestion.query);
|
|
146
|
+
}, [query, onQueryChange, onSuggestionSelect, onSearch, analytics]);
|
|
147
|
+
// Handle product click
|
|
148
|
+
const handleProductClick = useCallback((product) => {
|
|
149
|
+
onProductClick?.(product);
|
|
150
|
+
analytics.trackProductClick({
|
|
151
|
+
product,
|
|
152
|
+
position: 0,
|
|
153
|
+
query,
|
|
154
|
+
});
|
|
155
|
+
setIsDropdownOpen(false);
|
|
156
|
+
}, [query, onProductClick, analytics]);
|
|
157
|
+
// Handle recent search click
|
|
158
|
+
const handleRecentSearchClick = useCallback((search) => {
|
|
159
|
+
setQuery(search.query);
|
|
160
|
+
onQueryChange?.(search.query);
|
|
161
|
+
analytics.trackRecentSearchClick(search);
|
|
162
|
+
setIsDropdownOpen(false);
|
|
163
|
+
onSearch?.(search.query);
|
|
164
|
+
}, [onQueryChange, onSearch, analytics]);
|
|
165
|
+
// Handle focus
|
|
166
|
+
const handleFocus = useCallback(() => {
|
|
167
|
+
setIsFocused(true);
|
|
168
|
+
setIsDropdownOpen(true);
|
|
169
|
+
analytics.trackDropdownOpen(query);
|
|
170
|
+
}, [query, analytics]);
|
|
171
|
+
// Handle blur
|
|
172
|
+
const handleBlur = useCallback(() => {
|
|
173
|
+
setIsFocused(false);
|
|
174
|
+
// Delay closing to allow click events on dropdown
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
setIsDropdownOpen(false);
|
|
177
|
+
analytics.trackDropdownClose(query);
|
|
178
|
+
}, 200);
|
|
179
|
+
}, [query, analytics]);
|
|
180
|
+
// Handle clear
|
|
181
|
+
const handleClear = useCallback(() => {
|
|
182
|
+
setQuery('');
|
|
183
|
+
onQueryChange?.('');
|
|
184
|
+
inputRef.current?.focus();
|
|
185
|
+
}, [onQueryChange]);
|
|
186
|
+
// Handle keyboard navigation
|
|
187
|
+
const handleKeyDown = useCallback((e) => {
|
|
188
|
+
if (!isDropdownOpen) {
|
|
189
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
190
|
+
setIsDropdownOpen(true);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
switch (e.key) {
|
|
195
|
+
case 'ArrowDown':
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
dropdownRef.current?.navigateNext?.();
|
|
198
|
+
break;
|
|
199
|
+
case 'ArrowUp':
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
dropdownRef.current?.navigatePrevious?.();
|
|
202
|
+
break;
|
|
203
|
+
case 'Enter':
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
if ((dropdownRef.current?.getActiveIndex?.() ?? -1) >= 0) {
|
|
206
|
+
dropdownRef.current?.selectActive?.();
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
handleSubmit();
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case 'Escape':
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
setIsDropdownOpen(false);
|
|
215
|
+
inputRef.current?.blur();
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}, [isDropdownOpen, handleSubmit]);
|
|
219
|
+
// Expose ref methods
|
|
220
|
+
useImperativeHandle(ref, () => ({
|
|
221
|
+
focus: () => inputRef.current?.focus(),
|
|
222
|
+
blur: () => inputRef.current?.blur(),
|
|
223
|
+
clear: handleClear,
|
|
224
|
+
getQuery: () => query,
|
|
225
|
+
setQuery: (newQuery) => {
|
|
226
|
+
setQuery(newQuery);
|
|
227
|
+
onQueryChange?.(newQuery);
|
|
228
|
+
},
|
|
229
|
+
openDropdown: () => setIsDropdownOpen(true),
|
|
230
|
+
closeDropdown: () => setIsDropdownOpen(false),
|
|
231
|
+
navigateNext: () => dropdownRef.current?.navigateNext?.(),
|
|
232
|
+
navigatePrevious: () => dropdownRef.current?.navigatePrevious?.(),
|
|
233
|
+
selectCurrent: () => dropdownRef.current?.selectActive?.(),
|
|
234
|
+
}), [query, handleClear, onQueryChange]);
|
|
235
|
+
// Render dropdown based on variant
|
|
236
|
+
const renderDropdown = () => {
|
|
237
|
+
const commonProps = {
|
|
238
|
+
query,
|
|
239
|
+
isOpen: isDropdownOpen,
|
|
240
|
+
maxSuggestions,
|
|
241
|
+
minQueryLength,
|
|
242
|
+
debounceMs,
|
|
243
|
+
showRecentSearches,
|
|
244
|
+
classNames,
|
|
245
|
+
onSuggestionSelect: handleSuggestionSelect,
|
|
246
|
+
onRecentSearchClick: handleRecentSearchClick,
|
|
247
|
+
onClose: () => setIsDropdownOpen(false),
|
|
248
|
+
analyticsTags,
|
|
249
|
+
};
|
|
250
|
+
switch (variant) {
|
|
251
|
+
case 'rich':
|
|
252
|
+
return (React.createElement(RichQuerySuggestions, { ref: dropdownRef, ...commonProps, includeDropdownRecommendations: includeDropdownRecommendations, includeCategories: true, width: dropdownWidth || '100%', maxHeight: dropdownMaxHeight || '480px' }));
|
|
253
|
+
case 'federated':
|
|
254
|
+
return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px' }));
|
|
255
|
+
case 'compact':
|
|
256
|
+
return (React.createElement(QuerySuggestionsDropdown, { ref: dropdownRef, ...commonProps, maxSuggestions: 5, showCounts: false, width: dropdownWidth || '100%' }));
|
|
257
|
+
case 'classic':
|
|
258
|
+
default:
|
|
259
|
+
return (React.createElement(QuerySuggestionsDropdown, { ref: dropdownRef, ...commonProps, width: dropdownWidth || '100%' }));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return (React.createElement("div", { className: clsx('seekora-search-bar-with-suggestions', classNames.wrapper), style: { ...styles.wrapper, ...style } },
|
|
263
|
+
React.createElement("form", { onSubmit: handleSubmit },
|
|
264
|
+
React.createElement("div", { style: {
|
|
265
|
+
...styles.inputWrapper,
|
|
266
|
+
...(isFocused ? styles.inputWrapperFocused : {}),
|
|
267
|
+
} },
|
|
268
|
+
React.createElement(SearchIcon, null),
|
|
269
|
+
React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleQueryChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder, autoFocus: autoFocus, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-label": ariaLabel, "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-autocomplete": "list", role: "combobox", className: classNames.input, style: { ...styles.input, ...inputStyle } }),
|
|
270
|
+
showClearButton && query && (React.createElement("button", { type: "button", onClick: handleClear, className: classNames.clearButton, style: styles.clearButton, "aria-label": "Clear search" },
|
|
271
|
+
React.createElement(ClearIcon, null))),
|
|
272
|
+
showSearchButton && (React.createElement("button", { type: "submit", className: classNames.button, style: styles.searchButton }, searchButtonText)))),
|
|
273
|
+
React.createElement("div", { className: classNames.dropdown }, renderDropdown())));
|
|
274
|
+
});
|
|
275
|
+
export default SearchBarWithSuggestions;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchLayout Component
|
|
3
|
+
*
|
|
4
|
+
* Provides a layout structure for search interfaces with sidebar and main content
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
export interface SearchLayoutTheme {
|
|
8
|
+
container?: string;
|
|
9
|
+
sidebar?: string;
|
|
10
|
+
main?: string;
|
|
11
|
+
header?: string;
|
|
12
|
+
footer?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchLayoutProps {
|
|
15
|
+
/** Content to render in the sidebar (filters, facets, etc.) */
|
|
16
|
+
sidebar?: React.ReactNode;
|
|
17
|
+
/** Content to render in the main area (results, etc.) */
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
/** Header content */
|
|
20
|
+
header?: React.ReactNode;
|
|
21
|
+
/** Footer content */
|
|
22
|
+
footer?: React.ReactNode;
|
|
23
|
+
/** Sidebar width */
|
|
24
|
+
sidebarWidth?: string;
|
|
25
|
+
/** Custom className */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Custom styles */
|
|
28
|
+
style?: React.CSSProperties;
|
|
29
|
+
/** Custom theme */
|
|
30
|
+
theme?: SearchLayoutTheme;
|
|
31
|
+
/** Show sidebar on mobile */
|
|
32
|
+
showSidebarOnMobile?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const SearchLayout: React.FC<SearchLayoutProps>;
|
|
35
|
+
//# sourceMappingURL=SearchLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchLayout.d.ts","sourceRoot":"","sources":["../../src/components/SearchLayout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,qBAAqB;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,qBAAqB;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,mBAAmB;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,6BAA6B;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAkGpD,CAAC"}
|