@seekora-ai/ui-sdk-react 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CurrentRefinements.d.ts +22 -2
- package/dist/components/CurrentRefinements.d.ts.map +1 -1
- package/dist/components/CurrentRefinements.js +199 -47
- package/dist/components/Facets.d.ts +30 -1
- package/dist/components/Facets.d.ts.map +1 -1
- package/dist/components/Facets.js +418 -46
- package/dist/components/HierarchicalMenu.d.ts.map +1 -1
- package/dist/components/HierarchicalMenu.js +112 -4
- package/dist/components/InfiniteHits.d.ts +2 -0
- package/dist/components/InfiniteHits.d.ts.map +1 -1
- package/dist/components/InfiniteHits.js +6 -3
- package/dist/components/Pagination.d.ts +47 -1
- package/dist/components/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination.js +166 -28
- package/dist/components/QuerySuggestions.d.ts +2 -0
- package/dist/components/QuerySuggestions.d.ts.map +1 -1
- package/dist/components/QuerySuggestions.js +4 -3
- package/dist/components/QuerySuggestionsDropdown.d.ts +1 -1
- package/dist/components/QuerySuggestionsDropdown.d.ts.map +1 -1
- package/dist/components/QuerySuggestionsDropdown.js +4 -4
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +49 -2
- package/dist/components/Recommendations.d.ts +6 -0
- package/dist/components/Recommendations.d.ts.map +1 -1
- package/dist/components/Recommendations.js +12 -6
- package/dist/components/RichQuerySuggestions.d.ts +11 -0
- package/dist/components/RichQuerySuggestions.d.ts.map +1 -1
- package/dist/components/RichQuerySuggestions.js +2 -3
- package/dist/components/SearchBar.d.ts +18 -0
- package/dist/components/SearchBar.d.ts.map +1 -1
- package/dist/components/SearchBar.js +134 -24
- package/dist/components/SearchProvider.d.ts +8 -1
- package/dist/components/SearchProvider.d.ts.map +1 -1
- package/dist/components/SearchProvider.js +16 -4
- package/dist/components/SearchResults.d.ts +12 -0
- package/dist/components/SearchResults.d.ts.map +1 -1
- package/dist/components/SearchResults.js +11 -5
- package/dist/components/SortBy.d.ts +44 -4
- package/dist/components/SortBy.d.ts.map +1 -1
- package/dist/components/SortBy.js +154 -29
- package/dist/components/Stats.d.ts +14 -0
- package/dist/components/Stats.d.ts.map +1 -1
- package/dist/components/Stats.js +172 -23
- package/dist/components/section-primitives/SectionItemGrid.d.ts +3 -1
- package/dist/components/section-primitives/SectionItemGrid.d.ts.map +1 -1
- package/dist/components/section-primitives/SectionItemGrid.js +3 -2
- package/dist/components/suggestions/AmazonDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/AmazonDropdown.js +4 -6
- package/dist/components/suggestions/GoogleDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/GoogleDropdown.js +4 -8
- package/dist/components/suggestions/MinimalDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MinimalDropdown.js +4 -6
- package/dist/components/suggestions/MobileSheetDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/MobileSheetDropdown.js +4 -6
- package/dist/components/suggestions/PinterestDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/PinterestDropdown.js +4 -8
- package/dist/components/suggestions/ShopifyDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/ShopifyDropdown.js +4 -6
- package/dist/components/suggestions/SpotlightDropdown.d.ts.map +1 -1
- package/dist/components/suggestions/SpotlightDropdown.js +4 -6
- package/dist/components/suggestions/SuggestionSearchBar.d.ts.map +1 -1
- package/dist/components/suggestions/SuggestionSearchBar.js +1 -0
- package/dist/components/suggestions/types.d.ts +2 -0
- package/dist/components/suggestions/types.d.ts.map +1 -1
- package/dist/components/suggestions/utils.d.ts +10 -1
- package/dist/components/suggestions/utils.d.ts.map +1 -1
- package/dist/components/suggestions/utils.js +36 -0
- package/dist/components/suggestions-primitives/SuggestionList.d.ts +8 -1
- package/dist/components/suggestions-primitives/SuggestionList.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionList.js +7 -4
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/SuggestionsDropdownComposition.js +0 -2
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts +16 -4
- package/dist/components/suggestions-primitives/highlightMarkup.d.ts.map +1 -1
- package/dist/components/suggestions-primitives/highlightMarkup.js +42 -4
- package/dist/docsearch/components/Results.d.ts +3 -1
- package/dist/docsearch/components/Results.d.ts.map +1 -1
- package/dist/docsearch/components/Results.js +6 -2
- package/dist/hooks/useClickTracking.d.ts +36 -0
- package/dist/hooks/useClickTracking.d.ts.map +1 -0
- package/dist/hooks/useClickTracking.js +96 -0
- package/dist/hooks/useExperiment.d.ts +25 -0
- package/dist/hooks/useExperiment.d.ts.map +1 -0
- package/dist/hooks/useExperiment.js +146 -0
- package/dist/hooks/useKeyboardNavigation.d.ts +51 -0
- package/dist/hooks/useKeyboardNavigation.d.ts.map +1 -0
- package/dist/hooks/useKeyboardNavigation.js +113 -0
- package/dist/hooks/useQuerySuggestions.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestions.js +19 -3
- package/dist/hooks/useQuerySuggestionsEnhanced.d.ts.map +1 -1
- package/dist/hooks/useQuerySuggestionsEnhanced.js +25 -7
- package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
- package/dist/hooks/useSuggestionsAnalytics.js +6 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/src/index.d.ts +249 -19
- package/dist/src/index.esm.js +1659 -305
- package/dist/src/index.esm.js.map +1 -1
- package/dist/src/index.js +1658 -304
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -8,21 +8,24 @@
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import { useSuggestionsContext } from './SuggestionsContext';
|
|
10
10
|
import { SuggestionItem } from './SuggestionItem';
|
|
11
|
-
import { SuggestionsLoading } from './SuggestionsLoading';
|
|
12
11
|
import { clsx } from 'clsx';
|
|
13
12
|
const listStyle = {
|
|
14
13
|
margin: 0,
|
|
15
14
|
padding: '4px 0',
|
|
16
15
|
};
|
|
17
|
-
export function SuggestionList({ maxItems = 10, className, style, listClassName, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
|
|
16
|
+
export function SuggestionList({ maxItems = 10, className, style, listClassName, showLoadingState = false, renderLoading, enableHighlightMarkup = true, highlightMarkupOptions, renderItem, }) {
|
|
18
17
|
const { suggestions, activeIndex, loading, selectSuggestion, getAllNavigableItems, } = useSuggestionsContext();
|
|
19
18
|
const items = suggestions.slice(0, maxItems);
|
|
20
19
|
const navigableItems = getAllNavigableItems();
|
|
21
20
|
const suggestionStartIndex = navigableItems.findIndex((n) => n.type === 'suggestion');
|
|
22
21
|
const activeIsInSuggestions = suggestionStartIndex >= 0 && activeIndex >= suggestionStartIndex && activeIndex < suggestionStartIndex + items.length;
|
|
23
|
-
if (loading)
|
|
24
|
-
|
|
22
|
+
// When loading with no previous results, show loading only if showLoadingState (default: don't show loading screen)
|
|
23
|
+
if (loading && items.length === 0 && showLoadingState) {
|
|
24
|
+
if (renderLoading)
|
|
25
|
+
return React.createElement(React.Fragment, null, renderLoading());
|
|
26
|
+
return (React.createElement("div", { className: clsx('seekora-suggestions-loading', className), style: { padding: 16, color: 'var(--seekora-text-secondary, #6b7280)', fontSize: '0.875rem', ...style } }, "Loading..."));
|
|
25
27
|
}
|
|
28
|
+
// When loading with previous results, show previous results (no loading UI)
|
|
26
29
|
if (items.length === 0)
|
|
27
30
|
return null;
|
|
28
31
|
return (React.createElement("div", { className: clsx('seekora-suggestions-list', className), style: style },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SuggestionsDropdownComposition.d.ts","sourceRoot":"","sources":["../../../src/components/suggestions-primitives/SuggestionsDropdownComposition.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SuggestionsDropdownComposition.d.ts","sourceRoot":"","sources":["../../../src/components/suggestions-primitives/SuggestionsDropdownComposition.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,WAAW,mCAAoC,SAAQ,IAAI,CAAC,wBAAwB,EAAE,UAAU,CAAC;IACrG,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yBAAyB;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,kBAAyB,EACzB,YAAmB,EACnB,QAAe,EACf,YAAmB,EACnB,WAAW,EACX,GAAG,aAAa,EACjB,EAAE,mCAAmC,qBAgBrC"}
|
|
@@ -15,7 +15,6 @@ import { SuggestionList } from './SuggestionList';
|
|
|
15
15
|
import { CategoriesTabs } from './CategoriesTabs';
|
|
16
16
|
import { ProductGrid } from './ProductGrid';
|
|
17
17
|
import { TrendingList } from './TrendingList';
|
|
18
|
-
import { SuggestionsLoading } from './SuggestionsLoading';
|
|
19
18
|
import { SuggestionsError } from './SuggestionsError';
|
|
20
19
|
export function SuggestionsDropdownComposition({ showRecentSearches = true, showTrending = true, showTabs = true, showProducts = true, placeholder, ...providerProps }) {
|
|
21
20
|
return (React.createElement(SuggestionsProvider, { ...providerProps },
|
|
@@ -23,7 +22,6 @@ export function SuggestionsDropdownComposition({ showRecentSearches = true, show
|
|
|
23
22
|
React.createElement(SearchInput, { placeholder: placeholder }),
|
|
24
23
|
React.createElement(DropdownPanel, null,
|
|
25
24
|
React.createElement(SuggestionsError, null),
|
|
26
|
-
React.createElement(SuggestionsLoading, null),
|
|
27
25
|
showRecentSearches ? React.createElement(RecentSearchesList, null) : null,
|
|
28
26
|
React.createElement(SuggestionList, null),
|
|
29
27
|
showTabs ? React.createElement(CategoriesTabs, null) : null,
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses suggestion text containing <mark>...</mark> and returns React nodes
|
|
3
|
-
* with the marked segments rendered as
|
|
3
|
+
* with the marked segments rendered as styled elements. Safe: inner content
|
|
4
4
|
* is rendered as text, not HTML.
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
|
+
/** Visual highlight styles */
|
|
8
|
+
export type HighlightStyle = 'background' | 'underline' | 'bold' | 'color-only';
|
|
7
9
|
export interface HighlightMarkupOptions {
|
|
8
|
-
/** Class name for the
|
|
10
|
+
/** Class name for the highlight element. */
|
|
9
11
|
markClassName?: string;
|
|
10
|
-
/** Inline style for the
|
|
12
|
+
/** Inline style for the highlight element (merged with computed styles). */
|
|
11
13
|
markStyle?: React.CSSProperties;
|
|
14
|
+
/** Override background color for highlighted segments */
|
|
15
|
+
highlightColor?: string;
|
|
16
|
+
/** Text color for highlighted segments */
|
|
17
|
+
highlightTextColor?: string;
|
|
18
|
+
/** Font weight for highlighted segments */
|
|
19
|
+
highlightFontWeight?: string | number;
|
|
20
|
+
/** Visual highlight style (default: 'background') */
|
|
21
|
+
highlightStyle?: HighlightStyle;
|
|
22
|
+
/** Which HTML element to render for highlights (default: 'mark') */
|
|
23
|
+
highlightTag?: keyof JSX.IntrinsicElements;
|
|
12
24
|
}
|
|
13
25
|
/**
|
|
14
26
|
* Converts a string like "lined <mark>blue</mark>" into React nodes with
|
|
15
|
-
* the marked part rendered as a
|
|
27
|
+
* the marked part rendered as a styled element. When no <mark> tags are
|
|
16
28
|
* present, returns the string as-is.
|
|
17
29
|
*/
|
|
18
30
|
export declare function parseHighlightMarkup(text: string, options?: HighlightMarkupOptions): React.ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"highlightMarkup.d.ts","sourceRoot":"","sources":["../../../src/components/suggestions-primitives/highlightMarkup.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,sBAAsB;IACrC,
|
|
1
|
+
{"version":3,"file":"highlightMarkup.d.ts","sourceRoot":"","sources":["../../../src/components/suggestions-primitives/highlightMarkup.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,8BAA8B;AAC9B,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,YAAY,CAAC;AAEhF,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAChC,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,qDAAqD;IACrD,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,iBAAiB,CAAC;CAC5C;AA4CD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,sBAA2B,GACnC,KAAK,CAAC,SAAS,CAkCjB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses suggestion text containing <mark>...</mark> and returns React nodes
|
|
3
|
-
* with the marked segments rendered as
|
|
3
|
+
* with the marked segments rendered as styled elements. Safe: inner content
|
|
4
4
|
* is rendered as text, not HTML.
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
@@ -10,9 +10,40 @@ const defaultMarkStyle = {
|
|
|
10
10
|
borderRadius: '2px',
|
|
11
11
|
padding: '0 2px',
|
|
12
12
|
};
|
|
13
|
+
/** Compute styles based on highlight options */
|
|
14
|
+
function computeHighlightStyles(options) {
|
|
15
|
+
const style = options.highlightStyle || 'background';
|
|
16
|
+
const base = {};
|
|
17
|
+
switch (style) {
|
|
18
|
+
case 'background':
|
|
19
|
+
base.backgroundColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.4))';
|
|
20
|
+
base.borderRadius = '2px';
|
|
21
|
+
base.padding = '0 2px';
|
|
22
|
+
break;
|
|
23
|
+
case 'underline':
|
|
24
|
+
base.textDecoration = 'underline';
|
|
25
|
+
base.textDecorationColor = options.highlightColor || 'var(--seekora-highlight-bg, rgba(251, 191, 36, 0.8))';
|
|
26
|
+
base.textUnderlineOffset = '2px';
|
|
27
|
+
break;
|
|
28
|
+
case 'bold':
|
|
29
|
+
// Only bold, no background
|
|
30
|
+
break;
|
|
31
|
+
case 'color-only':
|
|
32
|
+
// Only color, no background
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (options.highlightTextColor) {
|
|
36
|
+
base.color = options.highlightTextColor;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
base.color = 'var(--seekora-highlight-color, inherit)';
|
|
40
|
+
}
|
|
41
|
+
base.fontWeight = options.highlightFontWeight || 'var(--seekora-highlight-weight, 500)';
|
|
42
|
+
return base;
|
|
43
|
+
}
|
|
13
44
|
/**
|
|
14
45
|
* Converts a string like "lined <mark>blue</mark>" into React nodes with
|
|
15
|
-
* the marked part rendered as a
|
|
46
|
+
* the marked part rendered as a styled element. When no <mark> tags are
|
|
16
47
|
* present, returns the string as-is.
|
|
17
48
|
*/
|
|
18
49
|
export function parseHighlightMarkup(text, options = {}) {
|
|
@@ -21,11 +52,18 @@ export function parseHighlightMarkup(text, options = {}) {
|
|
|
21
52
|
const parts = text.split(/(<mark>[\s\S]*?<\/mark>)/g);
|
|
22
53
|
if (parts.length <= 1)
|
|
23
54
|
return text;
|
|
24
|
-
const { markClassName, markStyle } = options;
|
|
55
|
+
const { markClassName, markStyle, highlightTag } = options;
|
|
56
|
+
const Tag = (highlightTag || 'mark');
|
|
57
|
+
// Compute styles: if no custom options provided, use legacy defaults
|
|
58
|
+
const hasCustomOptions = options.highlightColor || options.highlightTextColor
|
|
59
|
+
|| options.highlightFontWeight || options.highlightStyle;
|
|
60
|
+
const computedStyle = hasCustomOptions
|
|
61
|
+
? computeHighlightStyles(options)
|
|
62
|
+
: defaultMarkStyle;
|
|
25
63
|
return (React.createElement(React.Fragment, null, parts.map((part, i) => {
|
|
26
64
|
const m = part.match(/^<mark>([\s\S]*)<\/mark>$/);
|
|
27
65
|
if (m) {
|
|
28
|
-
return (React.createElement(
|
|
66
|
+
return (React.createElement(Tag, { key: i, className: markClassName, style: { ...computedStyle, ...markStyle } }, m[1]));
|
|
29
67
|
}
|
|
30
68
|
return part;
|
|
31
69
|
})));
|
|
@@ -14,10 +14,12 @@ interface ResultsProps {
|
|
|
14
14
|
scrollSelectionIntoViewRef?: React.MutableRefObject<boolean>;
|
|
15
15
|
query: string;
|
|
16
16
|
isLoading: boolean;
|
|
17
|
+
/** Show loading state when fetching and no previous hits (default false: show previous results until new render) */
|
|
18
|
+
showLoadingState?: boolean;
|
|
17
19
|
error: string | null;
|
|
18
20
|
translations?: DocSearchTranslations;
|
|
19
21
|
sources?: SearchSource[];
|
|
20
22
|
}
|
|
21
|
-
export declare function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations, sources: _sources, }: ResultsProps): React.JSX.Element;
|
|
23
|
+
export declare function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, showLoadingState, error, translations, sources: _sources, }: ResultsProps): React.JSX.Element;
|
|
22
24
|
export {};
|
|
23
25
|
//# sourceMappingURL=Results.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Results.d.ts","sourceRoot":"","sources":["../../../src/docsearch/components/Results.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEvG,UAAU,WAAW;IACnB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,CAAC,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IAC7C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,GAAG,EAAE,YAAY,GAAG,mBAAmB,KAAK,IAAI,CAAC;IAC5D,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,4GAA4G;IAC5G,0BAA0B,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAuGD,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,WAAW,EACX,aAAa,EACb,QAAQ,EACR,OAAO,EACP,0BAA0B,EAC1B,KAAK,EACL,SAAS,EACT,KAAK,EACL,YAAiB,EACjB,OAAO,EAAE,QAAa,GACvB,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"Results.d.ts","sourceRoot":"","sources":["../../../src/docsearch/components/Results.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEvG,UAAU,WAAW;IACnB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,CAAC,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IAC7C,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,GAAG,EAAE,YAAY,GAAG,mBAAmB,KAAK,IAAI,CAAC;IAC5D,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,4GAA4G;IAC5G,0BAA0B,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,oHAAoH;IACpH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAuGD,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,WAAW,EACX,aAAa,EACb,QAAQ,EACR,OAAO,EACP,0BAA0B,EAC1B,KAAK,EACL,SAAS,EACT,gBAAwB,EACxB,KAAK,EACL,YAAiB,EACjB,OAAO,EAAE,QAAa,GACvB,EAAE,YAAY,qBA0Gd"}
|
|
@@ -79,7 +79,7 @@ function getHitKey(hit, index) {
|
|
|
79
79
|
return hit.objectID;
|
|
80
80
|
return `suggestion-${hit.url}-${index}`;
|
|
81
81
|
}
|
|
82
|
-
export function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
|
|
82
|
+
export function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, showLoadingState = false, error, translations = {}, sources: _sources = [], }) {
|
|
83
83
|
void _sources;
|
|
84
84
|
const listRef = useRef(null);
|
|
85
85
|
useEffect(() => {
|
|
@@ -114,7 +114,7 @@ export function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, s
|
|
|
114
114
|
return (React.createElement("div", { className: "seekora-docsearch-empty" },
|
|
115
115
|
React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
|
|
116
116
|
}
|
|
117
|
-
if (isLoading && hits.length === 0) {
|
|
117
|
+
if (isLoading && hits.length === 0 && showLoadingState) {
|
|
118
118
|
return (React.createElement("div", { className: "seekora-docsearch-loading" },
|
|
119
119
|
React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
|
|
120
120
|
React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
|
|
@@ -122,6 +122,10 @@ export function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, s
|
|
|
122
122
|
React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
|
|
123
123
|
React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
|
|
124
124
|
}
|
|
125
|
+
if (isLoading && hits.length === 0) {
|
|
126
|
+
return React.createElement("div", { className: "seekora-docsearch-empty" });
|
|
127
|
+
}
|
|
128
|
+
// When loading with previous hits, fall through and show them (no loading screen)
|
|
125
129
|
if (error) {
|
|
126
130
|
return (React.createElement("div", { className: "seekora-docsearch-error" },
|
|
127
131
|
React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClickTracking Hook
|
|
3
|
+
*
|
|
4
|
+
* Wraps result link clicks with analytics fire-before-navigate.
|
|
5
|
+
* - For target="_blank" links: fires analytics normally (page stays)
|
|
6
|
+
* - For same-page navigation: uses sendBeacon + small delay before navigation
|
|
7
|
+
* - Tracks click_target: 'new_tab' | 'same_page' | 'in_page'
|
|
8
|
+
* - Includes destination_url and source_url in click event payload
|
|
9
|
+
*/
|
|
10
|
+
export type ClickTarget = 'new_tab' | 'same_page' | 'in_page';
|
|
11
|
+
export interface ClickTrackingOptions {
|
|
12
|
+
/** The destination URL the user is navigating to */
|
|
13
|
+
destinationUrl?: string;
|
|
14
|
+
/** The clicked item ID */
|
|
15
|
+
itemId: string;
|
|
16
|
+
/** Position in search results (1-based) */
|
|
17
|
+
position: number;
|
|
18
|
+
/** Click target type */
|
|
19
|
+
clickTarget?: ClickTarget;
|
|
20
|
+
/** Additional metadata */
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface UseClickTrackingReturn {
|
|
24
|
+
/**
|
|
25
|
+
* Track a click event and optionally delay navigation.
|
|
26
|
+
* Returns a promise that resolves when tracking is complete.
|
|
27
|
+
*/
|
|
28
|
+
trackClick: (options: ClickTrackingOptions) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Create an onClick handler that tracks before navigating.
|
|
31
|
+
* Use this to wrap <a> tag clicks.
|
|
32
|
+
*/
|
|
33
|
+
createClickHandler: (options: ClickTrackingOptions, onComplete?: () => void) => (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare const useClickTracking: () => UseClickTrackingReturn;
|
|
36
|
+
//# sourceMappingURL=useClickTracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClickTracking.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickTracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;OAGG;IACH,kBAAkB,EAAE,CAClB,OAAO,EAAE,oBAAoB,EAC7B,UAAU,CAAC,EAAE,MAAM,IAAI,KACpB,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CACvD;AAED,eAAO,MAAM,gBAAgB,QAAO,sBAwFnC,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClickTracking Hook
|
|
3
|
+
*
|
|
4
|
+
* Wraps result link clicks with analytics fire-before-navigate.
|
|
5
|
+
* - For target="_blank" links: fires analytics normally (page stays)
|
|
6
|
+
* - For same-page navigation: uses sendBeacon + small delay before navigation
|
|
7
|
+
* - Tracks click_target: 'new_tab' | 'same_page' | 'in_page'
|
|
8
|
+
* - Includes destination_url and source_url in click event payload
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useRef } from 'react';
|
|
11
|
+
import { useSearchContext } from '../components/SearchProvider';
|
|
12
|
+
import { useSearchState } from './useSearchState';
|
|
13
|
+
import { sendAnalyticsBeacon } from '@seekora-ai/ui-sdk-core';
|
|
14
|
+
export const useClickTracking = () => {
|
|
15
|
+
const { client, enableAnalytics, stateManager } = useSearchContext();
|
|
16
|
+
const { results } = useSearchState();
|
|
17
|
+
const pendingRef = useRef(false);
|
|
18
|
+
const trackClick = useCallback(async (options) => {
|
|
19
|
+
if (!enableAnalytics || pendingRef.current)
|
|
20
|
+
return;
|
|
21
|
+
pendingRef.current = true;
|
|
22
|
+
try {
|
|
23
|
+
const sourceUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
24
|
+
const searchContext = results?.context;
|
|
25
|
+
// Include A/B test fields from state manager (beacon payloads bypass client.trackEvent)
|
|
26
|
+
const abTestId = stateManager.getAbTestId?.();
|
|
27
|
+
const abVariant = stateManager.getAbVariant?.();
|
|
28
|
+
const eventPayload = {
|
|
29
|
+
event_name: 'product_click',
|
|
30
|
+
clicked_item_id: options.itemId,
|
|
31
|
+
position: options.position,
|
|
32
|
+
destination_url: options.destinationUrl || '',
|
|
33
|
+
source_url: sourceUrl || '',
|
|
34
|
+
click_target: options.clickTarget || 'in_page',
|
|
35
|
+
...(abTestId && { ab_test_id: abTestId }),
|
|
36
|
+
...(abVariant && { ab_variant: abVariant }),
|
|
37
|
+
...options.metadata,
|
|
38
|
+
};
|
|
39
|
+
// For same_page navigation, use sendBeacon for reliability
|
|
40
|
+
if (options.clickTarget === 'same_page') {
|
|
41
|
+
if (client.trackClick) {
|
|
42
|
+
// Try sendBeacon approach
|
|
43
|
+
const baseUrl = client.baseUrl || client.apiUrl || '';
|
|
44
|
+
if (baseUrl) {
|
|
45
|
+
sendAnalyticsBeacon(`${baseUrl}/api/analytics/event`, {
|
|
46
|
+
...eventPayload,
|
|
47
|
+
...(searchContext || {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Normal tracking via client SDK
|
|
54
|
+
if (client.trackClick) {
|
|
55
|
+
await client.trackClick(options.itemId, options.position, searchContext);
|
|
56
|
+
}
|
|
57
|
+
else if (client.trackEvent) {
|
|
58
|
+
await client.trackEvent(eventPayload, searchContext);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Silently fail — analytics should never break navigation
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
pendingRef.current = false;
|
|
67
|
+
}
|
|
68
|
+
}, [client, enableAnalytics, results, stateManager]);
|
|
69
|
+
const createClickHandler = useCallback((options, onComplete) => {
|
|
70
|
+
return (e) => {
|
|
71
|
+
const anchor = e.currentTarget;
|
|
72
|
+
const href = anchor.href;
|
|
73
|
+
const target = anchor.target;
|
|
74
|
+
const isNewTab = target === '_blank' || e.metaKey || e.ctrlKey;
|
|
75
|
+
const clickTarget = isNewTab ? 'new_tab' : 'same_page';
|
|
76
|
+
const trackOptions = { ...options, destinationUrl: href, clickTarget };
|
|
77
|
+
if (isNewTab) {
|
|
78
|
+
// New tab: page stays, track normally
|
|
79
|
+
trackClick(trackOptions);
|
|
80
|
+
onComplete?.();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Same page: prevent default, track with beacon, then navigate
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
trackClick(trackOptions).then(() => {
|
|
86
|
+
// Small delay to ensure beacon fires
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
onComplete?.();
|
|
89
|
+
window.location.href = href;
|
|
90
|
+
}, 50);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}, [trackClick]);
|
|
95
|
+
return { trackClick, createClickHandler };
|
|
96
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useExperiment Hook
|
|
3
|
+
*
|
|
4
|
+
* Fetches experiment variant assignments from the backend and caches them.
|
|
5
|
+
* Auto-injects ab_test_id and ab_variant into SearchProvider context.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { experimentId, variantId, variantConfig, isLoading } = useExperiment('search-ranking-v2');
|
|
9
|
+
*/
|
|
10
|
+
export interface ExperimentAssignment {
|
|
11
|
+
experiment_id: string;
|
|
12
|
+
variant_id: string;
|
|
13
|
+
variant_config?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface UseExperimentReturn {
|
|
16
|
+
experimentId: string | null;
|
|
17
|
+
variantId: string | null;
|
|
18
|
+
variantConfig: Record<string, unknown> | null;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
/** All active assignments for the current user */
|
|
22
|
+
allAssignments: ExperimentAssignment[];
|
|
23
|
+
}
|
|
24
|
+
export declare const useExperiment: (experimentId?: string) => UseExperimentReturn;
|
|
25
|
+
//# sourceMappingURL=useExperiment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExperiment.d.ts","sourceRoot":"","sources":["../../src/hooks/useExperiment.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,cAAc,EAAE,oBAAoB,EAAE,CAAC;CACxC;AAsCD,eAAO,MAAM,aAAa,GAAI,eAAe,MAAM,KAAG,mBAkHrD,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useExperiment Hook
|
|
3
|
+
*
|
|
4
|
+
* Fetches experiment variant assignments from the backend and caches them.
|
|
5
|
+
* Auto-injects ab_test_id and ab_variant into SearchProvider context.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { experimentId, variantId, variantConfig, isLoading } = useExperiment('search-ranking-v2');
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
11
|
+
import { useSearchContext } from '../components/SearchProvider';
|
|
12
|
+
const CACHE_PREFIX = 'seekora_experiment_';
|
|
13
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
14
|
+
function getCachedAssignment(userKey, experimentId) {
|
|
15
|
+
if (typeof localStorage === 'undefined')
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const key = `${CACHE_PREFIX}${userKey}_${experimentId}`;
|
|
19
|
+
const raw = localStorage.getItem(key);
|
|
20
|
+
if (!raw)
|
|
21
|
+
return null;
|
|
22
|
+
const cached = JSON.parse(raw);
|
|
23
|
+
if (Date.now() - cached.timestamp > CACHE_TTL_MS) {
|
|
24
|
+
localStorage.removeItem(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return cached.assignment;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function setCachedAssignment(userKey, assignment) {
|
|
34
|
+
if (typeof localStorage === 'undefined')
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
const key = `${CACHE_PREFIX}${userKey}_${assignment.experiment_id}`;
|
|
38
|
+
const cached = { assignment, timestamp: Date.now() };
|
|
39
|
+
localStorage.setItem(key, JSON.stringify(cached));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Silently fail — localStorage may be full or unavailable
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const useExperiment = (experimentId) => {
|
|
46
|
+
const { client, stateManager } = useSearchContext();
|
|
47
|
+
const [allAssignments, setAllAssignments] = useState([]);
|
|
48
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
const fetchedRef = useRef(false);
|
|
51
|
+
const fetchAssignments = useCallback(async () => {
|
|
52
|
+
if (fetchedRef.current)
|
|
53
|
+
return;
|
|
54
|
+
fetchedRef.current = true;
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
try {
|
|
57
|
+
// Get user key from client config or generate anonymous one
|
|
58
|
+
const clientAny = client;
|
|
59
|
+
const baseUrl = clientAny.baseUrl || clientAny.apiUrl || '';
|
|
60
|
+
const storeId = clientAny.storeId || clientAny.xStoreId || '';
|
|
61
|
+
if (!baseUrl || !storeId) {
|
|
62
|
+
setIsLoading(false);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Try to determine user key
|
|
66
|
+
const userKey = clientAny.userId || clientAny.anonymousId || clientAny.anonId || '';
|
|
67
|
+
if (!userKey) {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Check cache first for specific experiment
|
|
72
|
+
if (experimentId) {
|
|
73
|
+
const cached = getCachedAssignment(userKey, experimentId);
|
|
74
|
+
if (cached) {
|
|
75
|
+
setAllAssignments([cached]);
|
|
76
|
+
// Update state manager and SDK client with cached assignment
|
|
77
|
+
stateManager.setAbTest(cached.experiment_id, cached.variant_id);
|
|
78
|
+
if (typeof client.setAbTest === 'function') {
|
|
79
|
+
client.setAbTest(cached.experiment_id, cached.variant_id);
|
|
80
|
+
}
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Fetch from backend (requires store authentication for orgID resolution)
|
|
86
|
+
const readSecret = clientAny.readSecret || clientAny.storeSecret || '';
|
|
87
|
+
const headers = {
|
|
88
|
+
'x-storeid': storeId,
|
|
89
|
+
'x-user-id': userKey,
|
|
90
|
+
};
|
|
91
|
+
if (readSecret) {
|
|
92
|
+
headers['x-storesecret'] = readSecret;
|
|
93
|
+
}
|
|
94
|
+
const response = await fetch(`${baseUrl}/api/v1/experiments/assignment`, {
|
|
95
|
+
headers,
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Failed to fetch experiment assignments: ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
const assignments = data.assignments || [];
|
|
102
|
+
// Cache all assignments
|
|
103
|
+
assignments.forEach(a => setCachedAssignment(userKey, a));
|
|
104
|
+
setAllAssignments(assignments);
|
|
105
|
+
// If a specific experiment was requested, set it on the state manager and SDK client
|
|
106
|
+
const setAbFields = (expId, varId) => {
|
|
107
|
+
stateManager.setAbTest(expId, varId);
|
|
108
|
+
if (typeof client.setAbTest === 'function') {
|
|
109
|
+
client.setAbTest(expId, varId);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (experimentId) {
|
|
113
|
+
const match = assignments.find(a => a.experiment_id === experimentId);
|
|
114
|
+
if (match) {
|
|
115
|
+
setAbFields(match.experiment_id, match.variant_id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (assignments.length > 0) {
|
|
119
|
+
// Use the first assignment by default
|
|
120
|
+
setAbFields(assignments[0].experiment_id, assignments[0].variant_id);
|
|
121
|
+
}
|
|
122
|
+
setError(null);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
setIsLoading(false);
|
|
129
|
+
}
|
|
130
|
+
}, [client, stateManager, experimentId]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
fetchAssignments();
|
|
133
|
+
}, [fetchAssignments]);
|
|
134
|
+
// Find the specific experiment assignment
|
|
135
|
+
const assignment = experimentId
|
|
136
|
+
? allAssignments.find(a => a.experiment_id === experimentId)
|
|
137
|
+
: allAssignments[0] || null;
|
|
138
|
+
return {
|
|
139
|
+
experimentId: assignment?.experiment_id || null,
|
|
140
|
+
variantId: assignment?.variant_id || null,
|
|
141
|
+
variantConfig: assignment?.variant_config || null,
|
|
142
|
+
isLoading,
|
|
143
|
+
error,
|
|
144
|
+
allAssignments,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useKeyboardNavigation Hook
|
|
3
|
+
*
|
|
4
|
+
* Shared focus management for navigable component lists.
|
|
5
|
+
* Handles arrow keys, Enter/Space, Escape, Home/End.
|
|
6
|
+
* Provides visible focus indicators via CSS variables.
|
|
7
|
+
*/
|
|
8
|
+
export interface KeyboardNavigationOptions {
|
|
9
|
+
/** Total number of items in the list */
|
|
10
|
+
itemCount: number;
|
|
11
|
+
/** Orientation: 'vertical' for up/down, 'horizontal' for left/right */
|
|
12
|
+
orientation?: 'vertical' | 'horizontal';
|
|
13
|
+
/** Whether navigation wraps around at boundaries */
|
|
14
|
+
wrap?: boolean;
|
|
15
|
+
/** Callback when an item is selected (Enter/Space) */
|
|
16
|
+
onSelect?: (index: number) => void;
|
|
17
|
+
/** Callback when Escape is pressed */
|
|
18
|
+
onEscape?: () => void;
|
|
19
|
+
/** Callback when Delete/Backspace is pressed on focused item */
|
|
20
|
+
onDelete?: (index: number) => void;
|
|
21
|
+
/** Whether keyboard navigation is enabled */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface UseKeyboardNavigationReturn {
|
|
25
|
+
/** Currently focused item index (-1 = none) */
|
|
26
|
+
activeIndex: number;
|
|
27
|
+
/** Set active index programmatically */
|
|
28
|
+
setActiveIndex: (index: number) => void;
|
|
29
|
+
/** Props to spread onto the container element */
|
|
30
|
+
containerProps: {
|
|
31
|
+
tabIndex: number;
|
|
32
|
+
role: string;
|
|
33
|
+
'aria-activedescendant': string | undefined;
|
|
34
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
35
|
+
onBlur: () => void;
|
|
36
|
+
};
|
|
37
|
+
/** Get props for an individual item */
|
|
38
|
+
getItemProps: (index: number) => {
|
|
39
|
+
id: string;
|
|
40
|
+
role: string;
|
|
41
|
+
tabIndex: number;
|
|
42
|
+
'aria-selected': boolean;
|
|
43
|
+
onMouseEnter: () => void;
|
|
44
|
+
onFocus: () => void;
|
|
45
|
+
style: React.CSSProperties;
|
|
46
|
+
};
|
|
47
|
+
/** Whether any item is currently focused */
|
|
48
|
+
hasFocus: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare const useKeyboardNavigation: (options: KeyboardNavigationOptions) => UseKeyboardNavigationReturn;
|
|
51
|
+
//# sourceMappingURL=useKeyboardNavigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useKeyboardNavigation.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardNavigation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,yBAAyB;IACxC,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,oDAAoD;IACpD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,iDAAiD;IACjD,cAAc,EAAE;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5C,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;QAC5C,MAAM,EAAE,MAAM,IAAI,CAAC;KACpB,CAAC;IACF,uCAAuC;IACvC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK;QAC/B,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,eAAe,EAAE,OAAO,CAAC;QACzB,YAAY,EAAE,MAAM,IAAI,CAAC;QACzB,OAAO,EAAE,MAAM,IAAI,CAAC;QACpB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC;KAC5B,CAAC;IACF,4CAA4C;IAC5C,QAAQ,EAAE,OAAO,CAAC;CACnB;AAaD,eAAO,MAAM,qBAAqB,GAAI,SAAS,yBAAyB,KAAG,2BAkH1E,CAAC"}
|