@seekora-ai/ui-sdk-react 0.2.5 → 0.2.8
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/SearchBarWithSuggestions.d.ts +2 -0
- package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -1
- package/dist/components/SearchBarWithSuggestions.js +2 -2
- package/dist/components/primitives/ImageDisplay.d.ts +19 -0
- package/dist/components/primitives/ImageDisplay.d.ts.map +1 -0
- package/dist/components/primitives/ImageDisplay.js +74 -0
- package/dist/components/primitives/index.d.ts +2 -0
- package/dist/components/primitives/index.d.ts.map +1 -0
- package/dist/components/primitives/index.js +1 -0
- package/dist/components/product-page/ProductGallery.d.ts +19 -0
- package/dist/components/product-page/ProductGallery.d.ts.map +1 -0
- package/dist/components/product-page/ProductGallery.js +13 -0
- package/dist/components/product-page/ProductInfo.d.ts +21 -0
- package/dist/components/product-page/ProductInfo.d.ts.map +1 -0
- package/dist/components/product-page/ProductInfo.js +19 -0
- package/dist/components/product-page/ProductRecommendations.d.ts +21 -0
- package/dist/components/product-page/ProductRecommendations.d.ts.map +1 -0
- package/dist/components/product-page/ProductRecommendations.js +17 -0
- package/dist/components/product-page/index.d.ts +4 -0
- package/dist/components/product-page/index.d.ts.map +1 -0
- package/dist/components/product-page/index.js +3 -0
- package/dist/components/section-primitives/SectionError.d.ts +11 -0
- package/dist/components/section-primitives/SectionError.d.ts.map +1 -0
- package/dist/components/section-primitives/SectionError.js +13 -0
- package/dist/components/section-primitives/SectionItemGrid.d.ts +18 -0
- package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -0
- package/dist/components/section-primitives/SectionItemGrid.js +16 -0
- package/dist/components/section-primitives/SectionLoading.d.ts +11 -0
- package/dist/components/section-primitives/SectionLoading.d.ts.map +1 -0
- package/dist/components/section-primitives/SectionLoading.js +11 -0
- package/dist/components/section-primitives/SectionSearchContext.d.ts +17 -0
- package/dist/components/section-primitives/SectionSearchContext.d.ts.map +1 -0
- package/dist/components/section-primitives/SectionSearchContext.js +17 -0
- package/dist/components/section-primitives/SectionSearchProvider.d.ts +23 -0
- package/dist/components/section-primitives/SectionSearchProvider.d.ts.map +1 -0
- package/dist/components/section-primitives/SectionSearchProvider.js +105 -0
- package/dist/components/section-primitives/index.d.ts +6 -0
- package/dist/components/section-primitives/index.d.ts.map +1 -0
- package/dist/components/section-primitives/index.js +5 -0
- package/dist/components/suggestions-primitives/CategoriesTabs.d.ts +13 -0
- package/dist/components/suggestions-primitives/CategoriesTabs.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/CategoriesTabs.js +35 -0
- package/dist/components/suggestions-primitives/DropdownPanel.d.ts +24 -0
- package/dist/components/suggestions-primitives/DropdownPanel.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/DropdownPanel.js +54 -0
- package/dist/components/suggestions-primitives/ItemCard.d.ts +39 -0
- package/dist/components/suggestions-primitives/ItemCard.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ItemCard.js +52 -0
- package/dist/components/suggestions-primitives/ItemGrid.d.ts +28 -0
- package/dist/components/suggestions-primitives/ItemGrid.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ItemGrid.js +42 -0
- package/dist/components/suggestions-primitives/ProductCard.d.ts +21 -0
- package/dist/components/suggestions-primitives/ProductCard.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ProductCard.js +46 -0
- package/dist/components/suggestions-primitives/ProductGrid.d.ts +17 -0
- package/dist/components/suggestions-primitives/ProductGrid.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/ProductGrid.js +36 -0
- package/dist/components/suggestions-primitives/RecentSearchesList.d.ts +17 -0
- package/dist/components/suggestions-primitives/RecentSearchesList.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/RecentSearchesList.js +39 -0
- package/dist/components/suggestions-primitives/SearchInput.d.ts +23 -0
- package/dist/components/suggestions-primitives/SearchInput.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SearchInput.js +95 -0
- package/dist/components/suggestions-primitives/SuggestionItem.d.ts +18 -0
- package/dist/components/suggestions-primitives/SuggestionItem.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionItem.js +34 -0
- package/dist/components/suggestions-primitives/SuggestionList.d.ts +15 -0
- package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionList.js +36 -0
- package/dist/components/suggestions-primitives/SuggestionsContext.d.ts +41 -0
- package/dist/components/suggestions-primitives/SuggestionsContext.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionsContext.js +18 -0
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts +24 -0
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +32 -0
- package/dist/components/suggestions-primitives/SuggestionsError.d.ts +11 -0
- package/dist/components/suggestions-primitives/SuggestionsError.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionsError.js +19 -0
- package/dist/components/suggestions-primitives/SuggestionsLoading.d.ts +11 -0
- package/dist/components/suggestions-primitives/SuggestionsLoading.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionsLoading.js +17 -0
- package/dist/components/suggestions-primitives/SuggestionsProvider.d.ts +38 -0
- package/dist/components/suggestions-primitives/SuggestionsProvider.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/SuggestionsProvider.js +222 -0
- package/dist/components/suggestions-primitives/TrendingList.d.ts +17 -0
- package/dist/components/suggestions-primitives/TrendingList.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/TrendingList.js +41 -0
- package/dist/components/suggestions-primitives/index.d.ts +39 -0
- package/dist/components/suggestions-primitives/index.d.ts.map +1 -0
- package/dist/components/suggestions-primitives/index.js +24 -0
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestionsEnhanced.js +30 -8
- package/dist/hooks/useSuggestionsAnalytics.d.ts +6 -6
- package/dist/hooks/useSuggestionsAnalytics.js +6 -6
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +630 -130
- package/dist/src/index.esm.js +1110 -43
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1134 -42
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/index.js
CHANGED
|
@@ -4536,15 +4536,16 @@ function parseHighlight(highlighted) {
|
|
|
4536
4536
|
.replace(/__\/ais-highlight__/g, '</mark>');
|
|
4537
4537
|
}
|
|
4538
4538
|
function transformProduct(raw) {
|
|
4539
|
+
const meta = raw.metadata || {};
|
|
4539
4540
|
return {
|
|
4540
4541
|
id: raw.id || raw.objectID,
|
|
4541
4542
|
objectID: raw.objectID || raw.id,
|
|
4542
|
-
title: raw.title || raw.name || raw.productName,
|
|
4543
|
-
name: raw.name || raw.title,
|
|
4544
|
-
image: raw.image || raw.imageUrl ||
|
|
4545
|
-
price: raw.price
|
|
4546
|
-
currency: raw.currency ||
|
|
4547
|
-
url: raw.url || raw.productId ||
|
|
4543
|
+
title: raw.title || raw.name || raw.productName || meta.name || meta.productName || '',
|
|
4544
|
+
name: raw.name || raw.title || meta.name || meta.productName || '',
|
|
4545
|
+
image: raw.image || raw.imageUrl || meta.image || meta.image_url || meta.images?.[0] || '',
|
|
4546
|
+
price: raw.price ?? raw.sellPrice ?? meta.sellPrice ?? meta.price,
|
|
4547
|
+
currency: raw.currency || meta.currency || '',
|
|
4548
|
+
url: raw.url || raw.productId || meta.url || meta.productId || '',
|
|
4548
4549
|
clicks: raw.clicks,
|
|
4549
4550
|
conversions: raw.conversions,
|
|
4550
4551
|
revenue: raw.revenue,
|
|
@@ -4594,7 +4595,10 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4594
4595
|
}, [enableRecentSearches, recentSearchesKey, maxRecentSearches]);
|
|
4595
4596
|
// Fetch suggestions
|
|
4596
4597
|
const fetchSuggestions = React.useCallback(async (searchQuery) => {
|
|
4597
|
-
if (!client
|
|
4598
|
+
if (!client)
|
|
4599
|
+
return;
|
|
4600
|
+
// When minQueryLength is 0, allow empty query so overlay can show default/trending recommendations on open
|
|
4601
|
+
if (!searchQuery.trim() && minQueryLength > 0) {
|
|
4598
4602
|
setSuggestions([]);
|
|
4599
4603
|
setDropdownRecommendations(null);
|
|
4600
4604
|
return;
|
|
@@ -4629,12 +4633,17 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4629
4633
|
setSuggestions(transformedSuggestions);
|
|
4630
4634
|
// Extract dropdown recommendations from extensions
|
|
4631
4635
|
const extensions = (response.extensions || {});
|
|
4636
|
+
const rawResults = response.results;
|
|
4637
|
+
const secondResultHits = Array.isArray(rawResults?.[1]?.hits) ? rawResults[1].hits : [];
|
|
4638
|
+
// Multi-index response: results[0]=suggestions, results[1]=product hits; expose results[1].hits as product_hits for fallback when extensions have no products
|
|
4639
|
+
const productHits = secondResultHits.length > 0 ? secondResultHits.map((h) => transformProduct(h)) : [];
|
|
4632
4640
|
const recommendations = {
|
|
4633
4641
|
trending_searches: Array.isArray(extensions.trending_searches) ? extensions.trending_searches : [],
|
|
4634
4642
|
top_searches: Array.isArray(extensions.top_searches) ? extensions.top_searches : [],
|
|
4635
4643
|
related_searches: Array.isArray(extensions.related_searches) ? extensions.related_searches : [],
|
|
4636
4644
|
trending_products: Array.isArray(extensions.trending_products) ? extensions.trending_products.map(transformProduct) : [],
|
|
4637
4645
|
item_recommendations: Array.isArray(extensions.item_recommendations) ? extensions.item_recommendations.map(transformProduct) : [],
|
|
4646
|
+
product_hits: productHits.length > 0 ? productHits : undefined,
|
|
4638
4647
|
popular_brands: Array.isArray(extensions.popular_brands) ? extensions.popular_brands : [],
|
|
4639
4648
|
filtered_tabs: Array.isArray(extensions.filtered_tabs) ? extensions.filtered_tabs.map(transformFilteredTab) : [],
|
|
4640
4649
|
processing_time_ms: typeof extensions.processing_time_ms === 'number' ? extensions.processing_time_ms : undefined,
|
|
@@ -4694,6 +4703,7 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4694
4703
|
}
|
|
4695
4704
|
}, [
|
|
4696
4705
|
client,
|
|
4706
|
+
minQueryLength,
|
|
4697
4707
|
maxSuggestions,
|
|
4698
4708
|
includeDropdownRecommendations,
|
|
4699
4709
|
includeCategories,
|
|
@@ -4770,7 +4780,19 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4770
4780
|
const relatedSearches = dropdownRecommendations?.related_searches || [];
|
|
4771
4781
|
const popularBrands = dropdownRecommendations?.popular_brands || [];
|
|
4772
4782
|
const filteredTabsResult = dropdownRecommendations?.filtered_tabs || [];
|
|
4773
|
-
|
|
4783
|
+
// Use trending_products, then item_recommendations, then first filtered_tab's products, then results[1].hits (product_hits) so grid shows for any API shape
|
|
4784
|
+
const trendingProducts = React.useMemo(() => {
|
|
4785
|
+
const fromTrending = dropdownRecommendations?.trending_products;
|
|
4786
|
+
if (fromTrending && fromTrending.length > 0)
|
|
4787
|
+
return fromTrending;
|
|
4788
|
+
const fromItemRecs = dropdownRecommendations?.item_recommendations;
|
|
4789
|
+
if (fromItemRecs && fromItemRecs.length > 0)
|
|
4790
|
+
return fromItemRecs;
|
|
4791
|
+
const firstTab = dropdownRecommendations?.filtered_tabs?.[0];
|
|
4792
|
+
if (firstTab?.products && firstTab.products.length > 0)
|
|
4793
|
+
return firstTab.products;
|
|
4794
|
+
return dropdownRecommendations?.product_hits ?? [];
|
|
4795
|
+
}, [dropdownRecommendations?.trending_products, dropdownRecommendations?.item_recommendations, dropdownRecommendations?.filtered_tabs, dropdownRecommendations?.product_hits]);
|
|
4774
4796
|
const hasContent = React.useMemo(() => {
|
|
4775
4797
|
return (suggestions.length > 0 ||
|
|
4776
4798
|
recentSearches.length > 0 ||
|
|
@@ -4858,7 +4880,7 @@ function useQuerySuggestionsEnhanced(options) {
|
|
|
4858
4880
|
// ============================================================================
|
|
4859
4881
|
// Styles
|
|
4860
4882
|
// ============================================================================
|
|
4861
|
-
const defaultStyles = {
|
|
4883
|
+
const defaultStyles$1 = {
|
|
4862
4884
|
container: {
|
|
4863
4885
|
backgroundColor: 'var(--seekora-bg-surface, #ffffff)',
|
|
4864
4886
|
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
@@ -5112,7 +5134,7 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5112
5134
|
return text;
|
|
5113
5135
|
}
|
|
5114
5136
|
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
|
|
5115
|
-
return parts.map((part, i) => part.toLowerCase() === searchQuery.toLowerCase() ? (React.createElement("mark", { key: i, className: classNames.suggestionItemHighlight, style: defaultStyles.highlight }, part)) : part);
|
|
5137
|
+
return parts.map((part, i) => part.toLowerCase() === searchQuery.toLowerCase() ? (React.createElement("mark", { key: i, className: classNames.suggestionItemHighlight, style: defaultStyles$1.highlight }, part)) : part);
|
|
5116
5138
|
}, [highlight.enabled, classNames.suggestionItemHighlight]);
|
|
5117
5139
|
// Parse highlighted query from API
|
|
5118
5140
|
const parseHighlightedQuery = React.useCallback((suggestion) => {
|
|
@@ -5128,30 +5150,30 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5128
5150
|
return renderSuggestion(suggestion, index, isActive, (text) => highlightText(text, query));
|
|
5129
5151
|
}
|
|
5130
5152
|
return (React.createElement("div", { style: {
|
|
5131
|
-
...defaultStyles.suggestionItem,
|
|
5132
|
-
...(isActive ? defaultStyles.suggestionItemActive : {}),
|
|
5153
|
+
...defaultStyles$1.suggestionItem,
|
|
5154
|
+
...(isActive ? defaultStyles$1.suggestionItemActive : {}),
|
|
5133
5155
|
} },
|
|
5134
|
-
React.createElement(SearchIcon$9, { style: defaultStyles.recentIcon }),
|
|
5135
|
-
React.createElement("span", { style: defaultStyles.suggestionQuery }, parseHighlightedQuery(suggestion)),
|
|
5136
|
-
showCounts && suggestion.count !== undefined && (React.createElement("span", { style: defaultStyles.suggestionCount }, suggestion.count.toLocaleString()))));
|
|
5156
|
+
React.createElement(SearchIcon$9, { style: defaultStyles$1.recentIcon }),
|
|
5157
|
+
React.createElement("span", { style: defaultStyles$1.suggestionQuery }, parseHighlightedQuery(suggestion)),
|
|
5158
|
+
showCounts && suggestion.count !== undefined && (React.createElement("span", { style: defaultStyles$1.suggestionCount }, suggestion.count.toLocaleString()))));
|
|
5137
5159
|
};
|
|
5138
5160
|
const renderRecentSearchItem = (search, index, isActive) => {
|
|
5139
5161
|
if (renderRecentSearch) {
|
|
5140
5162
|
return renderRecentSearch(search, index, isActive);
|
|
5141
5163
|
}
|
|
5142
5164
|
return (React.createElement("div", { style: {
|
|
5143
|
-
...defaultStyles.suggestionItem,
|
|
5144
|
-
...(isActive ? defaultStyles.suggestionItemActive : {}),
|
|
5165
|
+
...defaultStyles$1.suggestionItem,
|
|
5166
|
+
...(isActive ? defaultStyles$1.suggestionItemActive : {}),
|
|
5145
5167
|
} },
|
|
5146
|
-
React.createElement(ClockIcon$6, { style: defaultStyles.recentIcon }),
|
|
5147
|
-
React.createElement("span", { style: defaultStyles.suggestionQuery }, search.query),
|
|
5168
|
+
React.createElement(ClockIcon$6, { style: defaultStyles$1.recentIcon }),
|
|
5169
|
+
React.createElement("span", { style: defaultStyles$1.suggestionQuery }, search.query),
|
|
5148
5170
|
React.createElement("button", { type: "button", onClick: (e) => {
|
|
5149
5171
|
e.stopPropagation();
|
|
5150
5172
|
removeRecentSearch(search.query);
|
|
5151
5173
|
onRecentSearchRemove?.(search);
|
|
5152
5174
|
}, style: {
|
|
5153
|
-
...defaultStyles.removeButton,
|
|
5154
|
-
...(isActive ? defaultStyles.removeButtonVisible : {}),
|
|
5175
|
+
...defaultStyles$1.removeButton,
|
|
5176
|
+
...(isActive ? defaultStyles$1.removeButtonVisible : {}),
|
|
5155
5177
|
}, "aria-label": `Remove ${search.query} from recent searches` },
|
|
5156
5178
|
React.createElement(CloseIcon$1, null))));
|
|
5157
5179
|
};
|
|
@@ -5175,24 +5197,24 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5175
5197
|
position,
|
|
5176
5198
|
width,
|
|
5177
5199
|
zIndex,
|
|
5178
|
-
...defaultStyles.container,
|
|
5200
|
+
...defaultStyles$1.container,
|
|
5179
5201
|
...animationStyle,
|
|
5180
5202
|
...style,
|
|
5181
5203
|
} },
|
|
5182
|
-
loading && showLoading && (React.createElement("div", { className: classNames.loadingState, style: defaultStyles.loadingState }, renderLoading ? renderLoading() : (React.createElement(React.Fragment, null,
|
|
5204
|
+
loading && showLoading && (React.createElement("div", { className: classNames.loadingState, style: defaultStyles$1.loadingState }, renderLoading ? renderLoading() : (React.createElement(React.Fragment, null,
|
|
5183
5205
|
React.createElement(LoadingSpinner, null),
|
|
5184
5206
|
React.createElement("span", null, "Searching..."))))),
|
|
5185
5207
|
!loading && showRecent && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.recentSearches) },
|
|
5186
|
-
React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles.sectionTitle }, "Recent Searches"),
|
|
5208
|
+
React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Recent Searches"),
|
|
5187
5209
|
recentSearches.slice(0, maxRecentSearches).map((search, index) => {
|
|
5188
5210
|
const isActive = activeIndex === index;
|
|
5189
5211
|
return (React.createElement("div", { key: `recent-${search.query}-${index}`, role: "option", "aria-selected": isActive, className: clsx(classNames.recentItem, isActive && classNames.suggestionItemActive), onClick: () => {
|
|
5190
5212
|
onRecentSearchClick?.(search);
|
|
5191
5213
|
}, onMouseEnter: () => setActiveIndex(index) }, renderRecentSearchItem(search, index, isActive)));
|
|
5192
5214
|
}))),
|
|
5193
|
-
!loading && showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles.divider })),
|
|
5215
|
+
!loading && showRecent && showSuggestions && (React.createElement("div", { style: defaultStyles$1.divider })),
|
|
5194
5216
|
!loading && showSuggestions && (React.createElement("div", { className: clsx('seekora-suggestions-section', classNames.section, classNames.suggestionsList) },
|
|
5195
|
-
query.length > 0 && (React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles.sectionTitle }, "Suggestions")),
|
|
5217
|
+
query.length > 0 && (React.createElement("div", { className: classNames.sectionTitle, style: defaultStyles$1.sectionTitle }, "Suggestions")),
|
|
5196
5218
|
suggestions.map((suggestion, index) => {
|
|
5197
5219
|
const itemIndex = showRecent ? recentSearches.length + index : index;
|
|
5198
5220
|
const isActive = activeIndex === itemIndex;
|
|
@@ -5201,19 +5223,19 @@ const QuerySuggestionsDropdown = React.forwardRef(function QuerySuggestionsDropd
|
|
|
5201
5223
|
addRecentSearch(suggestion.query, suggestion.count);
|
|
5202
5224
|
}, onMouseEnter: () => setActiveIndex(itemIndex) }, renderSuggestionItem(suggestion, index, isActive)));
|
|
5203
5225
|
}))),
|
|
5204
|
-
showEmpty && (React.createElement("div", { className: classNames.emptyState, style: defaultStyles.emptyState }, renderEmpty ? renderEmpty() : (React.createElement(React.Fragment, null,
|
|
5226
|
+
showEmpty && (React.createElement("div", { className: classNames.emptyState, style: defaultStyles$1.emptyState }, renderEmpty ? renderEmpty() : (React.createElement(React.Fragment, null,
|
|
5205
5227
|
React.createElement(SearchIcon$9, { style: { width: 24, height: 24, marginBottom: 8, opacity: 0.5 } }),
|
|
5206
5228
|
React.createElement("span", null,
|
|
5207
5229
|
"No suggestions found for \"",
|
|
5208
5230
|
query,
|
|
5209
5231
|
"\""))))),
|
|
5210
|
-
footer || (keyboardNav.enabled && allItems.length > 0) ? (React.createElement("div", { className: classNames.footer, style: defaultStyles.footer }, footer || (React.createElement("div", { style: defaultStyles.keyboardHint },
|
|
5211
|
-
React.createElement("span", { style: defaultStyles.keyboardKey }, "\u2191"),
|
|
5212
|
-
React.createElement("span", { style: defaultStyles.keyboardKey }, "\u2193"),
|
|
5232
|
+
footer || (keyboardNav.enabled && allItems.length > 0) ? (React.createElement("div", { className: classNames.footer, style: defaultStyles$1.footer }, footer || (React.createElement("div", { style: defaultStyles$1.keyboardHint },
|
|
5233
|
+
React.createElement("span", { style: defaultStyles$1.keyboardKey }, "\u2191"),
|
|
5234
|
+
React.createElement("span", { style: defaultStyles$1.keyboardKey }, "\u2193"),
|
|
5213
5235
|
React.createElement("span", null, "to navigate"),
|
|
5214
|
-
React.createElement("span", { style: defaultStyles.keyboardKey }, "\u21B5"),
|
|
5236
|
+
React.createElement("span", { style: defaultStyles$1.keyboardKey }, "\u21B5"),
|
|
5215
5237
|
React.createElement("span", null, "to select"),
|
|
5216
|
-
React.createElement("span", { style: defaultStyles.keyboardKey }, "esc"),
|
|
5238
|
+
React.createElement("span", { style: defaultStyles$1.keyboardKey }, "esc"),
|
|
5217
5239
|
React.createElement("span", null, "to close"))))) : null,
|
|
5218
5240
|
React.createElement("style", null, `
|
|
5219
5241
|
@keyframes spin {
|
|
@@ -6214,12 +6236,12 @@ const FederatedDropdown = React.forwardRef(function FederatedDropdown(props, ref
|
|
|
6214
6236
|
/**
|
|
6215
6237
|
* Suggestions Analytics Hook
|
|
6216
6238
|
*
|
|
6217
|
-
* Provides analytics tracking for query suggestions components
|
|
6218
|
-
*
|
|
6219
|
-
* -
|
|
6220
|
-
*
|
|
6221
|
-
*
|
|
6222
|
-
*
|
|
6239
|
+
* Provides analytics tracking for query suggestions components using
|
|
6240
|
+
* Analytics V3 event structure and schema/fields (event_name, event_ts,
|
|
6241
|
+
* correlation_id/search_id via context, and V3 top-level fields such as
|
|
6242
|
+
* original_query, position, section, clicked_item_id, click_type, tab_id,
|
|
6243
|
+
* suggestions_count, etc.). All events are sent via client.trackEvent(payload, context)
|
|
6244
|
+
* so the SDK can attach correlation_id, search_id, event_ts, orgcode, xstoreid.
|
|
6223
6245
|
*/
|
|
6224
6246
|
// ============================================================================
|
|
6225
6247
|
// Event Names
|
|
@@ -6518,13 +6540,13 @@ const styles = {
|
|
|
6518
6540
|
// ============================================================================
|
|
6519
6541
|
const SearchIcon$6 = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", style: styles.searchIcon },
|
|
6520
6542
|
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" })));
|
|
6521
|
-
const ClearIcon = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", style: { width: 16, height: 16 } },
|
|
6543
|
+
const ClearIcon$1 = () => (React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", style: { width: 16, height: 16 } },
|
|
6522
6544
|
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" })));
|
|
6523
6545
|
// ============================================================================
|
|
6524
6546
|
// Component
|
|
6525
6547
|
// ============================================================================
|
|
6526
6548
|
const SearchBarWithSuggestions = React.forwardRef(function SearchBarWithSuggestions(props, ref) {
|
|
6527
|
-
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, includeFacets, includeCategories, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
|
|
6549
|
+
const { variant = 'classic', placeholder = 'Search...', initialQuery = '', value, onQueryChange, onSearch, onSuggestionSelect, onProductClick, showSearchButton = false, searchButtonText = 'Search', showClearButton = true, autoFocus = false, minQueryLength = 1, maxSuggestions = 8, maxProducts = 8, debounceMs = 200, showRecentSearches = true, showTrendingOnEmpty = true, includeDropdownRecommendations = false, filteredTabs, enableAnalytics = true, analyticsTags, includeFacets, includeCategories, dropdownWidth, dropdownMaxHeight, classNames = {}, style, inputStyle, ariaLabel = 'Search', } = props;
|
|
6528
6550
|
const { client } = useSearchContext();
|
|
6529
6551
|
const inputRef = React.useRef(null);
|
|
6530
6552
|
const dropdownRef = React.useRef(null);
|
|
@@ -6683,7 +6705,7 @@ const SearchBarWithSuggestions = React.forwardRef(function SearchBarWithSuggesti
|
|
|
6683
6705
|
case 'rich':
|
|
6684
6706
|
return (React.createElement(RichQuerySuggestions, { ref: dropdownRef, ...commonProps, includeDropdownRecommendations: includeDropdownRecommendations, includeCategories: true, width: dropdownWidth || '100%', maxHeight: dropdownMaxHeight || '480px' }));
|
|
6685
6707
|
case 'federated':
|
|
6686
|
-
return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px', includeFacets: includeFacets, includeCategories: includeCategories, includeDropdownRecommendations: includeDropdownRecommendations }));
|
|
6708
|
+
return (React.createElement(FederatedDropdown, { ref: dropdownRef, ...commonProps, maxProducts: maxProducts, filteredTabs: filteredTabs, showProducts: true, showBrands: true, showFilteredTabs: !!filteredTabs, onProductClick: handleProductClick, width: dropdownWidth || '800px', maxHeight: dropdownMaxHeight || '600px', includeFacets: includeFacets, includeCategories: includeCategories, includeDropdownRecommendations: includeDropdownRecommendations }));
|
|
6687
6709
|
case 'compact':
|
|
6688
6710
|
return (React.createElement(QuerySuggestionsDropdown, { ref: dropdownRef, ...commonProps, maxSuggestions: 5, showCounts: false, width: dropdownWidth || '100%' }));
|
|
6689
6711
|
case 'classic':
|
|
@@ -6700,11 +6722,1056 @@ const SearchBarWithSuggestions = React.forwardRef(function SearchBarWithSuggesti
|
|
|
6700
6722
|
React.createElement(SearchIcon$6, null),
|
|
6701
6723
|
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 } }),
|
|
6702
6724
|
showClearButton && query && (React.createElement("button", { type: "button", onClick: handleClear, className: classNames.clearButton, style: styles.clearButton, "aria-label": "Clear search" },
|
|
6703
|
-
React.createElement(ClearIcon, null))),
|
|
6725
|
+
React.createElement(ClearIcon$1, null))),
|
|
6704
6726
|
showSearchButton && (React.createElement("button", { type: "submit", className: classNames.button, style: styles.searchButton }, searchButtonText)))),
|
|
6705
6727
|
React.createElement("div", { className: classNames.dropdown }, renderDropdown())));
|
|
6706
6728
|
});
|
|
6707
6729
|
|
|
6730
|
+
/**
|
|
6731
|
+
* Suggestions Context
|
|
6732
|
+
*
|
|
6733
|
+
* Type definitions and React context for composable suggestions primitives.
|
|
6734
|
+
* Consuming components use useSuggestionsContext() for state and actions.
|
|
6735
|
+
*/
|
|
6736
|
+
const SuggestionsContext = React.createContext(null);
|
|
6737
|
+
function useSuggestionsContext() {
|
|
6738
|
+
const context = React.useContext(SuggestionsContext);
|
|
6739
|
+
if (!context) {
|
|
6740
|
+
const error = new Error('useSuggestionsContext must be used within a SuggestionsProvider');
|
|
6741
|
+
log.error('SuggestionsContext: not available', { error: error.message });
|
|
6742
|
+
throw error;
|
|
6743
|
+
}
|
|
6744
|
+
return context;
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6747
|
+
/**
|
|
6748
|
+
* SuggestionsProvider
|
|
6749
|
+
*
|
|
6750
|
+
* Provides suggestions state (query, suggestions, products, recent, trending, tabs),
|
|
6751
|
+
* actions (selectSuggestion, selectProduct, submitSearch, etc.), and keyboard navigation.
|
|
6752
|
+
* Uses useQuerySuggestionsEnhanced and useSuggestionsAnalytics; must be inside SearchProvider.
|
|
6753
|
+
*/
|
|
6754
|
+
function SuggestionsProvider({ children, minQueryLength = 1, debounceMs = 200, maxSuggestions = 10, includeDropdownRecommendations = true, filteredTabs, enableAnalytics = true, analyticsTags, onSearch, onSuggestionSelect, onProductClick, }) {
|
|
6755
|
+
const { client } = useSearchContext();
|
|
6756
|
+
const [query, setQueryState] = React.useState('');
|
|
6757
|
+
const [isOpen, setIsOpenState] = React.useState(false);
|
|
6758
|
+
const [activeIndex, setActiveIndex] = React.useState(-1);
|
|
6759
|
+
const [activeTabId, setActiveTabId] = React.useState('');
|
|
6760
|
+
const suggestionsData = useQuerySuggestionsEnhanced({
|
|
6761
|
+
client,
|
|
6762
|
+
query,
|
|
6763
|
+
enabled: isOpen,
|
|
6764
|
+
minQueryLength,
|
|
6765
|
+
debounceMs,
|
|
6766
|
+
maxSuggestions,
|
|
6767
|
+
includeDropdownRecommendations,
|
|
6768
|
+
filteredTabs,
|
|
6769
|
+
includeCategories: true,
|
|
6770
|
+
enableRecentSearches: true,
|
|
6771
|
+
maxRecentSearches: 8,
|
|
6772
|
+
analyticsTags,
|
|
6773
|
+
});
|
|
6774
|
+
const analytics = useSuggestionsAnalytics({
|
|
6775
|
+
client,
|
|
6776
|
+
enabled: enableAnalytics,
|
|
6777
|
+
analyticsTags,
|
|
6778
|
+
trackClicks: true,
|
|
6779
|
+
trackImpressions: true,
|
|
6780
|
+
});
|
|
6781
|
+
const { suggestions, recentSearches, trendingSearches, trendingProducts, filteredTabs: filteredTabsData, loading, error, hasContent, getAllNavigableItems, addRecentSearch, } = suggestionsData;
|
|
6782
|
+
const onSearchRef = React.useRef(onSearch);
|
|
6783
|
+
const onSuggestionSelectRef = React.useRef(onSuggestionSelect);
|
|
6784
|
+
const onProductClickRef = React.useRef(onProductClick);
|
|
6785
|
+
onSearchRef.current = onSearch;
|
|
6786
|
+
onSuggestionSelectRef.current = onSuggestionSelect;
|
|
6787
|
+
onProductClickRef.current = onProductClick;
|
|
6788
|
+
const setQuery = React.useCallback((q) => {
|
|
6789
|
+
setQueryState(q);
|
|
6790
|
+
setActiveIndex(-1);
|
|
6791
|
+
}, []);
|
|
6792
|
+
const close = React.useCallback(() => {
|
|
6793
|
+
analytics.trackDropdownClose(query);
|
|
6794
|
+
setIsOpenState(false);
|
|
6795
|
+
setActiveIndex(-1);
|
|
6796
|
+
}, [analytics, query]);
|
|
6797
|
+
const setIsOpen = React.useCallback((open) => {
|
|
6798
|
+
if (open) {
|
|
6799
|
+
setIsOpenState(true);
|
|
6800
|
+
analytics.trackDropdownOpen(query);
|
|
6801
|
+
}
|
|
6802
|
+
else {
|
|
6803
|
+
close();
|
|
6804
|
+
}
|
|
6805
|
+
}, [analytics, query, close]);
|
|
6806
|
+
const selectSuggestion = React.useCallback((suggestion, position) => {
|
|
6807
|
+
analytics.trackSuggestionClick({
|
|
6808
|
+
suggestion,
|
|
6809
|
+
position,
|
|
6810
|
+
query,
|
|
6811
|
+
totalSuggestions: suggestions.length,
|
|
6812
|
+
});
|
|
6813
|
+
setQuery(suggestion.query);
|
|
6814
|
+
onSuggestionSelectRef.current?.(suggestion);
|
|
6815
|
+
analytics.trackSearchSubmit(suggestion.query, true, suggestion);
|
|
6816
|
+
addRecentSearch(suggestion.query);
|
|
6817
|
+
close();
|
|
6818
|
+
onSearchRef.current?.(suggestion.query);
|
|
6819
|
+
}, [analytics, query, suggestions.length, addRecentSearch, close]);
|
|
6820
|
+
const selectProduct = React.useCallback((product, position, section, tabId) => {
|
|
6821
|
+
analytics.trackProductClick({
|
|
6822
|
+
product,
|
|
6823
|
+
position,
|
|
6824
|
+
section: section ?? 'products',
|
|
6825
|
+
tabId,
|
|
6826
|
+
query,
|
|
6827
|
+
});
|
|
6828
|
+
close();
|
|
6829
|
+
onProductClickRef.current?.(product);
|
|
6830
|
+
}, [analytics, query, close]);
|
|
6831
|
+
const selectRecentSearch = React.useCallback((search) => {
|
|
6832
|
+
analytics.trackRecentSearchClick(search);
|
|
6833
|
+
setQuery(search.query);
|
|
6834
|
+
close();
|
|
6835
|
+
onSearchRef.current?.(search.query);
|
|
6836
|
+
analytics.trackSearchSubmit(search.query, false);
|
|
6837
|
+
}, [analytics, close]);
|
|
6838
|
+
const selectTrendingSearch = React.useCallback((trending, position) => {
|
|
6839
|
+
analytics.trackTrendingClick(trending, position);
|
|
6840
|
+
setQuery(trending.query);
|
|
6841
|
+
close();
|
|
6842
|
+
onSearchRef.current?.(trending.query);
|
|
6843
|
+
analytics.trackSearchSubmit(trending.query, false);
|
|
6844
|
+
}, [analytics, close]);
|
|
6845
|
+
const setActiveTab = React.useCallback((tab) => {
|
|
6846
|
+
setActiveTabId(tab.id);
|
|
6847
|
+
analytics.trackTabSelect(tab, query);
|
|
6848
|
+
}, [analytics, query]);
|
|
6849
|
+
const submitSearch = React.useCallback((q, fromSuggestion, suggestion) => {
|
|
6850
|
+
const trimmed = q.trim();
|
|
6851
|
+
if (!trimmed)
|
|
6852
|
+
return;
|
|
6853
|
+
analytics.trackSearchSubmit(trimmed, !!fromSuggestion, suggestion);
|
|
6854
|
+
addRecentSearch(trimmed);
|
|
6855
|
+
close();
|
|
6856
|
+
onSearchRef.current?.(trimmed);
|
|
6857
|
+
}, [analytics, addRecentSearch, close]);
|
|
6858
|
+
const navigateNext = React.useCallback(() => {
|
|
6859
|
+
const items = getAllNavigableItems();
|
|
6860
|
+
if (items.length === 0)
|
|
6861
|
+
return;
|
|
6862
|
+
setActiveIndex((i) => (i < items.length - 1 ? i + 1 : 0));
|
|
6863
|
+
}, [getAllNavigableItems]);
|
|
6864
|
+
const navigatePrev = React.useCallback(() => {
|
|
6865
|
+
const items = getAllNavigableItems();
|
|
6866
|
+
if (items.length === 0)
|
|
6867
|
+
return;
|
|
6868
|
+
setActiveIndex((i) => (i <= 0 ? items.length - 1 : i - 1));
|
|
6869
|
+
}, [getAllNavigableItems]);
|
|
6870
|
+
const selectActive = React.useCallback(() => {
|
|
6871
|
+
const items = getAllNavigableItems();
|
|
6872
|
+
if (activeIndex < 0 || activeIndex >= items.length) {
|
|
6873
|
+
submitSearch(query);
|
|
6874
|
+
return;
|
|
6875
|
+
}
|
|
6876
|
+
const item = items[activeIndex];
|
|
6877
|
+
switch (item.type) {
|
|
6878
|
+
case 'suggestion':
|
|
6879
|
+
selectSuggestion(item.data, item.index);
|
|
6880
|
+
break;
|
|
6881
|
+
case 'product':
|
|
6882
|
+
selectProduct(item.data, item.index, 'products');
|
|
6883
|
+
break;
|
|
6884
|
+
case 'recent':
|
|
6885
|
+
selectRecentSearch(item.data);
|
|
6886
|
+
break;
|
|
6887
|
+
case 'trending':
|
|
6888
|
+
selectTrendingSearch(item.data, item.index);
|
|
6889
|
+
break;
|
|
6890
|
+
default:
|
|
6891
|
+
submitSearch(query);
|
|
6892
|
+
}
|
|
6893
|
+
}, [activeIndex, getAllNavigableItems, query, submitSearch, selectSuggestion, selectProduct, selectRecentSearch, selectTrendingSearch]);
|
|
6894
|
+
// Impression when open and content changes
|
|
6895
|
+
React.useEffect(() => {
|
|
6896
|
+
if (!isOpen || !hasContent || !query)
|
|
6897
|
+
return;
|
|
6898
|
+
analytics.trackImpression({
|
|
6899
|
+
suggestions,
|
|
6900
|
+
products: trendingProducts,
|
|
6901
|
+
query,
|
|
6902
|
+
timestamp: Date.now(),
|
|
6903
|
+
});
|
|
6904
|
+
}, [isOpen, hasContent, query, suggestions, trendingProducts, analytics]);
|
|
6905
|
+
const value = React.useMemo(() => ({
|
|
6906
|
+
query,
|
|
6907
|
+
setQuery,
|
|
6908
|
+
isOpen,
|
|
6909
|
+
setIsOpen,
|
|
6910
|
+
activeIndex,
|
|
6911
|
+
setActiveIndex,
|
|
6912
|
+
suggestions,
|
|
6913
|
+
recentSearches,
|
|
6914
|
+
trendingSearches,
|
|
6915
|
+
trendingProducts,
|
|
6916
|
+
filteredTabs: filteredTabsData,
|
|
6917
|
+
activeTabId,
|
|
6918
|
+
setActiveTabId,
|
|
6919
|
+
loading,
|
|
6920
|
+
error,
|
|
6921
|
+
hasContent,
|
|
6922
|
+
getAllNavigableItems,
|
|
6923
|
+
selectSuggestion,
|
|
6924
|
+
selectProduct,
|
|
6925
|
+
selectRecentSearch,
|
|
6926
|
+
selectTrendingSearch,
|
|
6927
|
+
setActiveTab,
|
|
6928
|
+
submitSearch,
|
|
6929
|
+
close,
|
|
6930
|
+
navigateNext,
|
|
6931
|
+
navigatePrev,
|
|
6932
|
+
selectActive,
|
|
6933
|
+
}), [
|
|
6934
|
+
query,
|
|
6935
|
+
setQuery,
|
|
6936
|
+
isOpen,
|
|
6937
|
+
setIsOpen,
|
|
6938
|
+
activeIndex,
|
|
6939
|
+
setActiveIndex,
|
|
6940
|
+
suggestions,
|
|
6941
|
+
recentSearches,
|
|
6942
|
+
trendingSearches,
|
|
6943
|
+
trendingProducts,
|
|
6944
|
+
filteredTabsData,
|
|
6945
|
+
activeTabId,
|
|
6946
|
+
setActiveTabId,
|
|
6947
|
+
loading,
|
|
6948
|
+
error,
|
|
6949
|
+
hasContent,
|
|
6950
|
+
getAllNavigableItems,
|
|
6951
|
+
selectSuggestion,
|
|
6952
|
+
selectProduct,
|
|
6953
|
+
selectRecentSearch,
|
|
6954
|
+
selectTrendingSearch,
|
|
6955
|
+
setActiveTab,
|
|
6956
|
+
submitSearch,
|
|
6957
|
+
close,
|
|
6958
|
+
navigateNext,
|
|
6959
|
+
navigatePrev,
|
|
6960
|
+
selectActive,
|
|
6961
|
+
]);
|
|
6962
|
+
return (React.createElement(SuggestionsContext.Provider, { value: value }, children));
|
|
6963
|
+
}
|
|
6964
|
+
|
|
6965
|
+
/**
|
|
6966
|
+
* SearchInput – suggestion-scoped search input (primitive)
|
|
6967
|
+
*
|
|
6968
|
+
* Single input bound to suggestions context: query, setQuery, focus opens dropdown,
|
|
6969
|
+
* keydown ArrowUp/Down/Enter/Escape delegates to context. Overridable via className/style.
|
|
6970
|
+
*/
|
|
6971
|
+
const defaultStyles = {
|
|
6972
|
+
position: 'relative',
|
|
6973
|
+
width: '100%',
|
|
6974
|
+
};
|
|
6975
|
+
const inputWrapperStyles = {
|
|
6976
|
+
display: 'flex',
|
|
6977
|
+
alignItems: 'center',
|
|
6978
|
+
gap: '8px',
|
|
6979
|
+
padding: '8px 12px',
|
|
6980
|
+
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
6981
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
6982
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
6983
|
+
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
|
6984
|
+
};
|
|
6985
|
+
const inputStyles = {
|
|
6986
|
+
flex: 1,
|
|
6987
|
+
minWidth: 0,
|
|
6988
|
+
padding: '8px 0',
|
|
6989
|
+
border: 'none',
|
|
6990
|
+
outline: 'none',
|
|
6991
|
+
backgroundColor: 'transparent',
|
|
6992
|
+
fontSize: 'inherit',
|
|
6993
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
6994
|
+
fontFamily: 'inherit',
|
|
6995
|
+
};
|
|
6996
|
+
function SearchInput({ placeholder = 'Search...', autoFocus = false, showClearButton = true, closeOnBlur = true, leftIcon, className, style, inputClassName, inputStyle, ariaLabel = 'Search', }) {
|
|
6997
|
+
const { query, setQuery, isOpen, setIsOpen, navigateNext, navigatePrev, selectActive, close, } = useSuggestionsContext();
|
|
6998
|
+
const inputRef = React.useRef(null);
|
|
6999
|
+
const handleFocus = React.useCallback(() => {
|
|
7000
|
+
setIsOpen(true);
|
|
7001
|
+
}, [setIsOpen]);
|
|
7002
|
+
const handleBlur = React.useCallback(() => {
|
|
7003
|
+
if (!closeOnBlur)
|
|
7004
|
+
return;
|
|
7005
|
+
setTimeout(() => close(), 200);
|
|
7006
|
+
}, [close, closeOnBlur]);
|
|
7007
|
+
const handleChange = React.useCallback((e) => {
|
|
7008
|
+
setQuery(e.target.value);
|
|
7009
|
+
}, [setQuery]);
|
|
7010
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
7011
|
+
if (e.key === 'ArrowDown') {
|
|
7012
|
+
e.preventDefault();
|
|
7013
|
+
navigateNext();
|
|
7014
|
+
return;
|
|
7015
|
+
}
|
|
7016
|
+
if (e.key === 'ArrowUp') {
|
|
7017
|
+
e.preventDefault();
|
|
7018
|
+
navigatePrev();
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
if (e.key === 'Enter') {
|
|
7022
|
+
e.preventDefault();
|
|
7023
|
+
selectActive();
|
|
7024
|
+
return;
|
|
7025
|
+
}
|
|
7026
|
+
if (e.key === 'Escape') {
|
|
7027
|
+
e.preventDefault();
|
|
7028
|
+
close();
|
|
7029
|
+
inputRef.current?.blur();
|
|
7030
|
+
return;
|
|
7031
|
+
}
|
|
7032
|
+
}, [navigateNext, navigatePrev, selectActive, close]);
|
|
7033
|
+
const handleClear = React.useCallback(() => {
|
|
7034
|
+
setQuery('');
|
|
7035
|
+
inputRef.current?.focus();
|
|
7036
|
+
}, [setQuery]);
|
|
7037
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-search-input-wrapper', className), style: { ...defaultStyles, ...style } },
|
|
7038
|
+
React.createElement("div", { className: "seekora-suggestions-input-wrapper", style: inputWrapperStyles },
|
|
7039
|
+
leftIcon ? (React.createElement("span", { className: "seekora-suggestions-input-left-icon", style: { display: 'flex', flexShrink: 0, color: 'var(--seekora-text-secondary, #6b7280)' } }, leftIcon)) : null,
|
|
7040
|
+
React.createElement("input", { ref: inputRef, type: "text", value: query, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder, autoFocus: autoFocus, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-label": ariaLabel, "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-autocomplete": "list", role: "combobox", className: clsx('seekora-suggestions-input', inputClassName), style: { ...inputStyles, ...inputStyle } }),
|
|
7041
|
+
showClearButton && query ? (React.createElement("button", { type: "button", onClick: handleClear, className: "seekora-suggestions-input-clear", "aria-label": "Clear search", style: {
|
|
7042
|
+
padding: 4,
|
|
7043
|
+
border: 'none',
|
|
7044
|
+
background: 'transparent',
|
|
7045
|
+
cursor: 'pointer',
|
|
7046
|
+
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
7047
|
+
display: 'flex',
|
|
7048
|
+
alignItems: 'center',
|
|
7049
|
+
justifyContent: 'center',
|
|
7050
|
+
} },
|
|
7051
|
+
React.createElement(ClearIcon, null))) : null)));
|
|
7052
|
+
}
|
|
7053
|
+
function ClearIcon() {
|
|
7054
|
+
return (React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
|
|
7055
|
+
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" })));
|
|
7056
|
+
}
|
|
7057
|
+
|
|
7058
|
+
/**
|
|
7059
|
+
* DropdownPanel – floating panel for suggestions (primitive)
|
|
7060
|
+
*
|
|
7061
|
+
* Shows when isOpen; handles click-outside and Escape to close. Children = any composition
|
|
7062
|
+
* of SuggestionList, ProductGrid, etc. Position and z-index configurable.
|
|
7063
|
+
*/
|
|
7064
|
+
function DropdownPanel({ children, position = 'absolute', top = '100%', left = 0, right, width = '100%', maxHeight = '80vh', zIndex = 1000, className, style, closeOnClickOutside = true, closeOnEscape = true, }) {
|
|
7065
|
+
const { isOpen, close } = useSuggestionsContext();
|
|
7066
|
+
const panelRef = React.useRef(null);
|
|
7067
|
+
React.useEffect(() => {
|
|
7068
|
+
if (!closeOnEscape || !isOpen)
|
|
7069
|
+
return;
|
|
7070
|
+
const handleKeyDown = (e) => {
|
|
7071
|
+
if (e.key === 'Escape')
|
|
7072
|
+
close();
|
|
7073
|
+
};
|
|
7074
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
7075
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
7076
|
+
}, [closeOnEscape, isOpen, close]);
|
|
7077
|
+
const handleClickOutside = React.useCallback((e) => {
|
|
7078
|
+
if (!closeOnClickOutside || !panelRef.current)
|
|
7079
|
+
return;
|
|
7080
|
+
const target = e.target;
|
|
7081
|
+
if (!panelRef.current.contains(target))
|
|
7082
|
+
close();
|
|
7083
|
+
}, [closeOnClickOutside, close]);
|
|
7084
|
+
React.useEffect(() => {
|
|
7085
|
+
if (!isOpen)
|
|
7086
|
+
return;
|
|
7087
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
7088
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
7089
|
+
}, [isOpen, handleClickOutside]);
|
|
7090
|
+
if (!isOpen)
|
|
7091
|
+
return null;
|
|
7092
|
+
const panelStyle = {
|
|
7093
|
+
position,
|
|
7094
|
+
top,
|
|
7095
|
+
left,
|
|
7096
|
+
right,
|
|
7097
|
+
width,
|
|
7098
|
+
maxHeight,
|
|
7099
|
+
zIndex,
|
|
7100
|
+
overflow: 'auto',
|
|
7101
|
+
backgroundColor: 'var(--seekora-bg-surface, #fff)',
|
|
7102
|
+
border: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
7103
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7104
|
+
boxShadow: 'var(--seekora-shadow-lg, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05))',
|
|
7105
|
+
marginTop: 4,
|
|
7106
|
+
};
|
|
7107
|
+
return (React.createElement("div", { ref: panelRef, role: "listbox", className: clsx('seekora-suggestions-dropdown-panel', className), style: { ...panelStyle, ...style } }, children));
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7110
|
+
/**
|
|
7111
|
+
* SuggestionItem – single text suggestion row (primitive)
|
|
7112
|
+
*
|
|
7113
|
+
* Highlights when isActive; onClick calls context selectSuggestion. Overridable via className/style.
|
|
7114
|
+
*/
|
|
7115
|
+
const defaultItemStyle = {
|
|
7116
|
+
padding: '10px 12px',
|
|
7117
|
+
cursor: 'pointer',
|
|
7118
|
+
listStyle: 'none',
|
|
7119
|
+
border: 'none',
|
|
7120
|
+
width: '100%',
|
|
7121
|
+
textAlign: 'left',
|
|
7122
|
+
fontSize: 'inherit',
|
|
7123
|
+
fontFamily: 'inherit',
|
|
7124
|
+
backgroundColor: 'transparent',
|
|
7125
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
7126
|
+
transition: 'background-color 120ms ease',
|
|
7127
|
+
};
|
|
7128
|
+
function SuggestionItem({ suggestion, index, isActive, onSelect, className, style, renderHighlight, }) {
|
|
7129
|
+
const displayText = suggestion.highlightedQuery ?? suggestion.query;
|
|
7130
|
+
const content = renderHighlight ? renderHighlight(displayText) : displayText;
|
|
7131
|
+
return (React.createElement("li", { role: "option", "aria-selected": isActive, id: `seekora-suggestion-${index}`, className: clsx('seekora-suggestions-item', isActive && 'seekora-suggestions-item--active', className), style: {
|
|
7132
|
+
...defaultItemStyle,
|
|
7133
|
+
...(isActive ? { backgroundColor: 'var(--seekora-bg-hover, #f3f4f6)' } : {}),
|
|
7134
|
+
...style,
|
|
7135
|
+
}, onMouseDown: (e) => {
|
|
7136
|
+
e.preventDefault();
|
|
7137
|
+
onSelect();
|
|
7138
|
+
} },
|
|
7139
|
+
content,
|
|
7140
|
+
suggestion.count != null ? (React.createElement("span", { className: "seekora-suggestions-item-count", style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, suggestion.count)) : null));
|
|
7141
|
+
}
|
|
7142
|
+
|
|
7143
|
+
/**
|
|
7144
|
+
* SuggestionsLoading – loading indicator (primitive)
|
|
7145
|
+
*/
|
|
7146
|
+
function SuggestionsLoading({ className, style, text = 'Loading...' }) {
|
|
7147
|
+
const { loading } = useSuggestionsContext();
|
|
7148
|
+
if (!loading)
|
|
7149
|
+
return null;
|
|
7150
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: {
|
|
7151
|
+
padding: 16,
|
|
7152
|
+
color: 'var(--seekora-text-secondary, #6b7280)',
|
|
7153
|
+
fontSize: '0.875rem',
|
|
7154
|
+
...style,
|
|
7155
|
+
} }, text));
|
|
7156
|
+
}
|
|
7157
|
+
|
|
7158
|
+
/**
|
|
7159
|
+
* SuggestionList – container for text suggestions (primitive)
|
|
7160
|
+
*
|
|
7161
|
+
* Renders list of SuggestionItem from context; uses activeIndex for highlight. Optional renderItem.
|
|
7162
|
+
*/
|
|
7163
|
+
const listStyle = {
|
|
7164
|
+
margin: 0,
|
|
7165
|
+
padding: '4px 0',
|
|
7166
|
+
};
|
|
7167
|
+
function SuggestionList({ maxItems = 10, className, style, listClassName, renderItem, }) {
|
|
7168
|
+
const { suggestions, activeIndex, loading, selectSuggestion, getAllNavigableItems, } = useSuggestionsContext();
|
|
7169
|
+
const items = suggestions.slice(0, maxItems);
|
|
7170
|
+
const navigableItems = getAllNavigableItems();
|
|
7171
|
+
const suggestionStartIndex = navigableItems.findIndex((n) => n.type === 'suggestion');
|
|
7172
|
+
const activeIsInSuggestions = suggestionStartIndex >= 0 && activeIndex >= suggestionStartIndex && activeIndex < suggestionStartIndex + items.length;
|
|
7173
|
+
if (loading) {
|
|
7174
|
+
return React.createElement(SuggestionsLoading, { className: className, style: style });
|
|
7175
|
+
}
|
|
7176
|
+
if (items.length === 0)
|
|
7177
|
+
return null;
|
|
7178
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-list', className), style: style },
|
|
7179
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-list-ul', listClassName), style: listStyle, role: "listbox" }, items.map((suggestion, i) => {
|
|
7180
|
+
const globalIndex = suggestionStartIndex >= 0 ? suggestionStartIndex + i : i;
|
|
7181
|
+
const isActive = activeIsInSuggestions && activeIndex === globalIndex;
|
|
7182
|
+
const onSelect = () => selectSuggestion(suggestion, globalIndex);
|
|
7183
|
+
if (renderItem) {
|
|
7184
|
+
return (React.createElement("li", { key: suggestion.objectID ?? suggestion.query ?? i, role: "option" }, renderItem(suggestion, globalIndex, isActive, onSelect)));
|
|
7185
|
+
}
|
|
7186
|
+
return (React.createElement(SuggestionItem, { key: suggestion.objectID ?? suggestion.query ?? i, suggestion: suggestion, index: globalIndex, isActive: isActive, onSelect: onSelect }));
|
|
7187
|
+
}))));
|
|
7188
|
+
}
|
|
7189
|
+
|
|
7190
|
+
/**
|
|
7191
|
+
* ImageDisplay – configurable multi-image display (primitive)
|
|
7192
|
+
*
|
|
7193
|
+
* Variants: single, carousel, hover, thumbStrip, thumbList. Use in ItemCard,
|
|
7194
|
+
* ProductCard, ProductGallery. Overridable via className/style.
|
|
7195
|
+
*/
|
|
7196
|
+
const imgBaseStyle = {
|
|
7197
|
+
width: '100%',
|
|
7198
|
+
aspectRatio: '1',
|
|
7199
|
+
objectFit: 'cover',
|
|
7200
|
+
borderRadius: 4,
|
|
7201
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7202
|
+
};
|
|
7203
|
+
function ImageDisplay({ images, variant = 'single', alt = '', className, style, carouselAutoplay = false, carouselIntervalMs = 4000, }) {
|
|
7204
|
+
const [index, setIndex] = React.useState(0);
|
|
7205
|
+
const [hovering, setHovering] = React.useState(false);
|
|
7206
|
+
const safeImages = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
7207
|
+
const current = safeImages[index] ?? safeImages[0];
|
|
7208
|
+
if (safeImages.length === 0) {
|
|
7209
|
+
return React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-placeholder', className), style: { ...imgBaseStyle, ...style }, "aria-hidden": true });
|
|
7210
|
+
}
|
|
7211
|
+
if (variant === 'single') {
|
|
7212
|
+
return (React.createElement("img", { src: safeImages[0], alt: alt, className: clsx('seekora-img-display', 'seekora-img-single', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" }));
|
|
7213
|
+
}
|
|
7214
|
+
if (variant === 'hover') {
|
|
7215
|
+
const showSecond = safeImages.length > 1 && hovering;
|
|
7216
|
+
const src = showSecond ? safeImages[1] : safeImages[0];
|
|
7217
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-hover', className), style: { position: 'relative', ...style }, onMouseEnter: () => setHovering(true), onMouseLeave: () => setHovering(false) },
|
|
7218
|
+
React.createElement("img", { src: src, alt: alt, className: "seekora-img-hover-img", style: imgBaseStyle, loading: "lazy" })));
|
|
7219
|
+
}
|
|
7220
|
+
if (variant === 'carousel') {
|
|
7221
|
+
const go = (delta) => {
|
|
7222
|
+
setIndex((i) => {
|
|
7223
|
+
const next = i + delta;
|
|
7224
|
+
if (next < 0)
|
|
7225
|
+
return safeImages.length - 1;
|
|
7226
|
+
if (next >= safeImages.length)
|
|
7227
|
+
return 0;
|
|
7228
|
+
return next;
|
|
7229
|
+
});
|
|
7230
|
+
};
|
|
7231
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-carousel', className), style: { position: 'relative', ...style } },
|
|
7232
|
+
React.createElement("img", { src: current, alt: alt, className: "seekora-img-carousel-main", style: imgBaseStyle, loading: "lazy" }),
|
|
7233
|
+
safeImages.length > 1 && (React.createElement(React.Fragment, null,
|
|
7234
|
+
React.createElement("button", { type: "button", "aria-label": "Previous", className: "seekora-img-carousel-prev", style: arrowStyle(true), onMouseDown: () => go(-1) }),
|
|
7235
|
+
React.createElement("button", { type: "button", "aria-label": "Next", className: "seekora-img-carousel-next", style: arrowStyle(false), onMouseDown: () => go(1) })))));
|
|
7236
|
+
}
|
|
7237
|
+
if (variant === 'thumbStrip' || variant === 'thumbList') {
|
|
7238
|
+
return (React.createElement("div", { className: clsx('seekora-img-display', 'seekora-img-thumbstrip', className), style: { display: 'flex', flexDirection: 'column', gap: 8, ...style } },
|
|
7239
|
+
React.createElement("img", { src: current, alt: alt, className: "seekora-img-thumb-main", style: imgBaseStyle, loading: "lazy" }),
|
|
7240
|
+
React.createElement("div", { className: "seekora-img-thumbs", style: { display: 'flex', gap: 4, overflowX: 'auto', paddingBottom: 4 } }, safeImages.map((src, i) => (React.createElement("button", { type: "button", key: i, className: clsx('seekora-img-thumb', i === index && 'seekora-img-thumb--active'), style: { flexShrink: 0, width: 48, height: 48, padding: 0, border: i === index ? '2px solid var(--seekora-primary)' : '1px solid transparent', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', background: 'none' }, onMouseDown: () => setIndex(i) },
|
|
7241
|
+
React.createElement("img", { src: src, alt: "", style: { width: '100%', height: '100%', objectFit: 'cover' } })))))));
|
|
7242
|
+
}
|
|
7243
|
+
return React.createElement("img", { src: current, alt: alt, className: clsx('seekora-img-display', className), style: { ...imgBaseStyle, ...style }, loading: "lazy" });
|
|
7244
|
+
}
|
|
7245
|
+
function arrowStyle(left) {
|
|
7246
|
+
return {
|
|
7247
|
+
position: 'absolute',
|
|
7248
|
+
top: '50%',
|
|
7249
|
+
[left ? 'left' : 'right']: 8,
|
|
7250
|
+
transform: 'translateY(-50%)',
|
|
7251
|
+
width: 32,
|
|
7252
|
+
height: 32,
|
|
7253
|
+
borderRadius: '50%',
|
|
7254
|
+
border: '1px solid var(--seekora-border-color)',
|
|
7255
|
+
backgroundColor: 'var(--seekora-bg-surface)',
|
|
7256
|
+
cursor: 'pointer',
|
|
7257
|
+
display: 'flex',
|
|
7258
|
+
alignItems: 'center',
|
|
7259
|
+
justifyContent: 'center',
|
|
7260
|
+
};
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7263
|
+
/**
|
|
7264
|
+
* ItemCard – generic item tile (primitive)
|
|
7265
|
+
*
|
|
7266
|
+
* Domain-agnostic: works for docs, articles, products, or any hit with id, title,
|
|
7267
|
+
* optional description, image, url. Overridable via className/style. Use for
|
|
7268
|
+
* search results, section blocks, or any list; for e-commerce products use ProductCard.
|
|
7269
|
+
*/
|
|
7270
|
+
const cardStyle$1 = {
|
|
7271
|
+
display: 'flex',
|
|
7272
|
+
flexDirection: 'column',
|
|
7273
|
+
gap: 8,
|
|
7274
|
+
padding: 8,
|
|
7275
|
+
cursor: 'pointer',
|
|
7276
|
+
border: 'none',
|
|
7277
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7278
|
+
backgroundColor: 'transparent',
|
|
7279
|
+
textAlign: 'left',
|
|
7280
|
+
fontSize: 'inherit',
|
|
7281
|
+
fontFamily: 'inherit',
|
|
7282
|
+
transition: 'background-color 120ms ease',
|
|
7283
|
+
};
|
|
7284
|
+
const imgStyle$1 = {
|
|
7285
|
+
width: '100%',
|
|
7286
|
+
aspectRatio: '1',
|
|
7287
|
+
objectFit: 'cover',
|
|
7288
|
+
borderRadius: 4,
|
|
7289
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7290
|
+
};
|
|
7291
|
+
function ItemCard({ item, position, onSelect, className, style, asLink = true, imageVariant = 'single', }) {
|
|
7292
|
+
const images = item.images?.length ? item.images : item.image ?? item.imageUrl ? [String(item.image ?? item.imageUrl)] : [];
|
|
7293
|
+
const title = item.title ?? item.primaryText ?? '';
|
|
7294
|
+
const description = item.description ?? item.secondaryText;
|
|
7295
|
+
const href = item.url;
|
|
7296
|
+
const content = (React.createElement(React.Fragment, null,
|
|
7297
|
+
images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: String(title), className: "seekora-item-card-image" })) : (React.createElement("div", { className: "seekora-item-card-placeholder", style: imgStyle$1, "aria-hidden": true })),
|
|
7298
|
+
React.createElement("span", { className: "seekora-item-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, String(title)),
|
|
7299
|
+
description ? (React.createElement("span", { className: "seekora-item-card-description", style: { fontSize: '0.8125rem', color: 'var(--seekora-text-secondary, #6b7280)', lineHeight: 1.3 } }, String(description))) : null));
|
|
7300
|
+
const commonProps = {
|
|
7301
|
+
className: clsx('seekora-item-card', className),
|
|
7302
|
+
style: { ...cardStyle$1, ...style },
|
|
7303
|
+
'data-position': position,
|
|
7304
|
+
onClick: onSelect,
|
|
7305
|
+
onMouseDown: onSelect ? (e) => { e.preventDefault(); onSelect(); } : undefined,
|
|
7306
|
+
};
|
|
7307
|
+
if (asLink && href) {
|
|
7308
|
+
return (React.createElement("a", { href: href, ...commonProps, "data-item-id": item.id }, content));
|
|
7309
|
+
}
|
|
7310
|
+
return (React.createElement("button", { type: "button", ...commonProps, "data-item-id": item.id }, content));
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
/**
|
|
7314
|
+
* ItemGrid – generic grid of items (primitive)
|
|
7315
|
+
*
|
|
7316
|
+
* Domain-agnostic: accepts any items[] and onItemClick. Renders ItemCard by default
|
|
7317
|
+
* or custom renderItem. Use for search hits, section blocks, docs, articles; for
|
|
7318
|
+
* product-only lists use ProductGrid.
|
|
7319
|
+
*/
|
|
7320
|
+
const defaultGridStyle = {
|
|
7321
|
+
display: 'grid',
|
|
7322
|
+
gridTemplateColumns: 'repeat(var(--seekora-grid-cols, 4), minmax(0, 1fr))',
|
|
7323
|
+
gap: 12,
|
|
7324
|
+
padding: 12,
|
|
7325
|
+
};
|
|
7326
|
+
function toGenericItem(item, getItemId, getItemTitle, getItemImage, getItemDescription, getItemUrl) {
|
|
7327
|
+
return {
|
|
7328
|
+
id: getItemId(item),
|
|
7329
|
+
title: getItemTitle(item),
|
|
7330
|
+
image: getItemImage?.(item),
|
|
7331
|
+
description: getItemDescription?.(item),
|
|
7332
|
+
url: getItemUrl?.(item),
|
|
7333
|
+
...(typeof item === 'object' && item !== null ? item : {}),
|
|
7334
|
+
};
|
|
7335
|
+
}
|
|
7336
|
+
function ItemGrid({ items, onItemClick, getItemId = (i) => i.id, getItemTitle = (i) => i.title, getItemImage = (i) => i.image, getItemDescription = (i) => i.description, getItemUrl = (i) => i.url, renderItem, maxItems = 24, columns = 4, className, style, gridClassName, }) {
|
|
7337
|
+
const slice = items.slice(0, maxItems);
|
|
7338
|
+
if (slice.length === 0)
|
|
7339
|
+
return null;
|
|
7340
|
+
const gridStyle = {
|
|
7341
|
+
...defaultGridStyle,
|
|
7342
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
7343
|
+
};
|
|
7344
|
+
return (React.createElement("div", { className: clsx('seekora-item-grid', className), style: style },
|
|
7345
|
+
React.createElement("div", { className: clsx('seekora-item-grid-inner', gridClassName), style: gridStyle }, slice.map((item, i) => {
|
|
7346
|
+
if (renderItem)
|
|
7347
|
+
return React.createElement(React.Fragment, { key: getItemId(item) }, renderItem(item, i));
|
|
7348
|
+
const generic = toGenericItem(item, getItemId, getItemTitle, getItemImage, getItemDescription, getItemUrl);
|
|
7349
|
+
return (React.createElement(ItemCard, { key: generic.id, item: generic, position: i, onSelect: onItemClick ? () => onItemClick(item, i) : undefined }));
|
|
7350
|
+
}))));
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
/**
|
|
7354
|
+
* ProductCard – one product tile (primitive)
|
|
7355
|
+
*
|
|
7356
|
+
* Minimal layout: image (via ImageDisplay when imageVariant set), title, price.
|
|
7357
|
+
* onClick calls context selectProduct. Overridable via className/style.
|
|
7358
|
+
*/
|
|
7359
|
+
const cardStyle = {
|
|
7360
|
+
display: 'flex',
|
|
7361
|
+
flexDirection: 'column',
|
|
7362
|
+
gap: 8,
|
|
7363
|
+
padding: 8,
|
|
7364
|
+
cursor: 'pointer',
|
|
7365
|
+
border: 'none',
|
|
7366
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7367
|
+
backgroundColor: 'transparent',
|
|
7368
|
+
textAlign: 'left',
|
|
7369
|
+
transition: 'background-color 120ms ease',
|
|
7370
|
+
};
|
|
7371
|
+
const imgStyle = {
|
|
7372
|
+
width: '100%',
|
|
7373
|
+
aspectRatio: '1',
|
|
7374
|
+
objectFit: 'cover',
|
|
7375
|
+
borderRadius: 4,
|
|
7376
|
+
backgroundColor: 'var(--seekora-bg-secondary, #f3f4f6)',
|
|
7377
|
+
};
|
|
7378
|
+
function ProductCard({ product, position, section, tabId, onSelect, className, style, imageVariant = 'single', }) {
|
|
7379
|
+
const images = product.images?.length
|
|
7380
|
+
? product.images
|
|
7381
|
+
: product.image ?? product.imageUrl
|
|
7382
|
+
? [String(product.image ?? product.imageUrl)]
|
|
7383
|
+
: [];
|
|
7384
|
+
const title = product.title ?? product.name ?? '';
|
|
7385
|
+
const price = product.price != null ? (typeof product.price === 'number' ? product.price : Number(product.price)) : null;
|
|
7386
|
+
return (React.createElement("button", { type: "button", className: clsx('seekora-suggestions-product-card', className), style: { ...cardStyle, ...style }, onMouseDown: (e) => {
|
|
7387
|
+
e.preventDefault();
|
|
7388
|
+
onSelect();
|
|
7389
|
+
}, "data-position": position, "data-section": section, "data-tab-id": tabId },
|
|
7390
|
+
images.length > 0 ? (React.createElement(ImageDisplay, { images: images, variant: imageVariant, alt: title, className: "seekora-suggestions-product-card-image" })) : (React.createElement("div", { className: "seekora-suggestions-product-card-placeholder", style: imgStyle, "aria-hidden": true })),
|
|
7391
|
+
React.createElement("span", { className: "seekora-suggestions-product-card-title", style: { fontSize: '0.875rem', fontWeight: 500 } }, title),
|
|
7392
|
+
price != null && !Number.isNaN(price) ? (React.createElement("span", { className: "seekora-suggestions-product-card-price", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary, #6b7280)' } },
|
|
7393
|
+
product.currency ?? '$',
|
|
7394
|
+
price.toFixed(2))) : null));
|
|
7395
|
+
}
|
|
7396
|
+
|
|
7397
|
+
/**
|
|
7398
|
+
* ProductGrid – grid of product cards from context (primitive)
|
|
7399
|
+
*
|
|
7400
|
+
* Uses trendingProducts or active tab products; each click calls context selectProduct.
|
|
7401
|
+
*/
|
|
7402
|
+
function ProductGrid({ maxItems = 8, source = 'trending', columns = 4, className, style, gridClassName, }) {
|
|
7403
|
+
const { trendingProducts, filteredTabs, activeTabId, selectProduct, getAllNavigableItems, } = useSuggestionsContext();
|
|
7404
|
+
const products = React.useMemo(() => {
|
|
7405
|
+
if (source === 'trending')
|
|
7406
|
+
return trendingProducts;
|
|
7407
|
+
const tab = filteredTabs.find((t) => t.id === (source === 'tab' ? activeTabId : source));
|
|
7408
|
+
return tab?.products ?? [];
|
|
7409
|
+
}, [source, activeTabId, trendingProducts, filteredTabs]);
|
|
7410
|
+
const items = products.slice(0, maxItems);
|
|
7411
|
+
const navigableItems = getAllNavigableItems();
|
|
7412
|
+
const productStartIndex = navigableItems.findIndex((n) => n.type === 'product');
|
|
7413
|
+
if (items.length === 0)
|
|
7414
|
+
return null;
|
|
7415
|
+
const gridStyle = {
|
|
7416
|
+
display: 'grid',
|
|
7417
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
7418
|
+
gap: 12,
|
|
7419
|
+
padding: 12,
|
|
7420
|
+
};
|
|
7421
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-product-grid', className), style: style },
|
|
7422
|
+
React.createElement("div", { className: clsx('seekora-suggestions-product-grid-inner', gridClassName), style: gridStyle }, items.map((product, i) => {
|
|
7423
|
+
const globalIndex = productStartIndex >= 0 ? productStartIndex + i : i;
|
|
7424
|
+
const section = source === 'trending' ? 'products' : 'filtered_tab';
|
|
7425
|
+
const tabId = source !== 'trending' ? (source === 'tab' ? activeTabId : source) : undefined;
|
|
7426
|
+
return (React.createElement(ProductCard, { key: product.id ?? product.objectID ?? i, product: product, position: globalIndex, section: section, tabId: tabId, onSelect: () => selectProduct(product, globalIndex, section, tabId) }));
|
|
7427
|
+
}))));
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
/**
|
|
7431
|
+
* CategoriesTabs – horizontal tabs (e.g. filtered tabs) (primitive)
|
|
7432
|
+
*
|
|
7433
|
+
* Active tab from context; on select updates context and tracks analytics.
|
|
7434
|
+
*/
|
|
7435
|
+
function CategoriesTabs({ className, style, tabClassName }) {
|
|
7436
|
+
const { filteredTabs, activeTabId, setActiveTab } = useSuggestionsContext();
|
|
7437
|
+
if (filteredTabs.length === 0)
|
|
7438
|
+
return null;
|
|
7439
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-categories-tabs', className), style: {
|
|
7440
|
+
display: 'flex',
|
|
7441
|
+
gap: 4,
|
|
7442
|
+
padding: '8px 12px',
|
|
7443
|
+
borderBottom: '1px solid var(--seekora-border-color, #e5e7eb)',
|
|
7444
|
+
overflowX: 'auto',
|
|
7445
|
+
...style,
|
|
7446
|
+
}, role: "tablist" }, filteredTabs.map((tab) => {
|
|
7447
|
+
const isActive = activeTabId === tab.id;
|
|
7448
|
+
return (React.createElement("button", { key: tab.id, type: "button", role: "tab", "aria-selected": isActive, className: clsx('seekora-suggestions-tab', isActive && 'seekora-suggestions-tab--active', tabClassName), style: {
|
|
7449
|
+
padding: '8px 12px',
|
|
7450
|
+
border: 'none',
|
|
7451
|
+
borderRadius: 'var(--seekora-border-radius, 6px)',
|
|
7452
|
+
backgroundColor: isActive ? 'var(--seekora-primary-light, rgba(59, 130, 246, 0.1))' : 'transparent',
|
|
7453
|
+
color: isActive ? 'var(--seekora-primary, #3b82f6)' : 'var(--seekora-text-primary, #111827)',
|
|
7454
|
+
cursor: 'pointer',
|
|
7455
|
+
fontSize: '0.875rem',
|
|
7456
|
+
fontWeight: isActive ? 600 : 400,
|
|
7457
|
+
whiteSpace: 'nowrap',
|
|
7458
|
+
transition: 'background-color 120ms ease',
|
|
7459
|
+
}, onClick: () => setActiveTab(tab) }, tab.label));
|
|
7460
|
+
})));
|
|
7461
|
+
}
|
|
7462
|
+
|
|
7463
|
+
/**
|
|
7464
|
+
* RecentSearchesList – list of recent queries (primitive)
|
|
7465
|
+
*
|
|
7466
|
+
* Reads recentSearches from context; each click calls selectRecentSearch. Optional title/render.
|
|
7467
|
+
*/
|
|
7468
|
+
const itemStyle$1 = {
|
|
7469
|
+
padding: '10px 12px',
|
|
7470
|
+
cursor: 'pointer',
|
|
7471
|
+
border: 'none',
|
|
7472
|
+
width: '100%',
|
|
7473
|
+
textAlign: 'left',
|
|
7474
|
+
fontSize: 'inherit',
|
|
7475
|
+
fontFamily: 'inherit',
|
|
7476
|
+
backgroundColor: 'transparent',
|
|
7477
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
7478
|
+
transition: 'background-color 120ms ease',
|
|
7479
|
+
};
|
|
7480
|
+
function RecentSearchesList({ title = 'Recent', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
7481
|
+
const { recentSearches, query, selectRecentSearch } = useSuggestionsContext();
|
|
7482
|
+
const items = recentSearches.slice(0, maxItems);
|
|
7483
|
+
if (items.length === 0)
|
|
7484
|
+
return null;
|
|
7485
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-recent-list', className), style: style },
|
|
7486
|
+
title ? (React.createElement("div", { className: "seekora-suggestions-recent-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
7487
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-recent-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((search, i) => {
|
|
7488
|
+
const onSelect = () => selectRecentSearch(search);
|
|
7489
|
+
if (renderItem) {
|
|
7490
|
+
return React.createElement("li", { key: `${search.query}-${search.timestamp}` }, renderItem(search, i, onSelect));
|
|
7491
|
+
}
|
|
7492
|
+
return (React.createElement("li", { key: `${search.query}-${search.timestamp}` },
|
|
7493
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-recent-item", style: itemStyle$1, onMouseDown: (e) => {
|
|
7494
|
+
e.preventDefault();
|
|
7495
|
+
onSelect();
|
|
7496
|
+
} }, search.query)));
|
|
7497
|
+
}))));
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
/**
|
|
7501
|
+
* TrendingList – list of trending searches (primitive)
|
|
7502
|
+
*
|
|
7503
|
+
* Reads trendingSearches from context; each click calls selectTrendingSearch. Optional title/render.
|
|
7504
|
+
*/
|
|
7505
|
+
const itemStyle = {
|
|
7506
|
+
padding: '10px 12px',
|
|
7507
|
+
cursor: 'pointer',
|
|
7508
|
+
border: 'none',
|
|
7509
|
+
width: '100%',
|
|
7510
|
+
textAlign: 'left',
|
|
7511
|
+
fontSize: 'inherit',
|
|
7512
|
+
fontFamily: 'inherit',
|
|
7513
|
+
backgroundColor: 'transparent',
|
|
7514
|
+
color: 'var(--seekora-text-primary, #111827)',
|
|
7515
|
+
transition: 'background-color 120ms ease',
|
|
7516
|
+
};
|
|
7517
|
+
function TrendingList({ title = 'Trending', maxItems = 8, className, style, listClassName, renderItem, }) {
|
|
7518
|
+
const { trendingSearches, selectTrendingSearch } = useSuggestionsContext();
|
|
7519
|
+
const items = trendingSearches.slice(0, maxItems);
|
|
7520
|
+
if (items.length === 0)
|
|
7521
|
+
return null;
|
|
7522
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-trending-list', className), style: style },
|
|
7523
|
+
title ? (React.createElement("div", { className: "seekora-suggestions-trending-title", style: { padding: '8px 12px', fontSize: '0.75rem', fontWeight: 600, color: 'var(--seekora-text-secondary, #6b7280)', textTransform: 'uppercase' } }, title)) : null,
|
|
7524
|
+
React.createElement("ul", { className: clsx('seekora-suggestions-trending-ul', listClassName), style: { margin: 0, padding: 0, listStyle: 'none' } }, items.map((trending, i) => {
|
|
7525
|
+
const onSelect = () => selectTrendingSearch(trending, i);
|
|
7526
|
+
if (renderItem) {
|
|
7527
|
+
return React.createElement("li", { key: `${trending.query}-${i}` }, renderItem(trending, i, onSelect));
|
|
7528
|
+
}
|
|
7529
|
+
return (React.createElement("li", { key: `${trending.query}-${i}` },
|
|
7530
|
+
React.createElement("button", { type: "button", className: "seekora-suggestions-trending-item", style: itemStyle, onMouseDown: (e) => {
|
|
7531
|
+
e.preventDefault();
|
|
7532
|
+
onSelect();
|
|
7533
|
+
} },
|
|
7534
|
+
trending.query,
|
|
7535
|
+
trending.count != null ? (React.createElement("span", { style: { marginLeft: 8, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875em' } }, trending.count)) : null)));
|
|
7536
|
+
}))));
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
/**
|
|
7540
|
+
* SuggestionsError – error message (primitive)
|
|
7541
|
+
*/
|
|
7542
|
+
function SuggestionsError({ className, style, render }) {
|
|
7543
|
+
const { error } = useSuggestionsContext();
|
|
7544
|
+
if (!error)
|
|
7545
|
+
return null;
|
|
7546
|
+
if (render)
|
|
7547
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
7548
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-error', className), style: {
|
|
7549
|
+
padding: 16,
|
|
7550
|
+
color: 'var(--seekora-error, #dc2626)',
|
|
7551
|
+
fontSize: '0.875rem',
|
|
7552
|
+
...style,
|
|
7553
|
+
} }, error.message));
|
|
7554
|
+
}
|
|
7555
|
+
|
|
7556
|
+
/**
|
|
7557
|
+
* SuggestionsDropdownComposition – reference composition
|
|
7558
|
+
*
|
|
7559
|
+
* Example layout built from primitives: SearchInput + DropdownPanel containing
|
|
7560
|
+
* RecentSearchesList (when query empty), SuggestionList, CategoriesTabs, ProductGrid, TrendingList.
|
|
7561
|
+
* Wrap with SearchProvider and SuggestionsProvider. Use as reference or replace
|
|
7562
|
+
* with your own arrangement of the same primitives.
|
|
7563
|
+
*/
|
|
7564
|
+
function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
|
|
7565
|
+
return (React.createElement(SuggestionsProvider, { ...providerProps },
|
|
7566
|
+
React.createElement("div", { className: "seekora-suggestions-dropdown-composition", style: { position: 'relative', width: '100%' } },
|
|
7567
|
+
React.createElement(SearchInput, { placeholder: placeholder }),
|
|
7568
|
+
React.createElement(DropdownPanel, null,
|
|
7569
|
+
React.createElement(SuggestionsError, null),
|
|
7570
|
+
React.createElement(SuggestionsLoading, null),
|
|
7571
|
+
showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
|
|
7572
|
+
React.createElement(SuggestionList, null),
|
|
7573
|
+
showTabs ? React.createElement(CategoriesTabs, null) : null,
|
|
7574
|
+
showProducts ? React.createElement(ProductGrid, null) : null,
|
|
7575
|
+
showTrending ? React.createElement(TrendingList, null) : null))));
|
|
7576
|
+
}
|
|
7577
|
+
|
|
7578
|
+
/**
|
|
7579
|
+
* SectionSearchContext – preset query/filter section state
|
|
7580
|
+
*
|
|
7581
|
+
* For menus, sidebar, front-page blocks. Independent of main search state.
|
|
7582
|
+
*/
|
|
7583
|
+
const SectionSearchContext = React.createContext(null);
|
|
7584
|
+
function useSectionSearchContext() {
|
|
7585
|
+
const context = React.useContext(SectionSearchContext);
|
|
7586
|
+
if (!context) {
|
|
7587
|
+
const error = new Error('useSectionSearchContext must be used within a SectionSearchProvider');
|
|
7588
|
+
log.error('SectionSearchContext: not available', { error: error.message });
|
|
7589
|
+
throw error;
|
|
7590
|
+
}
|
|
7591
|
+
return context;
|
|
7592
|
+
}
|
|
7593
|
+
|
|
7594
|
+
/**
|
|
7595
|
+
* SectionSearchProvider – preset query + filter section
|
|
7596
|
+
*
|
|
7597
|
+
* Runs client.search(query, { refinements, hitsPerPage, sortBy }) on mount and when
|
|
7598
|
+
* query/filters change. Does not use global SearchStateManager. Use for menus,
|
|
7599
|
+
* sidebar, front-page blocks (e.g. "New arrivals", "On sale").
|
|
7600
|
+
*/
|
|
7601
|
+
function extractItems(response) {
|
|
7602
|
+
if (!response)
|
|
7603
|
+
return [];
|
|
7604
|
+
if (Array.isArray(response.results))
|
|
7605
|
+
return response.results;
|
|
7606
|
+
if (Array.isArray(response.hits))
|
|
7607
|
+
return response.hits;
|
|
7608
|
+
const data = response.data;
|
|
7609
|
+
if (data && Array.isArray(data.results))
|
|
7610
|
+
return data.results;
|
|
7611
|
+
if (data && Array.isArray(data.data))
|
|
7612
|
+
return data.data;
|
|
7613
|
+
return [];
|
|
7614
|
+
}
|
|
7615
|
+
function extractTotal(response) {
|
|
7616
|
+
if (!response)
|
|
7617
|
+
return 0;
|
|
7618
|
+
const n = response.totalResults ?? response.total ?? response.total_results;
|
|
7619
|
+
if (typeof n === 'number')
|
|
7620
|
+
return n;
|
|
7621
|
+
const data = response.data;
|
|
7622
|
+
if (data?.total_results != null)
|
|
7623
|
+
return Number(data.total_results);
|
|
7624
|
+
if (data?.data?.total_results != null)
|
|
7625
|
+
return Number(data.data.total_results);
|
|
7626
|
+
return 0;
|
|
7627
|
+
}
|
|
7628
|
+
function SectionSearchProvider({ children, query, refinements = [], maxItems = 12, sortBy, enabled = true, sectionId, }) {
|
|
7629
|
+
const { client } = useSearchContext();
|
|
7630
|
+
const [items, setItems] = React.useState([]);
|
|
7631
|
+
const [loading, setLoading] = React.useState(true);
|
|
7632
|
+
const [error, setError] = React.useState(null);
|
|
7633
|
+
const [totalCount, setTotalCount] = React.useState(0);
|
|
7634
|
+
React.useEffect(() => {
|
|
7635
|
+
if (!enabled || !client?.search) {
|
|
7636
|
+
setItems([]);
|
|
7637
|
+
setLoading(false);
|
|
7638
|
+
setError(null);
|
|
7639
|
+
setTotalCount(0);
|
|
7640
|
+
return;
|
|
7641
|
+
}
|
|
7642
|
+
let cancelled = false;
|
|
7643
|
+
setLoading(true);
|
|
7644
|
+
setError(null);
|
|
7645
|
+
const options = {
|
|
7646
|
+
per_page: maxItems,
|
|
7647
|
+
page: 1,
|
|
7648
|
+
};
|
|
7649
|
+
if (sortBy)
|
|
7650
|
+
options.sort_by = sortBy;
|
|
7651
|
+
if (refinements.length > 0) {
|
|
7652
|
+
options.filter_by = refinements.map((r) => `${r.field}:${r.value}`).join(',');
|
|
7653
|
+
}
|
|
7654
|
+
client
|
|
7655
|
+
.search(query, options)
|
|
7656
|
+
.then((response) => {
|
|
7657
|
+
if (cancelled)
|
|
7658
|
+
return;
|
|
7659
|
+
setItems(extractItems(response));
|
|
7660
|
+
setTotalCount(extractTotal(response));
|
|
7661
|
+
setLoading(false);
|
|
7662
|
+
})
|
|
7663
|
+
.catch((err) => {
|
|
7664
|
+
if (cancelled)
|
|
7665
|
+
return;
|
|
7666
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
7667
|
+
setItems([]);
|
|
7668
|
+
setLoading(false);
|
|
7669
|
+
});
|
|
7670
|
+
return () => {
|
|
7671
|
+
cancelled = true;
|
|
7672
|
+
};
|
|
7673
|
+
}, [client, enabled, query, maxItems, sortBy, refinements]);
|
|
7674
|
+
const trackClick = React.useCallback((item, position) => {
|
|
7675
|
+
if (!client?.trackEvent)
|
|
7676
|
+
return;
|
|
7677
|
+
const id = item?.id ?? item?.objectID;
|
|
7678
|
+
client.trackEvent({
|
|
7679
|
+
event_name: 'section_result_click',
|
|
7680
|
+
clicked_item_id: id,
|
|
7681
|
+
position,
|
|
7682
|
+
section: sectionId,
|
|
7683
|
+
metadata: { section_id: sectionId },
|
|
7684
|
+
}, undefined);
|
|
7685
|
+
}, [client, sectionId]);
|
|
7686
|
+
const value = React.useMemo(() => ({
|
|
7687
|
+
items,
|
|
7688
|
+
loading,
|
|
7689
|
+
error,
|
|
7690
|
+
totalCount,
|
|
7691
|
+
sectionId,
|
|
7692
|
+
trackClick,
|
|
7693
|
+
}), [items, loading, error, totalCount, sectionId, trackClick]);
|
|
7694
|
+
return React.createElement(SectionSearchContext.Provider, { value: value }, children);
|
|
7695
|
+
}
|
|
7696
|
+
|
|
7697
|
+
/**
|
|
7698
|
+
* SectionLoading – loading state for section (primitive)
|
|
7699
|
+
*/
|
|
7700
|
+
function SectionLoading({ className, style, text = 'Loading...' }) {
|
|
7701
|
+
const { loading } = useSectionSearchContext();
|
|
7702
|
+
if (!loading)
|
|
7703
|
+
return null;
|
|
7704
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-text-secondary)', fontSize: '0.875rem', ...style } }, text));
|
|
7705
|
+
}
|
|
7706
|
+
|
|
7707
|
+
/**
|
|
7708
|
+
* SectionError – error state for section (primitive)
|
|
7709
|
+
*/
|
|
7710
|
+
function SectionError({ className, style, render }) {
|
|
7711
|
+
const { error } = useSectionSearchContext();
|
|
7712
|
+
if (!error)
|
|
7713
|
+
return null;
|
|
7714
|
+
if (render)
|
|
7715
|
+
return React.createElement(React.Fragment, null, render(error));
|
|
7716
|
+
return (React.createElement("div", { className: className, style: { padding: 16, color: 'var(--seekora-error,#dc2626)', fontSize: '0.875rem', ...style } }, error.message));
|
|
7717
|
+
}
|
|
7718
|
+
|
|
7719
|
+
/**
|
|
7720
|
+
* SectionItemGrid – generic grid of items from SectionSearchProvider (primitive)
|
|
7721
|
+
*/
|
|
7722
|
+
function SectionItemGrid({ columns = 4, maxItems = 12, className, style, getItemId = (i) => i.id ?? String(i?.objectID ?? ''), getItemTitle = (i) => i.title ?? i?.title ?? '', getItemImage = (i) => i.image ?? i?.image, getItemDescription = (i) => i.description ?? i?.description, getItemUrl = (i) => i.url ?? i?.url, renderItem, }) {
|
|
7723
|
+
const { items, loading, error, trackClick } = useSectionSearchContext();
|
|
7724
|
+
if (loading)
|
|
7725
|
+
return React.createElement(SectionLoading, { className: className, style: style });
|
|
7726
|
+
if (error)
|
|
7727
|
+
return React.createElement(SectionError, { className: className, style: style });
|
|
7728
|
+
return (React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, className: className, style: style, getItemId: getItemId, getItemTitle: getItemTitle, getItemImage: getItemImage, getItemDescription: getItemDescription, getItemUrl: getItemUrl, renderItem: renderItem, onItemClick: (item, index) => trackClick(item, index) }));
|
|
7729
|
+
}
|
|
7730
|
+
|
|
7731
|
+
/**
|
|
7732
|
+
* ProductGallery – product detail image gallery (primitive)
|
|
7733
|
+
*
|
|
7734
|
+
* Uses ImageDisplay with configurable variant (carousel, thumbStrip, etc.).
|
|
7735
|
+
* For use on individual product page.
|
|
7736
|
+
*/
|
|
7737
|
+
function ProductGallery({ images, variant = 'thumbStrip', alt = 'Product', className, style, carouselAutoplay, carouselIntervalMs, }) {
|
|
7738
|
+
return (React.createElement("div", { className: clsx('seekora-product-gallery', className), style: style },
|
|
7739
|
+
React.createElement(ImageDisplay, { images: images, variant: variant, alt: alt, carouselAutoplay: carouselAutoplay, carouselIntervalMs: carouselIntervalMs })));
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7742
|
+
/**
|
|
7743
|
+
* ProductInfo – product detail block (primitive)
|
|
7744
|
+
*
|
|
7745
|
+
* Title, description, price, optional variant selector and CTA. Minimal layout;
|
|
7746
|
+
* override with className/style. For use on individual product page.
|
|
7747
|
+
*/
|
|
7748
|
+
function ProductInfo({ title, description, price, currency = '$', renderVariantSelector, renderCTA, className, style, }) {
|
|
7749
|
+
const priceNum = price != null ? (typeof price === 'number' ? price : parseFloat(String(price))) : null;
|
|
7750
|
+
return (React.createElement("div", { className: clsx('seekora-product-info', className), style: { display: 'flex', flexDirection: 'column', gap: 12, ...style } },
|
|
7751
|
+
React.createElement("h1", { className: "seekora-product-info-title", style: { fontSize: '1.25rem', fontWeight: 600, margin: 0 } }, title),
|
|
7752
|
+
priceNum != null && !Number.isNaN(priceNum) ? (React.createElement("span", { className: "seekora-product-info-price", style: { fontSize: '1.125rem', fontWeight: 600 } },
|
|
7753
|
+
currency,
|
|
7754
|
+
priceNum.toFixed(2))) : null,
|
|
7755
|
+
description ? (React.createElement("p", { className: "seekora-product-info-description", style: { fontSize: '0.875rem', color: 'var(--seekora-text-secondary)', margin: 0, lineHeight: 1.5 } }, description)) : null,
|
|
7756
|
+
renderVariantSelector?.(),
|
|
7757
|
+
renderCTA?.()));
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
/**
|
|
7761
|
+
* ProductRecommendations – related / frequently bought (primitive)
|
|
7762
|
+
*
|
|
7763
|
+
* Renders a section of recommended items (generic ItemGrid or product list).
|
|
7764
|
+
* Pass items and onItemClick; or wrap SectionSearchProvider with preset query for "related".
|
|
7765
|
+
* For use on individual product page.
|
|
7766
|
+
*/
|
|
7767
|
+
function ProductRecommendations({ title = 'You may also like', items, onItemClick, maxItems = 6, columns = 3, className, style, renderItem, }) {
|
|
7768
|
+
if (!items?.length)
|
|
7769
|
+
return null;
|
|
7770
|
+
return (React.createElement("div", { className: clsx('seekora-product-recommendations', className), style: style },
|
|
7771
|
+
React.createElement("h2", { className: "seekora-product-recommendations-title", style: { fontSize: '1rem', fontWeight: 600, marginBottom: 12 } }, title),
|
|
7772
|
+
React.createElement(ItemGrid, { items: items, maxItems: maxItems, columns: columns, onItemClick: onItemClick, renderItem: renderItem })));
|
|
7773
|
+
}
|
|
7774
|
+
|
|
6708
7775
|
/**
|
|
6709
7776
|
* Utility functions for Query Suggestions components
|
|
6710
7777
|
*/
|
|
@@ -13300,10 +14367,12 @@ function updateSuggestionsStyles(theme) {
|
|
|
13300
14367
|
|
|
13301
14368
|
exports.AmazonDropdown = AmazonDropdown;
|
|
13302
14369
|
exports.Breadcrumb = Breadcrumb;
|
|
14370
|
+
exports.CategoriesTabs = CategoriesTabs;
|
|
13303
14371
|
exports.ClearRefinements = ClearRefinements;
|
|
13304
14372
|
exports.CurrentRefinements = CurrentRefinements;
|
|
13305
14373
|
exports.DocSearch = DocSearch;
|
|
13306
14374
|
exports.DocSearchButton = DocSearchButton;
|
|
14375
|
+
exports.DropdownPanel = DropdownPanel;
|
|
13307
14376
|
exports.Facets = Facets;
|
|
13308
14377
|
exports.FederatedDropdown = FederatedDropdown;
|
|
13309
14378
|
exports.Fingerprint = Fingerprint;
|
|
@@ -13312,33 +14381,54 @@ exports.GoogleDropdown = GoogleDropdown;
|
|
|
13312
14381
|
exports.HierarchicalMenu = HierarchicalMenu;
|
|
13313
14382
|
exports.Highlight = Highlight$1;
|
|
13314
14383
|
exports.HitsPerPage = HitsPerPage;
|
|
14384
|
+
exports.ImageDisplay = ImageDisplay;
|
|
13315
14385
|
exports.InfiniteHits = InfiniteHits;
|
|
14386
|
+
exports.ItemCard = ItemCard;
|
|
14387
|
+
exports.ItemGrid = ItemGrid;
|
|
13316
14388
|
exports.MinimalDropdown = MinimalDropdown;
|
|
13317
14389
|
exports.MobileFilters = MobileFilters;
|
|
13318
14390
|
exports.MobileFiltersButton = MobileFiltersButton;
|
|
13319
14391
|
exports.MobileSheetDropdown = MobileSheetDropdown;
|
|
13320
14392
|
exports.Pagination = Pagination;
|
|
13321
14393
|
exports.PinterestDropdown = PinterestDropdown;
|
|
14394
|
+
exports.ProductCard = ProductCard;
|
|
14395
|
+
exports.ProductGallery = ProductGallery;
|
|
14396
|
+
exports.ProductGrid = ProductGrid;
|
|
14397
|
+
exports.ProductInfo = ProductInfo;
|
|
14398
|
+
exports.ProductRecommendations = ProductRecommendations;
|
|
13322
14399
|
exports.QuerySuggestions = QuerySuggestions;
|
|
13323
14400
|
exports.QuerySuggestionsDropdown = QuerySuggestionsDropdown;
|
|
13324
14401
|
exports.RangeInput = RangeInput;
|
|
13325
14402
|
exports.RangeSlider = RangeSlider;
|
|
14403
|
+
exports.RecentSearchesList = RecentSearchesList;
|
|
13326
14404
|
exports.RecentlyViewed = RecentlyViewed;
|
|
13327
14405
|
exports.RelatedProducts = RelatedProducts;
|
|
13328
14406
|
exports.RichQuerySuggestions = RichQuerySuggestions;
|
|
13329
14407
|
exports.SearchBar = SearchBar;
|
|
13330
14408
|
exports.SearchBarWithSuggestions = SearchBarWithSuggestions;
|
|
14409
|
+
exports.SearchInput = SearchInput;
|
|
13331
14410
|
exports.SearchLayout = SearchLayout;
|
|
13332
14411
|
exports.SearchProvider = SearchProvider;
|
|
13333
14412
|
exports.SearchResults = SearchResults;
|
|
14413
|
+
exports.SectionError = SectionError;
|
|
14414
|
+
exports.SectionItemGrid = SectionItemGrid;
|
|
14415
|
+
exports.SectionLoading = SectionLoading;
|
|
14416
|
+
exports.SectionSearchProvider = SectionSearchProvider;
|
|
13334
14417
|
exports.ShopifyDropdown = ShopifyDropdown;
|
|
13335
14418
|
exports.Snippet = Snippet;
|
|
13336
14419
|
exports.SortBy = SortBy;
|
|
13337
14420
|
exports.SpotlightDropdown = SpotlightDropdown;
|
|
13338
14421
|
exports.Stats = Stats;
|
|
13339
14422
|
exports.SuggestionDropdownVariants = SuggestionDropdownVariants;
|
|
14423
|
+
exports.SuggestionItem = SuggestionItem;
|
|
14424
|
+
exports.SuggestionList = SuggestionList;
|
|
13340
14425
|
exports.SuggestionSearchBar = SuggestionSearchBar;
|
|
14426
|
+
exports.SuggestionsDropdownComposition = SuggestionsDropdownComposition;
|
|
14427
|
+
exports.SuggestionsError = SuggestionsError;
|
|
14428
|
+
exports.SuggestionsLoading = SuggestionsLoading;
|
|
14429
|
+
exports.SuggestionsProvider = SuggestionsProvider;
|
|
13341
14430
|
exports.TrendingItems = TrendingItems;
|
|
14431
|
+
exports.TrendingList = TrendingList;
|
|
13342
14432
|
exports.addRecentSearch = addRecentSearch;
|
|
13343
14433
|
exports.addToRecentlyViewed = addToRecentlyViewed;
|
|
13344
14434
|
exports.brandPresets = brandPresets;
|
|
@@ -13384,7 +14474,9 @@ exports.useQuerySuggestionsEnhanced = useQuerySuggestionsEnhanced;
|
|
|
13384
14474
|
exports.useResponsive = useResponsive;
|
|
13385
14475
|
exports.useSearchContext = useSearchContext;
|
|
13386
14476
|
exports.useSearchState = useSearchState;
|
|
14477
|
+
exports.useSectionSearchContext = useSectionSearchContext;
|
|
13387
14478
|
exports.useSeekoraSearch = useSeekoraSearch;
|
|
13388
14479
|
exports.useSmartSuggestions = useSmartSuggestions;
|
|
13389
14480
|
exports.useSuggestionsAnalytics = useSuggestionsAnalytics;
|
|
14481
|
+
exports.useSuggestionsContext = useSuggestionsContext;
|
|
13390
14482
|
//# sourceMappingURL=index.js.map
|