@seekora-ai/ui-sdk-react 0.2.0 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/FederatedDropdown.d.ts +6 -0
  2. package/dist/components/FederatedDropdown.d.ts.map +1 -1
  3. package/dist/components/FederatedDropdown.js +4 -2
  4. package/dist/components/SearchBarWithSuggestions.d.ts +4 -0
  5. package/dist/components/SearchBarWithSuggestions.d.ts.map +1 -1
  6. package/dist/components/SearchBarWithSuggestions.js +2 -2
  7. package/dist/components/SearchResults.d.ts.map +1 -1
  8. package/dist/components/SearchResults.js +24 -58
  9. package/dist/docsearch/components/DocSearch.d.ts +4 -0
  10. package/dist/docsearch/components/DocSearch.d.ts.map +1 -0
  11. package/dist/docsearch/components/DocSearch.js +91 -0
  12. package/dist/docsearch/components/DocSearchButton.d.ts +4 -0
  13. package/dist/docsearch/components/DocSearchButton.d.ts.map +1 -0
  14. package/dist/docsearch/components/DocSearchButton.js +12 -0
  15. package/dist/docsearch/components/Footer.d.ts +8 -0
  16. package/dist/docsearch/components/Footer.d.ts.map +1 -0
  17. package/dist/docsearch/components/Footer.js +23 -0
  18. package/dist/docsearch/components/Highlight.d.ts +9 -0
  19. package/dist/docsearch/components/Highlight.d.ts.map +1 -0
  20. package/dist/docsearch/components/Highlight.js +48 -0
  21. package/dist/docsearch/components/Hit.d.ts +15 -0
  22. package/dist/docsearch/components/Hit.d.ts.map +1 -0
  23. package/dist/docsearch/components/Hit.js +96 -0
  24. package/dist/docsearch/components/Modal.d.ts +9 -0
  25. package/dist/docsearch/components/Modal.d.ts.map +1 -0
  26. package/dist/docsearch/components/Modal.js +54 -0
  27. package/dist/docsearch/components/Results.d.ts +23 -0
  28. package/dist/docsearch/components/Results.d.ts.map +1 -0
  29. package/dist/docsearch/components/Results.js +145 -0
  30. package/dist/docsearch/components/SearchBox.d.ts +12 -0
  31. package/dist/docsearch/components/SearchBox.d.ts.map +1 -0
  32. package/dist/docsearch/components/SearchBox.js +18 -0
  33. package/dist/docsearch/hooks/useDocSearch.d.ts +33 -0
  34. package/dist/docsearch/hooks/useDocSearch.d.ts.map +1 -0
  35. package/dist/docsearch/hooks/useDocSearch.js +211 -0
  36. package/dist/docsearch/hooks/useKeyboard.d.ts +17 -0
  37. package/dist/docsearch/hooks/useKeyboard.d.ts.map +1 -0
  38. package/dist/docsearch/hooks/useKeyboard.js +71 -0
  39. package/dist/docsearch/hooks/useSeekoraSearch.d.ts +27 -0
  40. package/dist/docsearch/hooks/useSeekoraSearch.d.ts.map +1 -0
  41. package/dist/docsearch/hooks/useSeekoraSearch.js +207 -0
  42. package/dist/docsearch/index.d.ts +13 -0
  43. package/dist/docsearch/index.d.ts.map +1 -0
  44. package/dist/docsearch/index.js +11 -0
  45. package/dist/docsearch/types.d.ts +172 -0
  46. package/dist/docsearch/types.d.ts.map +1 -0
  47. package/dist/docsearch/types.js +4 -0
  48. package/dist/docsearch.css +237 -0
  49. package/dist/hooks/useAnalytics.d.ts +8 -4
  50. package/dist/hooks/useAnalytics.d.ts.map +1 -1
  51. package/dist/hooks/useAnalytics.js +14 -9
  52. package/dist/hooks/useQuerySuggestionsEnhanced.js +1 -1
  53. package/dist/hooks/useSuggestionsAnalytics.d.ts +3 -1
  54. package/dist/hooks/useSuggestionsAnalytics.d.ts.map +1 -1
  55. package/dist/hooks/useSuggestionsAnalytics.js +11 -9
  56. package/dist/index.d.ts +4 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +11 -0
  59. package/dist/index.umd.js +1 -1
  60. package/dist/src/index.d.ts +271 -7
  61. package/dist/src/index.esm.js +1705 -84
  62. package/dist/src/index.esm.js.map +1 -1
  63. package/dist/src/index.js +1712 -83
  64. package/dist/src/index.js.map +1 -1
  65. package/package.json +9 -6
  66. package/src/docsearch/docsearch.css +237 -0
@@ -0,0 +1,54 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ export function Modal({ isOpen, onClose, children }) {
4
+ const overlayRef = useRef(null);
5
+ const containerRef = useRef(null);
6
+ useEffect(() => {
7
+ const handleClickOutside = (event) => {
8
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
9
+ onClose();
10
+ }
11
+ };
12
+ if (isOpen)
13
+ document.addEventListener('mousedown', handleClickOutside);
14
+ return () => document.removeEventListener('mousedown', handleClickOutside);
15
+ }, [isOpen, onClose]);
16
+ useEffect(() => {
17
+ if (isOpen) {
18
+ const originalOverflow = document.body.style.overflow;
19
+ document.body.style.overflow = 'hidden';
20
+ return () => { document.body.style.overflow = originalOverflow; };
21
+ }
22
+ }, [isOpen]);
23
+ useEffect(() => {
24
+ if (!isOpen || !containerRef.current)
25
+ return;
26
+ const container = containerRef.current;
27
+ const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
28
+ const firstElement = focusableElements[0];
29
+ const lastElement = focusableElements[focusableElements.length - 1];
30
+ const handleTabKey = (e) => {
31
+ if (e.key !== 'Tab')
32
+ return;
33
+ if (e.shiftKey) {
34
+ if (document.activeElement === firstElement) {
35
+ e.preventDefault();
36
+ lastElement?.focus();
37
+ }
38
+ }
39
+ else {
40
+ if (document.activeElement === lastElement) {
41
+ e.preventDefault();
42
+ firstElement?.focus();
43
+ }
44
+ }
45
+ };
46
+ container.addEventListener('keydown', handleTabKey);
47
+ return () => container.removeEventListener('keydown', handleTabKey);
48
+ }, [isOpen]);
49
+ if (!isOpen || typeof document === 'undefined')
50
+ return null;
51
+ const modalContent = (React.createElement("div", { ref: overlayRef, className: "seekora-docsearch-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Search documentation" },
52
+ React.createElement("div", { ref: containerRef, className: "seekora-docsearch-container" }, children)));
53
+ return createPortal(modalContent, document.body);
54
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import type { DocSearchHit, DocSearchSuggestion, DocSearchTranslations, SearchSource } from '../types';
3
+ interface GroupedHits {
4
+ source: SearchSource;
5
+ items: DocSearchSuggestion[];
6
+ }
7
+ interface ResultsProps {
8
+ hits: (DocSearchHit | DocSearchSuggestion)[];
9
+ groupedHits?: GroupedHits[];
10
+ selectedIndex: number;
11
+ onSelect: (hit: DocSearchHit | DocSearchSuggestion) => void;
12
+ onHover: (index: number) => void;
13
+ /** When true, scroll the selected item into view (set by keyboard nav only; avoids scroll-on-hover jump) */
14
+ scrollSelectionIntoViewRef?: React.MutableRefObject<boolean>;
15
+ query: string;
16
+ isLoading: boolean;
17
+ error: string | null;
18
+ translations?: DocSearchTranslations;
19
+ sources?: SearchSource[];
20
+ }
21
+ export declare function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations, sources: _sources, }: ResultsProps): React.JSX.Element;
22
+ export {};
23
+ //# sourceMappingURL=Results.d.ts.map
@@ -0,0 +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,qBAsGd"}
@@ -0,0 +1,145 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Hit } from './Hit';
3
+ function getTypeLevel(type) {
4
+ if (!type)
5
+ return 1;
6
+ const match = type.match(/^lvl(\d+)$/);
7
+ return match ? parseInt(match[1], 10) : 1;
8
+ }
9
+ function isChildType(type) {
10
+ return getTypeLevel(type) >= 2;
11
+ }
12
+ /** Build a stable grouping key from all hierarchy levels (lvl0 through lvl5). */
13
+ function getHierarchyKey(hit) {
14
+ const h = hit.hierarchy ?? {};
15
+ const parts = [
16
+ h.lvl0 ?? '',
17
+ h.lvl1 ?? '',
18
+ h.lvl2 ?? '',
19
+ h.lvl3 ?? '',
20
+ h.lvl4 ?? '',
21
+ h.lvl5 ?? '',
22
+ ];
23
+ return parts.join('\0');
24
+ }
25
+ /** Build a display breadcrumb from hierarchy, skipping consecutive duplicates (e.g. lvl0 === lvl1). */
26
+ function getHierarchyBreadcrumb(hit) {
27
+ const h = hit.hierarchy ?? {};
28
+ const parts = [h.lvl0, h.lvl1, h.lvl2, h.lvl3, h.lvl4, h.lvl5].filter((v) => typeof v === 'string' && v.length > 0);
29
+ const deduped = [];
30
+ for (const p of parts) {
31
+ if (deduped[deduped.length - 1] !== p)
32
+ deduped.push(p);
33
+ }
34
+ return deduped.join(' › ');
35
+ }
36
+ function groupHitsByHierarchy(hits) {
37
+ const hitToIndex = new Map();
38
+ hits.forEach((h, i) => hitToIndex.set(h, i));
39
+ const groups = new Map();
40
+ for (const hit of hits) {
41
+ const key = getHierarchyKey(hit);
42
+ if (!groups.has(key))
43
+ groups.set(key, []);
44
+ groups.get(key).push(hit);
45
+ }
46
+ const entries = Array.from(groups.entries());
47
+ entries.sort(([, aHits], [, bHits]) => {
48
+ const aMin = Math.min(...aHits.map(h => hitToIndex.get(h) ?? 0));
49
+ const bMin = Math.min(...bHits.map(h => hitToIndex.get(h) ?? 0));
50
+ return aMin - bMin;
51
+ });
52
+ const result = [];
53
+ for (const [, groupHits] of entries) {
54
+ const sortedHits = [...groupHits].sort((a, b) => {
55
+ const aType = a.type;
56
+ const bType = b.type;
57
+ return getTypeLevel(aType) - getTypeLevel(bType);
58
+ });
59
+ const markedHits = sortedHits.map((hit, index) => {
60
+ const suggestion = hit;
61
+ const isChild = isChildType(suggestion.type);
62
+ const nextHit = sortedHits[index + 1];
63
+ const isLastChild = isChild && (!nextHit || !isChildType(nextHit.type));
64
+ return { ...hit, isChild, isLastChild };
65
+ });
66
+ const category = getHierarchyBreadcrumb(groupHits[0]);
67
+ result.push({ category: category || null, hits: markedHits });
68
+ }
69
+ return result;
70
+ }
71
+ function getGlobalIndexMulti(groups, groupIndex, hitIndex) {
72
+ let index = 0;
73
+ for (let i = 0; i < groupIndex; i++)
74
+ index += groups[i].hits.length;
75
+ return index + hitIndex;
76
+ }
77
+ function getHitKey(hit, index) {
78
+ if ('objectID' in hit)
79
+ return hit.objectID;
80
+ return `suggestion-${hit.url}-${index}`;
81
+ }
82
+ export function Results({ hits, groupedHits, selectedIndex, onSelect, onHover, scrollSelectionIntoViewRef, query, isLoading, error, translations = {}, sources: _sources = [], }) {
83
+ void _sources;
84
+ const listRef = useRef(null);
85
+ useEffect(() => {
86
+ if (!listRef.current || hits.length === 0)
87
+ return;
88
+ // Only scroll when selection changed via keyboard (avoids scroll-to-selected on hover)
89
+ if (scrollSelectionIntoViewRef && !scrollSelectionIntoViewRef.current)
90
+ return;
91
+ // listRef's direct children are groups (li.seekora-docsearch-results-group), not hits.
92
+ // Find the actual hit element at flat selectedIndex.
93
+ const groupEls = listRef.current.querySelectorAll('.seekora-docsearch-results-group');
94
+ let idx = 0;
95
+ for (const groupEl of groupEls) {
96
+ const itemList = groupEl.querySelector('.seekora-docsearch-results-group-items');
97
+ if (!itemList)
98
+ continue;
99
+ const items = itemList.children;
100
+ for (let i = 0; i < items.length; i++) {
101
+ if (idx === selectedIndex) {
102
+ items[i].scrollIntoView({ block: 'nearest' });
103
+ if (scrollSelectionIntoViewRef)
104
+ scrollSelectionIntoViewRef.current = false;
105
+ return;
106
+ }
107
+ idx++;
108
+ }
109
+ }
110
+ if (scrollSelectionIntoViewRef)
111
+ scrollSelectionIntoViewRef.current = false;
112
+ }, [selectedIndex, hits.length, scrollSelectionIntoViewRef]);
113
+ if (!query) {
114
+ return (React.createElement("div", { className: "seekora-docsearch-empty" },
115
+ React.createElement("p", { className: "seekora-docsearch-empty-text" }, translations.searchPlaceholder || 'Type to start searching...')));
116
+ }
117
+ if (isLoading && hits.length === 0) {
118
+ return (React.createElement("div", { className: "seekora-docsearch-loading" },
119
+ React.createElement("div", { className: "seekora-docsearch-loading-spinner" },
120
+ React.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", "aria-hidden": "true" },
121
+ React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "50", strokeDashoffset: "15" },
122
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 12 12", to: "360 12 12", dur: "0.8s", repeatCount: "indefinite" })))),
123
+ React.createElement("p", { className: "seekora-docsearch-loading-text" }, translations.loadingText || 'Searching...')));
124
+ }
125
+ if (error) {
126
+ return (React.createElement("div", { className: "seekora-docsearch-error" },
127
+ React.createElement("p", { className: "seekora-docsearch-error-text" }, translations.errorText || error)));
128
+ }
129
+ if (hits.length === 0 && query) {
130
+ return (React.createElement("div", { className: "seekora-docsearch-no-results" },
131
+ React.createElement("p", { className: "seekora-docsearch-no-results-text" }, translations.noResultsText || `No results found for "${query}"`)));
132
+ }
133
+ const displayGroups = groupedHits && groupedHits.length > 0
134
+ ? groupedHits.map(g => ({ category: g.source.name, sourceId: g.source.id, openInNewTab: g.source.openInNewTab, hits: g.items }))
135
+ : groupHitsByHierarchy(hits).map(g => ({ category: g.category, sourceId: 'default', openInNewTab: false, hits: g.hits }));
136
+ return (React.createElement("div", { className: "seekora-docsearch-results" },
137
+ React.createElement("ul", { ref: listRef, id: "seekora-docsearch-results", className: "seekora-docsearch-results-list", role: "listbox" }, displayGroups.map((group, groupIndex) => (React.createElement("li", { key: group.sourceId + '-' + groupIndex, className: "seekora-docsearch-results-group" },
138
+ group.category && React.createElement("div", { className: "seekora-docsearch-results-group-header" }, group.category),
139
+ React.createElement("ul", { className: "seekora-docsearch-results-group-items" }, group.hits.map((hit, hitIndex) => {
140
+ const globalIndex = getGlobalIndexMulti(displayGroups, groupIndex, hitIndex);
141
+ const extHit = hit;
142
+ return (React.createElement("li", { key: getHitKey(hit, hitIndex) },
143
+ React.createElement(Hit, { hit: hit, isSelected: globalIndex === selectedIndex, onClick: () => onSelect(hit), onMouseEnter: () => onHover(globalIndex), openInNewTab: group.openInNewTab, isChild: extHit.isChild, isLastChild: extHit.isLastChild, hierarchyType: hit.type })));
144
+ }))))))));
145
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ interface SearchBoxProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ onKeyDown: (event: React.KeyboardEvent) => void;
6
+ placeholder?: string;
7
+ isLoading?: boolean;
8
+ onClear?: () => void;
9
+ }
10
+ export declare function SearchBox({ value, onChange, onKeyDown, placeholder, isLoading, onClear, }: SearchBoxProps): React.JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=SearchBox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../../../src/docsearch/components/SearchBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAEjD,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,WAAuC,EACvC,SAAiB,EACjB,OAAO,GACR,EAAE,cAAc,qBAiDhB"}
@@ -0,0 +1,18 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ export function SearchBox({ value, onChange, onKeyDown, placeholder = 'Search documentation...', isLoading = false, onClear, }) {
3
+ const inputRef = useRef(null);
4
+ useEffect(() => { if (inputRef.current)
5
+ inputRef.current.focus(); }, []);
6
+ const handleChange = (event) => onChange(event.target.value);
7
+ const handleClear = () => { onChange(''); onClear?.(); inputRef.current?.focus(); };
8
+ return (React.createElement("div", { className: "seekora-docsearch-searchbox" },
9
+ React.createElement("label", { className: "seekora-docsearch-searchbox-icon", htmlFor: "seekora-docsearch-input" }, isLoading ? (React.createElement("span", { className: "seekora-docsearch-spinner", "aria-hidden": "true" },
10
+ React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20" },
11
+ React.createElement("circle", { cx: "10", cy: "10", r: "8", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeDasharray: "40", strokeDashoffset: "10" },
12
+ React.createElement("animateTransform", { attributeName: "transform", type: "rotate", from: "0 10 10", to: "360 10 10", dur: "0.8s", repeatCount: "indefinite" }))))) : (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
13
+ React.createElement("path", { d: "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", fill: "currentColor" })))),
14
+ React.createElement("input", { ref: inputRef, id: "seekora-docsearch-input", className: "seekora-docsearch-input", type: "text", value: value, onChange: handleChange, onKeyDown: onKeyDown, placeholder: placeholder, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: false, "aria-autocomplete": "list", "aria-controls": "seekora-docsearch-results" }),
15
+ value && (React.createElement("button", { type: "button", className: "seekora-docsearch-clear", onClick: handleClear, "aria-label": "Clear search" },
16
+ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true" },
17
+ React.createElement("path", { d: "M4.28 3.22a.75.75 0 00-1.06 1.06L6.94 8l-3.72 3.72a.75.75 0 101.06 1.06L8 9.06l3.72 3.72a.75.75 0 101.06-1.06L9.06 8l3.72-3.72a.75.75 0 00-1.06-1.06L8 6.94 4.28 3.22z", fill: "currentColor" }))))));
18
+ }
@@ -0,0 +1,33 @@
1
+ import type { DocSearchHit, DocSearchSuggestion, SearchSource } from '../types';
2
+ interface UseDocSearchOptions {
3
+ apiEndpoint?: string;
4
+ apiKey?: string;
5
+ sources?: SearchSource[];
6
+ maxResults?: number;
7
+ debounceMs?: number;
8
+ processGroupedResults?: (sourceId: string, items: DocSearchSuggestion[]) => DocSearchSuggestion[];
9
+ }
10
+ export declare function useDocSearch(options: UseDocSearchOptions): {
11
+ sources: SearchSource[];
12
+ setQuery: (query: string) => void;
13
+ search: (q: string) => Promise<void>;
14
+ fetchSuggestions: (query: string) => Promise<void>;
15
+ selectNext: () => void;
16
+ selectPrev: () => void;
17
+ setSelectedIndex: (index: number) => void;
18
+ reset: () => void;
19
+ getSelectedItem: () => DocSearchHit | DocSearchSuggestion | null;
20
+ groupedSuggestions: {
21
+ source: SearchSource;
22
+ items: DocSearchSuggestion[];
23
+ }[];
24
+ query: string;
25
+ results: DocSearchHit[];
26
+ suggestions: DocSearchSuggestion[];
27
+ isLoading: boolean;
28
+ error: string | null;
29
+ selectedIndex: number;
30
+ mode: "suggestions" | "results";
31
+ };
32
+ export {};
33
+ //# sourceMappingURL=useDocSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDocSearch.d.ts","sourceRoot":"","sources":["../../../src/docsearch/hooks/useDocSearch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACb,MAAM,UAAU,CAAC;AA0DlB,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,CAAC;CACnG;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB;;sBAuI7C,MAAM;gBAHe,MAAM;8BAlCrB,MAAM;;;8BA+CuB,MAAM;;2BASX,YAAY,GAAG,mBAAmB,GAAG,IAAI;wBA1N7D;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,mBAAmB,EAAE,CAAA;KAAE,EAAE;;;;;;;;EAmP7E"}
@@ -0,0 +1,211 @@
1
+ import { useCallback, useEffect, useReducer, useRef } from 'react';
2
+ const initialState = {
3
+ query: '',
4
+ results: [],
5
+ suggestions: [],
6
+ groupedSuggestions: [],
7
+ isLoading: false,
8
+ error: null,
9
+ selectedIndex: 0,
10
+ mode: 'suggestions',
11
+ };
12
+ function reducer(state, action) {
13
+ switch (action.type) {
14
+ case 'SET_QUERY':
15
+ return { ...state, query: action.payload, selectedIndex: 0 };
16
+ case 'SET_RESULTS':
17
+ return { ...state, results: action.payload, mode: 'results' };
18
+ case 'SET_SUGGESTIONS':
19
+ return { ...state, suggestions: action.payload, mode: 'suggestions' };
20
+ case 'SET_GROUPED_SUGGESTIONS': {
21
+ const flatSuggestions = action.payload.flatMap(group => group.items.map(item => ({ ...item, _source: group.source.id })));
22
+ return { ...state, groupedSuggestions: action.payload, suggestions: flatSuggestions, mode: 'suggestions' };
23
+ }
24
+ case 'SET_LOADING':
25
+ return { ...state, isLoading: action.payload };
26
+ case 'SET_ERROR':
27
+ return { ...state, error: action.payload };
28
+ case 'SET_SELECTED_INDEX':
29
+ return { ...state, selectedIndex: action.payload };
30
+ case 'SELECT_NEXT': {
31
+ const items = state.mode === 'results' ? state.results : state.suggestions;
32
+ const maxIndex = items.length - 1;
33
+ return { ...state, selectedIndex: state.selectedIndex >= maxIndex ? 0 : state.selectedIndex + 1 };
34
+ }
35
+ case 'SELECT_PREV': {
36
+ const items = state.mode === 'results' ? state.results : state.suggestions;
37
+ const maxIndex = items.length - 1;
38
+ return { ...state, selectedIndex: state.selectedIndex <= 0 ? maxIndex : state.selectedIndex - 1 };
39
+ }
40
+ case 'SET_MODE':
41
+ return { ...state, mode: action.payload, selectedIndex: 0 };
42
+ case 'RESET':
43
+ return initialState;
44
+ default:
45
+ return state;
46
+ }
47
+ }
48
+ export function useDocSearch(options) {
49
+ const { apiEndpoint, apiKey, sources, maxResults = 10, debounceMs = 200, processGroupedResults } = options;
50
+ const searchSources = sources || (apiEndpoint ? [{
51
+ id: 'default',
52
+ name: 'Results',
53
+ endpoint: apiEndpoint,
54
+ apiKey,
55
+ maxResults,
56
+ }] : []);
57
+ const [state, dispatch] = useReducer(reducer, initialState);
58
+ const abortControllersRef = useRef(new Map());
59
+ const debounceTimerRef = useRef(null);
60
+ const defaultTransform = (data, sourceId) => {
61
+ const items = data.data?.suggestions || data.data?.results || data.suggestions || data.results || data.hits || [];
62
+ return items.map((item) => ({
63
+ url: item.url || item.route || '',
64
+ title: item.title?.replace?.(/<\/?mark>/g, '') || item.title || '',
65
+ content: item.content?.replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || item.description || '',
66
+ description: item.description || item.content?.substring?.(0, 100) || '',
67
+ category: item.category || item.hierarchy?.lvl0 || '',
68
+ hierarchy: item.hierarchy,
69
+ route: item.route,
70
+ parentTitle: item.parent_title || item.parentTitle,
71
+ _source: sourceId,
72
+ }));
73
+ };
74
+ const transformPublicSearchResults = useCallback((data, sourceId) => {
75
+ const results = data?.data?.results || [];
76
+ return results.map((item) => {
77
+ const doc = item.document || item;
78
+ return {
79
+ url: doc.url || doc.route || '',
80
+ title: (doc.title || doc.name || '').replace?.(/<\/?mark>/g, '') || '',
81
+ content: (doc.content || doc.description || '').replace?.(/<\/?mark>/g, '')?.substring?.(0, 100) || '',
82
+ description: doc.description || doc.content?.substring?.(0, 100) || '',
83
+ category: doc.category || doc.hierarchy?.lvl0 || '',
84
+ hierarchy: doc.hierarchy,
85
+ route: doc.route,
86
+ parentTitle: doc.parent_title || doc.parentTitle,
87
+ _source: sourceId,
88
+ };
89
+ });
90
+ }, []);
91
+ const fetchFromSource = useCallback(async (source, query, signal) => {
92
+ const minLength = source.minQueryLength ?? 1;
93
+ if (query.length < minLength)
94
+ return [];
95
+ try {
96
+ if (source.storeId) {
97
+ const baseUrl = source.endpoint.replace(/\/$/, '');
98
+ const searchUrl = `${baseUrl}/api/v1/search`;
99
+ const headers = {
100
+ 'Content-Type': 'application/json',
101
+ 'x-storeid': source.storeId,
102
+ ...(source.storeSecret && { 'x-storesecret': source.storeSecret }),
103
+ };
104
+ const response = await fetch(searchUrl, {
105
+ method: 'POST',
106
+ headers,
107
+ body: JSON.stringify({ q: query.trim() || '*', per_page: source.maxResults || 8 }),
108
+ signal,
109
+ });
110
+ if (!response.ok)
111
+ return [];
112
+ const data = await response.json();
113
+ if (source.transformResults) {
114
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
115
+ }
116
+ return transformPublicSearchResults(data, source.id);
117
+ }
118
+ const url = new URL(source.endpoint);
119
+ url.searchParams.set('query', query);
120
+ url.searchParams.set('limit', String(source.maxResults || 8));
121
+ const headers = { 'Content-Type': 'application/json' };
122
+ if (source.apiKey)
123
+ headers['X-Docs-API-Key'] = source.apiKey;
124
+ const response = await fetch(url.toString(), { method: 'GET', headers, signal });
125
+ if (!response.ok)
126
+ return [];
127
+ const data = await response.json();
128
+ if (source.transformResults) {
129
+ return source.transformResults(data).map((item) => ({ ...item, _source: source.id }));
130
+ }
131
+ return defaultTransform(data, source.id);
132
+ }
133
+ catch (error) {
134
+ if (error instanceof Error && error.name === 'AbortError')
135
+ throw error;
136
+ return [];
137
+ }
138
+ }, [transformPublicSearchResults]);
139
+ const fetchSuggestions = useCallback(async (query) => {
140
+ if (!query.trim()) {
141
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: [] });
142
+ return;
143
+ }
144
+ abortControllersRef.current.forEach(c => c.abort());
145
+ abortControllersRef.current.clear();
146
+ dispatch({ type: 'SET_LOADING', payload: true });
147
+ dispatch({ type: 'SET_ERROR', payload: null });
148
+ try {
149
+ const results = await Promise.all(searchSources.map(async (source) => {
150
+ const controller = new AbortController();
151
+ abortControllersRef.current.set(source.id, controller);
152
+ let items = await fetchFromSource(source, query, controller.signal);
153
+ if (processGroupedResults) {
154
+ items = processGroupedResults(source.id, items);
155
+ }
156
+ return { source, items };
157
+ }));
158
+ const groupedResults = results.filter(r => r.items.length > 0);
159
+ dispatch({ type: 'SET_GROUPED_SUGGESTIONS', payload: groupedResults });
160
+ }
161
+ catch (error) {
162
+ if (error instanceof Error && error.name === 'AbortError')
163
+ return;
164
+ dispatch({ type: 'SET_ERROR', payload: error instanceof Error ? error.message : 'Search failed' });
165
+ }
166
+ finally {
167
+ dispatch({ type: 'SET_LOADING', payload: false });
168
+ }
169
+ }, [searchSources, fetchFromSource, processGroupedResults]);
170
+ const search = useCallback((q) => fetchSuggestions(q), [fetchSuggestions]);
171
+ const setQuery = useCallback((query) => {
172
+ dispatch({ type: 'SET_QUERY', payload: query });
173
+ if (debounceTimerRef.current)
174
+ clearTimeout(debounceTimerRef.current);
175
+ debounceTimerRef.current = setTimeout(() => fetchSuggestions(query), debounceMs);
176
+ }, [fetchSuggestions, debounceMs]);
177
+ const selectNext = useCallback(() => dispatch({ type: 'SELECT_NEXT' }), []);
178
+ const selectPrev = useCallback(() => dispatch({ type: 'SELECT_PREV' }), []);
179
+ const setSelectedIndex = useCallback((index) => dispatch({ type: 'SET_SELECTED_INDEX', payload: index }), []);
180
+ const reset = useCallback(() => {
181
+ abortControllersRef.current.forEach(c => c.abort());
182
+ abortControllersRef.current.clear();
183
+ if (debounceTimerRef.current)
184
+ clearTimeout(debounceTimerRef.current);
185
+ dispatch({ type: 'RESET' });
186
+ }, []);
187
+ const getSelectedItem = useCallback(() => {
188
+ const items = state.mode === 'results' ? state.results : state.suggestions;
189
+ return items[state.selectedIndex] || null;
190
+ }, [state.mode, state.results, state.suggestions, state.selectedIndex]);
191
+ useEffect(() => {
192
+ return () => {
193
+ abortControllersRef.current.forEach(c => c.abort());
194
+ abortControllersRef.current.clear();
195
+ if (debounceTimerRef.current)
196
+ clearTimeout(debounceTimerRef.current);
197
+ };
198
+ }, []);
199
+ return {
200
+ ...state,
201
+ sources: searchSources,
202
+ setQuery,
203
+ search,
204
+ fetchSuggestions,
205
+ selectNext,
206
+ selectPrev,
207
+ setSelectedIndex,
208
+ reset,
209
+ getSelectedItem,
210
+ };
211
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ interface UseKeyboardOptions {
3
+ isOpen: boolean;
4
+ onOpen: () => void;
5
+ onClose: () => void;
6
+ onSelectNext: () => void;
7
+ onSelectPrev: () => void;
8
+ onEnter: () => void;
9
+ disableShortcut?: boolean;
10
+ shortcutKey?: string;
11
+ }
12
+ export declare function useKeyboard(options: UseKeyboardOptions): {
13
+ handleModalKeyDown: (event: KeyboardEvent | React.KeyboardEvent) => void;
14
+ };
15
+ export declare function getShortcutText(key?: string): string;
16
+ export {};
17
+ //# sourceMappingURL=useKeyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useKeyboard.d.ts","sourceRoot":"","sources":["../../../src/docsearch/hooks/useKeyboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAiC,MAAM,OAAO,CAAC;AAEtD,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB;gCAyC3C,aAAa,GAAG,KAAK,CAAC,aAAa;EAyC9C;AAED,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAY,GAAG,MAAM,CAMzD"}
@@ -0,0 +1,71 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ export function useKeyboard(options) {
3
+ const { isOpen, onOpen, onClose, onSelectNext, onSelectPrev, onEnter, disableShortcut = false, shortcutKey = 'k', } = options;
4
+ const handleGlobalKeyDown = useCallback((event) => {
5
+ if (!disableShortcut && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey) {
6
+ event.preventDefault();
7
+ if (isOpen) {
8
+ onClose();
9
+ }
10
+ else {
11
+ onOpen();
12
+ }
13
+ return;
14
+ }
15
+ if (!disableShortcut && event.key === '/' && !isOpen) {
16
+ const target = event.target;
17
+ const isInput = target.tagName === 'INPUT' ||
18
+ target.tagName === 'TEXTAREA' ||
19
+ target.isContentEditable;
20
+ if (!isInput) {
21
+ event.preventDefault();
22
+ onOpen();
23
+ }
24
+ }
25
+ }, [isOpen, onOpen, onClose, disableShortcut, shortcutKey]);
26
+ const handleModalKeyDown = useCallback((event) => {
27
+ switch (event.key) {
28
+ case 'Escape':
29
+ event.preventDefault();
30
+ onClose();
31
+ break;
32
+ case 'ArrowDown':
33
+ event.preventDefault();
34
+ onSelectNext();
35
+ break;
36
+ case 'ArrowUp':
37
+ event.preventDefault();
38
+ onSelectPrev();
39
+ break;
40
+ case 'Enter':
41
+ event.preventDefault();
42
+ onEnter();
43
+ break;
44
+ case 'Tab':
45
+ if (event.shiftKey) {
46
+ onSelectPrev();
47
+ }
48
+ else {
49
+ onSelectNext();
50
+ }
51
+ event.preventDefault();
52
+ break;
53
+ }
54
+ }, [onClose, onSelectNext, onSelectPrev, onEnter]);
55
+ useEffect(() => {
56
+ document.addEventListener('keydown', handleGlobalKeyDown);
57
+ return () => {
58
+ document.removeEventListener('keydown', handleGlobalKeyDown);
59
+ };
60
+ }, [handleGlobalKeyDown]);
61
+ return {
62
+ handleModalKeyDown,
63
+ };
64
+ }
65
+ export function getShortcutText(key = 'K') {
66
+ if (typeof navigator === 'undefined') {
67
+ return `⌘${key}`;
68
+ }
69
+ const isMac = navigator.platform.toLowerCase().includes('mac');
70
+ return isMac ? `⌘${key}` : `Ctrl+${key}`;
71
+ }
@@ -0,0 +1,27 @@
1
+ import type { DocSearchSuggestion } from '../types';
2
+ export interface UseSeekoraSearchOptions {
3
+ storeId: string;
4
+ storeSecret?: string;
5
+ apiEndpoint?: string;
6
+ maxResults?: number;
7
+ debounceMs?: number;
8
+ analyticsTags?: string[];
9
+ groupField?: string;
10
+ groupSize?: number;
11
+ }
12
+ export interface UseSeekoraSearchResult {
13
+ query: string;
14
+ suggestions: DocSearchSuggestion[];
15
+ isLoading: boolean;
16
+ error: string | null;
17
+ selectedIndex: number;
18
+ setQuery: (query: string) => void;
19
+ selectNext: () => void;
20
+ selectPrev: () => void;
21
+ setSelectedIndex: (index: number) => void;
22
+ reset: () => void;
23
+ getSelectedItem: () => DocSearchSuggestion | null;
24
+ trackDocClick: (hit: DocSearchSuggestion, position: number) => void | Promise<void>;
25
+ }
26
+ export declare function useSeekoraSearch(options: UseSeekoraSearchOptions): UseSeekoraSearchResult;
27
+ //# sourceMappingURL=useSeekoraSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSeekoraSearch.d.ts","sourceRoot":"","sources":["../../../src/docsearch/hooks/useSeekoraSearch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,eAAe,EAAE,MAAM,mBAAmB,GAAG,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,GAAG,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AA0DD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,sBAAsB,CA2KzF"}