@netlify/spark-ui 1.12.0-alpha.0 → 1.12.0-alpha.2
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/assets/AskNetlify.css +1 -1
- package/dist/assets/SearchLink.css +1 -1
- package/dist/assets/SearchModal.css +1 -1
- package/dist/chunks/{ChatInterface.EQAPs01R.js → AskNetlify.lrswYyKn.js} +119 -110
- package/dist/components/preact/AskNetlify/AskNetlify.d.ts +3 -3
- package/dist/components/preact/AskNetlify/AskNetlify.js +8 -34
- package/dist/components/preact/AskNetlify/components/ChatMessages.d.ts +2 -1
- package/dist/components/preact/AskNetlify/components/ChatMessages.js +25 -21
- package/dist/components/preact/AskNetlify/components/SourcesList.js +17 -13
- package/dist/components/preact/AskNetlify/hooks/useAskAnalytics.d.ts +23 -0
- package/dist/components/preact/AskNetlify/hooks/useAskAnalytics.js +22 -0
- package/dist/components/preact/Icon/index.js +6 -6
- package/dist/components/preact/SearchModal/components/SearchInput.js +18 -8
- package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.d.ts +1 -1
- package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +23 -22
- package/dist/components/preact/SearchModal/components/SearchLinkThemeToggle.d.ts +1 -0
- package/dist/components/preact/SearchModal/components/SearchLinkThemeToggle.js +26 -0
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +89 -84
- package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
- package/dist/components/preact/SearchModal/components/SearchWindow.js +55 -48
- package/dist/components/preact/SearchModal/hooks/index.d.ts +1 -1
- package/dist/components/preact/SearchModal/hooks/index.js +8 -8
- package/dist/components/preact/SearchModal/hooks/useKeyboardNav.d.ts +1 -1
- package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +11 -12
- package/dist/components/preact/SearchModal/hooks/useSearch.js +19 -19
- package/dist/components/preact/SearchModal/hooks/{useSearchLogger.d.ts → useSearchAnalytics.d.ts} +2 -2
- package/dist/components/preact/SearchModal/hooks/{useSearchLogger.js → useSearchAnalytics.js} +5 -5
- package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.d.ts +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +8 -9
- package/dist/components/preact/SearchModal/hooks/useSearchParams.d.ts +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchParams.js +14 -7
- package/dist/components/preact/SearchModal/hooks/useSearchToggles.d.ts +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +7 -8
- package/dist/components/preact/SearchModal/types/search.types.d.ts +11 -4
- package/dist/components/preact/index.js +3 -3
- package/package.json +1 -1
- package/packages/components/preact/AskNetlify/AskNetlify.css +41 -1
- package/packages/components/preact/AskNetlify/AskNetlify.tsx +98 -34
- package/packages/components/preact/AskNetlify/components/ChatMessages.tsx +9 -7
- package/packages/components/preact/AskNetlify/components/SourcesList.tsx +24 -16
- package/packages/components/preact/AskNetlify/hooks/useAskAnalytics.tsx +50 -0
- package/packages/components/preact/Icon/index.tsx +2 -2
- package/packages/components/preact/SearchModal/components/SearchInput.tsx +10 -0
- package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.css +5 -0
- package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.tsx +6 -4
- package/packages/components/preact/SearchModal/components/SearchLinkThemeToggle.tsx +19 -0
- package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.css +33 -3
- package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +73 -89
- package/packages/components/preact/SearchModal/components/SearchWindow.tsx +64 -46
- package/packages/components/preact/SearchModal/hooks/index.ts +1 -1
- package/packages/components/preact/SearchModal/hooks/useKeyboardNav.ts +1 -3
- package/packages/components/preact/SearchModal/hooks/useSearch.ts +20 -16
- package/packages/components/preact/SearchModal/hooks/{useSearchLogger.ts → useSearchAnalytics.ts} +9 -10
- package/packages/components/preact/SearchModal/hooks/useSearchHotkeys.ts +2 -4
- package/packages/components/preact/SearchModal/hooks/useSearchParams.ts +24 -2
- package/packages/components/preact/SearchModal/hooks/useSearchToggles.ts +1 -3
- package/packages/components/preact/SearchModal/types/search.types.tsx +7 -4
- package/packages/styles/loaders.css +18 -0
- package/packages/styles/tokens.css +4 -0
- package/dist/components/preact/AskNetlify/components/ChatInterface.d.ts +0 -7
- package/dist/components/preact/AskNetlify/components/ChatInterface.js +0 -10
- package/packages/components/preact/AskNetlify/components/ChatInterface.tsx +0 -94
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ComponentChildren } from 'preact';
|
|
2
2
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
|
3
3
|
|
|
4
|
+
import { KapaProvider } from '@kapaai/react-sdk';
|
|
5
|
+
|
|
4
6
|
import Modal from '../../../../preact/Modal/Modal';
|
|
5
7
|
import KeyboardKey from '../../../../preact/KeyboardKey/KeyboardKey';
|
|
6
8
|
import SearchWindow from '../../components/SearchWindow';
|
|
@@ -13,10 +15,11 @@ import {
|
|
|
13
15
|
useSearchHotkeys,
|
|
14
16
|
useSearchParams,
|
|
15
17
|
useSearchToggles,
|
|
16
|
-
|
|
18
|
+
useSearchAnalytics,
|
|
17
19
|
} from '../../hooks';
|
|
18
20
|
|
|
19
21
|
import AskNetlify from '../../../../preact/AskNetlify/AskNetlify';
|
|
22
|
+
import { useAskAnalytics } from '../../../../preact/AskNetlify/hooks/useAskAnalytics';
|
|
20
23
|
|
|
21
24
|
import './SearchModal.css';
|
|
22
25
|
|
|
@@ -30,31 +33,9 @@ interface Props {
|
|
|
30
33
|
function SearchModal(
|
|
31
34
|
props: Props = { className: '', children: undefined, debug: false, kapaIntegrationId: undefined }
|
|
32
35
|
) {
|
|
33
|
-
const [isPagefindLoaded, setIsPagefindLoaded] = useState(false);
|
|
34
|
-
const [modalState, setModalState] = useState({ current: 'search' });
|
|
35
|
-
|
|
36
|
-
// Poll for the existence of window.pagefind.
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
let intervalId: number;
|
|
39
|
-
|
|
40
|
-
const checkForPagefind = () => {
|
|
41
|
-
if (window.pagefind) {
|
|
42
|
-
setIsPagefindLoaded(true);
|
|
43
|
-
if (intervalId) clearInterval(intervalId);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Check immediately and then start polling every 100ms.
|
|
48
|
-
checkForPagefind();
|
|
49
|
-
intervalId = setInterval(checkForPagefind, 100);
|
|
50
|
-
|
|
51
|
-
// Cleanup the interval when the component unmounts.
|
|
52
|
-
return () => clearInterval(intervalId);
|
|
53
|
-
}, []);
|
|
54
|
-
|
|
55
36
|
const { children, className, debug, ...rest } = props;
|
|
56
|
-
const { state, actions } = useSearch();
|
|
57
37
|
|
|
38
|
+
const [modalState, setModalState] = useState({ current: 'search' });
|
|
58
39
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
59
40
|
const openSearch = () => setSearchOpen(true);
|
|
60
41
|
const closeSearch = () => setSearchOpen(false);
|
|
@@ -62,11 +43,13 @@ function SearchModal(
|
|
|
62
43
|
const modalRef = useRef<HTMLDialogElement>(null);
|
|
63
44
|
const containerRef = useClickOutside<HTMLDivElement>(closeSearch);
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
useSearchToggles(
|
|
67
|
-
useSearchParams(
|
|
68
|
-
useSearchHotkeys(
|
|
69
|
-
useKeyboardNav(
|
|
46
|
+
const { state, actions } = useSearch();
|
|
47
|
+
useSearchToggles(openSearch, setModalState);
|
|
48
|
+
useSearchParams(actions.setSearchTerm, openSearch);
|
|
49
|
+
useSearchHotkeys(searchOpen, openSearch, closeSearch);
|
|
50
|
+
useKeyboardNav(modalRef, searchOpen, closeSearch);
|
|
51
|
+
useSearchAnalytics(state, debug);
|
|
52
|
+
const { handleNewQuery, handleAnswer } = useAskAnalytics();
|
|
70
53
|
|
|
71
54
|
useEffect(() => {
|
|
72
55
|
if (!modalRef.current) return;
|
|
@@ -75,71 +58,72 @@ function SearchModal(
|
|
|
75
58
|
|
|
76
59
|
useEffect(() => {
|
|
77
60
|
const input = modalRef.current?.querySelector('input');
|
|
78
|
-
|
|
79
61
|
if (input) input.focus();
|
|
80
|
-
|
|
81
|
-
if (modalState.current === 'ask') {
|
|
82
|
-
setTimeout(() => {
|
|
83
|
-
modalRef.current.classList.add('n-search-modal-ask');
|
|
84
|
-
}, 150);
|
|
85
|
-
} else if (modalState.current === 'search') {
|
|
86
|
-
setTimeout(() => {
|
|
87
|
-
modalRef.current.classList.remove('n-search-modal-ask');
|
|
88
|
-
}, 150);
|
|
89
|
-
}
|
|
90
62
|
}, [modalState.current]);
|
|
91
63
|
|
|
92
64
|
return (
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
65
|
+
<KapaProvider
|
|
66
|
+
integrationId={props.kapaIntegrationId || ''}
|
|
67
|
+
callbacks={{
|
|
68
|
+
askAI: {
|
|
69
|
+
onQuerySubmit: handleNewQuery,
|
|
70
|
+
onAnswerGenerationCompleted: handleAnswer,
|
|
71
|
+
},
|
|
72
|
+
}}
|
|
98
73
|
>
|
|
99
|
-
<
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
74
|
+
<Modal
|
|
75
|
+
ref={modalRef}
|
|
76
|
+
className={`n-search-modal ${className ? className : ''}`}
|
|
77
|
+
data-state={modalState.current}
|
|
78
|
+
{...rest}
|
|
79
|
+
>
|
|
80
|
+
<div ref={containerRef} className="n-search-container">
|
|
81
|
+
{modalState.current === 'ask' && (
|
|
82
|
+
<AskNetlify setModalState={setModalState} initialQuery={state.searchTerm} />
|
|
83
|
+
)}
|
|
84
|
+
{modalState.current === 'search' && (
|
|
85
|
+
<>
|
|
86
|
+
<SearchInput setSearchTerm={actions.setSearchTerm} initialValue={state.searchTerm} />
|
|
87
|
+
|
|
88
|
+
<SearchWindow
|
|
89
|
+
setModalState={setModalState}
|
|
90
|
+
searchLoading={state.isLoading}
|
|
91
|
+
searchTerm={state.searchTerm}
|
|
92
|
+
results={state.results}
|
|
93
|
+
>
|
|
94
|
+
<>
|
|
95
|
+
{props.kapaIntegrationId && (
|
|
96
|
+
<SearchLink
|
|
97
|
+
as="button"
|
|
98
|
+
icon="askNetlify"
|
|
99
|
+
label="Ask Netlify"
|
|
100
|
+
excerpt="Get an instant AI-generated answer"
|
|
101
|
+
onClick={() => {
|
|
102
|
+
setModalState({ current: 'ask' });
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
{children}
|
|
107
|
+
</>
|
|
108
|
+
</SearchWindow>
|
|
109
|
+
|
|
110
|
+
<div className="n-search-footer">
|
|
111
|
+
<p className="n-search-footer-tip">
|
|
112
|
+
<KeyboardKey keyNames={['Esc']} />
|
|
113
|
+
to close
|
|
114
|
+
</p>
|
|
115
|
+
<p className="n-search-footer-tip">
|
|
116
|
+
<KeyboardKey keyNames={['Tab']} />
|
|
117
|
+
<span>or</span>
|
|
118
|
+
<KeyboardKey keyNames={['↑', '↓']} />
|
|
119
|
+
to navigate
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</Modal>
|
|
126
|
+
</KapaProvider>
|
|
143
127
|
);
|
|
144
128
|
}
|
|
145
129
|
|
|
@@ -1,78 +1,96 @@
|
|
|
1
1
|
import type { FunctionComponent } from 'preact';
|
|
2
2
|
import type { SearchResultsProps, SearchWindowProps } from '../types/search.types';
|
|
3
|
-
import { useMemo } from 'preact/hooks';
|
|
4
3
|
import SearchHeading from './SearchHeading/SearchHeading';
|
|
5
4
|
import SearchLink from './SearchLink/SearchLink';
|
|
5
|
+
import SearchLinkThemeToggle from './SearchLinkThemeToggle';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const AskNetlifyLink = ({
|
|
8
|
+
searchTerm,
|
|
9
|
+
setModalState,
|
|
10
|
+
}: {
|
|
11
|
+
searchTerm: string;
|
|
12
|
+
setModalState: (state: { current: string }) => void;
|
|
13
|
+
}) => (
|
|
14
|
+
<SearchLink
|
|
15
|
+
as="button"
|
|
16
|
+
icon="askNetlify"
|
|
17
|
+
excerpt="Get an instant AI-generated answer"
|
|
18
|
+
onClick={() => setModalState({ current: 'ask' })}
|
|
19
|
+
>
|
|
20
|
+
Ask Netlify: <span className="n-search-term">{searchTerm}</span>
|
|
21
|
+
</SearchLink>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const NoResults = ({ searchTerm }: { searchTerm: string }) => (
|
|
25
|
+
<div className="n-search-feedback">
|
|
26
|
+
<p className="n-results-info">
|
|
27
|
+
0 results for <span className="n-search-term">{searchTerm}</span>
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
8
31
|
|
|
9
|
-
/* --- Search Results Window (for active searches) --- */
|
|
10
32
|
const SearchResults: FunctionComponent<SearchResultsProps> = ({
|
|
33
|
+
setModalState,
|
|
11
34
|
searchLoading,
|
|
12
35
|
searchTerm,
|
|
13
36
|
results,
|
|
14
37
|
}) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<SearchHeading>
|
|
18
|
-
Searching for <span className="n-search-term">{searchTerm}</span> ...
|
|
19
|
-
</SearchHeading>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!results || results.length === 0) {
|
|
24
|
-
return (
|
|
25
|
-
<SearchHeading>
|
|
26
|
-
0 results for <span className="n-search-term">{searchTerm}</span>
|
|
27
|
-
</SearchHeading>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const renderedResults = useMemo(() => {
|
|
32
|
-
return results.map((result, i) => (
|
|
33
|
-
<li key={i} className="n-results-list-item">
|
|
34
|
-
<SearchLink {...result} />
|
|
35
|
-
</li>
|
|
36
|
-
));
|
|
37
|
-
}, [results]);
|
|
38
|
+
const noResults =
|
|
39
|
+
searchTerm === 'no results' || (!searchLoading && (!results || results.length === 0));
|
|
38
40
|
|
|
39
41
|
return (
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
42
|
+
<>
|
|
43
|
+
<AskNetlifyLink searchTerm={searchTerm} setModalState={setModalState} />
|
|
44
|
+
{noResults ? (
|
|
45
|
+
<NoResults searchTerm={searchTerm} />
|
|
46
|
+
) : (
|
|
47
|
+
<ul className="n-results-list" role="list">
|
|
48
|
+
{results?.map((result, i) => (
|
|
49
|
+
<li key={i} className="n-results-list-item">
|
|
50
|
+
<SearchLink {...result} />
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
43
56
|
);
|
|
44
57
|
};
|
|
45
58
|
|
|
46
|
-
/* --- Main Search Results Component --- */
|
|
47
59
|
export default function SearchWindow({
|
|
60
|
+
setModalState,
|
|
48
61
|
searchLoading,
|
|
49
62
|
searchTerm,
|
|
50
63
|
results,
|
|
51
64
|
children: DefaultWindow,
|
|
52
65
|
}: SearchWindowProps) {
|
|
66
|
+
const hasQuery = searchTerm?.length > 1;
|
|
67
|
+
|
|
53
68
|
return (
|
|
54
69
|
<div className="n-search-window">
|
|
55
70
|
<div className="n-search-links">
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
{hasQuery ? (
|
|
72
|
+
searchLoading ? (
|
|
73
|
+
<>
|
|
74
|
+
<AskNetlifyLink searchTerm={searchTerm} setModalState={setModalState} />
|
|
75
|
+
<div className="n-search-feedback">
|
|
76
|
+
<div className="n-loader-dots-spinner" />
|
|
77
|
+
</div>
|
|
78
|
+
</>
|
|
79
|
+
) : (
|
|
80
|
+
<SearchResults
|
|
81
|
+
setModalState={setModalState}
|
|
82
|
+
searchLoading={searchLoading}
|
|
83
|
+
searchTerm={searchTerm}
|
|
84
|
+
results={results}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
) : (
|
|
59
88
|
<>
|
|
60
89
|
{DefaultWindow}
|
|
61
|
-
|
|
62
90
|
<SearchHeading>Actions</SearchHeading>
|
|
63
|
-
<
|
|
64
|
-
as="button"
|
|
65
|
-
icon="lightbulb"
|
|
66
|
-
label="Toggle theme"
|
|
67
|
-
onClick={() => {
|
|
68
|
-
const isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
69
|
-
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
70
|
-
const theme = isDarkTheme ? 'dark' : currentTheme ? currentTheme : 'light';
|
|
71
|
-
theme && handleThemeChange(ThemeMap[theme as Theme]);
|
|
72
|
-
}}
|
|
73
|
-
/>
|
|
91
|
+
<SearchLinkThemeToggle />
|
|
74
92
|
</>
|
|
75
|
-
)
|
|
93
|
+
)}
|
|
76
94
|
</div>
|
|
77
95
|
</div>
|
|
78
96
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useSearch } from '../hooks/useSearch';
|
|
2
|
-
export {
|
|
2
|
+
export { useSearchAnalytics } from './useSearchAnalytics';
|
|
3
3
|
export { useKeyboardNav } from '../hooks/useKeyboardNav';
|
|
4
4
|
export { useSearchHotkeys } from '../hooks/useSearchHotkeys';
|
|
5
5
|
export { useSearchParams } from '../hooks/useSearchParams';
|
|
@@ -15,13 +15,11 @@ import { useEffect } from 'preact/hooks';
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
export function useKeyboardNav(
|
|
18
|
-
isPagefindLoaded: boolean,
|
|
19
18
|
containerRef: RefObject<HTMLElement>,
|
|
20
19
|
isOpen: boolean,
|
|
21
20
|
onClose: () => void
|
|
22
21
|
) {
|
|
23
22
|
useEffect(() => {
|
|
24
|
-
if (!isPagefindLoaded) return;
|
|
25
23
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
26
24
|
if (!isOpen) return;
|
|
27
25
|
|
|
@@ -48,5 +46,5 @@ export function useKeyboardNav(
|
|
|
48
46
|
|
|
49
47
|
document.addEventListener('keydown', handleKeydown);
|
|
50
48
|
return () => document.removeEventListener('keydown', handleKeydown);
|
|
51
|
-
}, [
|
|
49
|
+
}, [containerRef, isOpen, onClose]);
|
|
52
50
|
}
|
|
@@ -21,14 +21,14 @@ export function useSearch() {
|
|
|
21
21
|
const cacheRef = useRef(new Map());
|
|
22
22
|
|
|
23
23
|
const { reset, run } = useTimeout((query: string) => {
|
|
24
|
-
|
|
24
|
+
if (query && query.length >= 2) submitSearch(query);
|
|
25
25
|
}, 150);
|
|
26
26
|
|
|
27
27
|
const submitSearch = async (query: string) => {
|
|
28
28
|
if (!window?.pagefind) return;
|
|
29
29
|
|
|
30
30
|
setSearchState(prev => ({ ...prev, isLoading: true }));
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
if (cacheRef.current.has(query)) {
|
|
33
33
|
setSearchState(prev => ({
|
|
34
34
|
...prev,
|
|
@@ -38,20 +38,24 @@ export function useSearch() {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
const search = await window.pagefind.debouncedSearch(query);
|
|
41
|
-
const results =
|
|
42
|
-
search?.results
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const results =
|
|
42
|
+
search?.results &&
|
|
43
|
+
(await Promise.all(
|
|
44
|
+
search?.results?.slice(0, 15).map(async (result: any) => {
|
|
45
|
+
const data = await result.data();
|
|
46
|
+
const removeAnchorLink = (text: string) => text.replace('#', '');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
icon: 'pageWithLines',
|
|
50
|
+
resource: data.meta?.resource,
|
|
51
|
+
label: data.meta?.title,
|
|
52
|
+
url: data.url,
|
|
53
|
+
excerpt: removeAnchorLink(data.excerpt),
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
));
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
icon: 'pageWithLines',
|
|
48
|
-
resource: data.meta?.resource,
|
|
49
|
-
label: data.meta?.title,
|
|
50
|
-
url: data.url,
|
|
51
|
-
excerpt: removeAnchorLink(data.excerpt),
|
|
52
|
-
};
|
|
53
|
-
})
|
|
54
|
-
);
|
|
58
|
+
cacheRef.current.set(query, results);
|
|
55
59
|
|
|
56
60
|
setSearchState(prev => ({
|
|
57
61
|
...prev,
|
|
@@ -61,7 +65,7 @@ export function useSearch() {
|
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
const handleSearchChange = (query: string) => {
|
|
64
|
-
setSearchState(prev => ({ ...prev, searchTerm: query }));
|
|
68
|
+
setSearchState(prev => ({ ...prev, results: [], searchTerm: query }));
|
|
65
69
|
reset();
|
|
66
70
|
run(query);
|
|
67
71
|
};
|
package/packages/components/preact/SearchModal/hooks/{useSearchLogger.ts → useSearchAnalytics.ts}
RENAMED
|
@@ -16,29 +16,29 @@ import Analytics from '../../../../analytics/analytics';
|
|
|
16
16
|
* - `state.searchTerm`: The current search query string.
|
|
17
17
|
* - `state.results`: The search results array. If it's empty, the hook will log a no-results event.
|
|
18
18
|
* @param {boolean} [debug=false] - Flag to enable debug mode. If true, additional debug information will be sent with the event payloads.
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* @returns {Object} - An object with the following function:
|
|
21
21
|
* - `handleClick`: A callback function that should be attached to search result click events. It tracks `search:result_selected` events with the clicked result's title and URL.
|
|
22
22
|
*
|
|
23
23
|
* @example
|
|
24
|
-
* const { handleClick } =
|
|
24
|
+
* const { handleClick } = useSearchAnalytics(searchState);
|
|
25
25
|
*
|
|
26
26
|
* // In your component, attach handleClick to the search result links
|
|
27
27
|
* <a href="result-url" onClick={handleClick}>Search Result</a>
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
export function
|
|
30
|
+
export function useSearchAnalytics(state?: SearchState, debug?: boolean) {
|
|
31
31
|
const ref = useRef<Analytics>(new Analytics());
|
|
32
32
|
const analytics = ref.current;
|
|
33
33
|
|
|
34
|
-
const enableDebugger = (o: SearchGTMObjectProps) => o.debug_mode = true
|
|
34
|
+
const enableDebugger = (o: SearchGTMObjectProps) => (o.debug_mode = true);
|
|
35
35
|
|
|
36
|
-
// Log search query
|
|
36
|
+
// Log search query that is minimum of 3 characters long
|
|
37
37
|
useEffect(() => {
|
|
38
|
-
if (!state?.searchTerm) return;
|
|
38
|
+
if (!state?.searchTerm || state?.searchTerm.length < 3) return;
|
|
39
39
|
const payload = { query: state?.searchTerm };
|
|
40
40
|
|
|
41
|
-
if (debug) enableDebugger(payload)
|
|
41
|
+
if (debug) enableDebugger(payload);
|
|
42
42
|
|
|
43
43
|
analytics.track('search:new_query', payload);
|
|
44
44
|
}, [state?.searchTerm]);
|
|
@@ -49,7 +49,7 @@ export function useSearchLogger(state?: SearchState, debug?: boolean) {
|
|
|
49
49
|
|
|
50
50
|
const payload = { query: state?.searchTerm };
|
|
51
51
|
|
|
52
|
-
if (debug) enableDebugger(payload)
|
|
52
|
+
if (debug) enableDebugger(payload);
|
|
53
53
|
|
|
54
54
|
if (state?.searchTerm && state.results.length === 0) {
|
|
55
55
|
analytics.track('search:no_results', payload);
|
|
@@ -68,8 +68,7 @@ export function useSearchLogger(state?: SearchState, debug?: boolean) {
|
|
|
68
68
|
resultUrl: url,
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
if (debug) enableDebugger(payload)
|
|
72
|
-
|
|
71
|
+
if (debug) enableDebugger(payload);
|
|
73
72
|
|
|
74
73
|
analytics.track('search:result_selected', payload);
|
|
75
74
|
}, []);
|
|
@@ -13,10 +13,8 @@ import { useEffect } from 'preact/hooks';
|
|
|
13
13
|
* @param onClose - Callback to close the search interface.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
export function useSearchHotkeys(
|
|
16
|
+
export function useSearchHotkeys(isOpen: boolean, onOpen: () => void, onClose: () => void) {
|
|
17
17
|
useEffect(() => {
|
|
18
|
-
if (!isPagefindLoaded) return;
|
|
19
|
-
|
|
20
18
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
21
19
|
if (e.metaKey && e.key === 'k') {
|
|
22
20
|
e.preventDefault();
|
|
@@ -27,5 +25,5 @@ export function useSearchHotkeys(isPagefindLoaded: boolean, isOpen: boolean, onO
|
|
|
27
25
|
|
|
28
26
|
document.addEventListener('keydown', handleKeydown);
|
|
29
27
|
return () => document.removeEventListener('keydown', handleKeydown);
|
|
30
|
-
}, [
|
|
28
|
+
}, [isOpen, onOpen, onClose]);
|
|
31
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'preact/hooks';
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Custom hook that checks the current URL for a `search` query parameter
|
|
@@ -11,8 +11,30 @@ import { useEffect } from 'preact/hooks';
|
|
|
11
11
|
* @param onOpen - Function to trigger opening of the search interface (e.g., modal or drawer).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export function useSearchParams(
|
|
14
|
+
export function useSearchParams(setSearchTerm: (term: string) => void, onOpen: () => void) {
|
|
15
|
+
const [isPagefindLoaded, setIsPagefindLoaded] = useState(false);
|
|
16
|
+
|
|
17
|
+
// Poll for the existence of window.pagefind.
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let intervalId: number;
|
|
20
|
+
|
|
21
|
+
const checkForPagefind = () => {
|
|
22
|
+
if (window.pagefind) {
|
|
23
|
+
setIsPagefindLoaded(true);
|
|
24
|
+
if (intervalId) clearInterval(intervalId);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Check immediately and then start polling every 100ms.
|
|
29
|
+
checkForPagefind();
|
|
30
|
+
intervalId = setInterval(checkForPagefind, 100);
|
|
31
|
+
|
|
32
|
+
// Cleanup the interval when the component unmounts.
|
|
33
|
+
return () => clearInterval(intervalId);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
15
36
|
useEffect(() => {
|
|
37
|
+
// Cleanup the interval when the component unmounts.
|
|
16
38
|
if (typeof location === 'undefined' || isPagefindLoaded) return;
|
|
17
39
|
const query = location?.search && new URLSearchParams(location.search).get('search');
|
|
18
40
|
if (query) {
|
|
@@ -13,12 +13,10 @@ import { useEffect } from 'preact/hooks';
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export function useSearchToggles(
|
|
16
|
-
isPagefindLoaded: boolean,
|
|
17
16
|
onOpen: () => void,
|
|
18
17
|
setState: (state: { current: string }) => void
|
|
19
18
|
) {
|
|
20
19
|
useEffect(() => {
|
|
21
|
-
if (!isPagefindLoaded) return;
|
|
22
20
|
const toggles = document.querySelectorAll('[data-ntl-search-toggle]');
|
|
23
21
|
|
|
24
22
|
if (!toggles.length) return;
|
|
@@ -35,5 +33,5 @@ export function useSearchToggles(
|
|
|
35
33
|
return () => {
|
|
36
34
|
toggles.forEach(t => t.removeEventListener('click', onOpen));
|
|
37
35
|
};
|
|
38
|
-
}, [
|
|
36
|
+
}, [onOpen]);
|
|
39
37
|
}
|
|
@@ -3,24 +3,27 @@ import type { IconName } from '../../Icon';
|
|
|
3
3
|
|
|
4
4
|
export interface SearchLinkProps {
|
|
5
5
|
as?: any;
|
|
6
|
-
icon
|
|
7
|
-
label
|
|
6
|
+
icon?: IconName;
|
|
7
|
+
label?: string;
|
|
8
8
|
resource?: 'blog' | 'changelog' | 'docs' | 'guide';
|
|
9
9
|
url?: string;
|
|
10
10
|
excerpt?: string;
|
|
11
|
+
children?: ComponentChildren;
|
|
11
12
|
onClick?: () => void;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface SearchWindowProps {
|
|
16
|
+
setModalState: (e: { current: string }) => void;
|
|
15
17
|
searchLoading: boolean;
|
|
16
|
-
searchTerm
|
|
18
|
+
searchTerm: string;
|
|
17
19
|
results?: SearchLinkProps[];
|
|
18
20
|
children: ComponentChildren;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface SearchResultsProps {
|
|
24
|
+
setModalState: (e: { current: string }) => void;
|
|
22
25
|
searchLoading: boolean;
|
|
23
|
-
searchTerm
|
|
26
|
+
searchTerm: string;
|
|
24
27
|
results?: SearchLinkProps[];
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.n-loader-dots-spinner {
|
|
2
|
+
width: var(--n-loader-dot-w, 50px);
|
|
3
|
+
aspect-ratio: 1;
|
|
4
|
+
--_c: no-repeat
|
|
5
|
+
radial-gradient(farthest-side, var(--n-loader-dot-color, var(--teal-600)) 92%, #0000);
|
|
6
|
+
background:
|
|
7
|
+
var(--_c) top,
|
|
8
|
+
var(--_c) left,
|
|
9
|
+
var(--_c) right,
|
|
10
|
+
var(--_c) bottom;
|
|
11
|
+
background-size: 12px 12px;
|
|
12
|
+
animation: l7 1s infinite;
|
|
13
|
+
}
|
|
14
|
+
@keyframes l7 {
|
|
15
|
+
to {
|
|
16
|
+
transform: rotate(0.5turn);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
--search-icon-color: var(--neutral-400);
|
|
167
167
|
--search-input-bg-color: var(--neutral-100);
|
|
168
168
|
--search-loading-color: var(--teal-700);
|
|
169
|
+
--search-source-list-color: var(--neutral-100);
|
|
169
170
|
--search-submit-btn-bg-color: var(--teal-200);
|
|
170
171
|
--search-user-message-color: var(--teal-000);
|
|
171
172
|
--shadow-deep: 0 16px 24px 0 color(srgb 0 0 0 / 0.07), 0 6px 30px 0 color(srgb 0 0 0 / 0.06), 0 8px 10px 0 color(srgb 0 0 0 / 0.1);
|
|
@@ -274,6 +275,7 @@
|
|
|
274
275
|
--search-icon-color: var(--neutral-300);
|
|
275
276
|
--search-input-bg-color: var(--neutral-600);
|
|
276
277
|
--search-loading-color: var(--teal-200);
|
|
278
|
+
--search-source-list-color: var(--neutral-700);
|
|
277
279
|
--search-submit-btn-bg-color: var(--teal-200);
|
|
278
280
|
--search-user-message-color: var(--teal-800);
|
|
279
281
|
--tabbed-content-bg-color: color(srgb 0.047058823529411764 0.16470588235294117 0.16470588235294117 / 0.2);
|
|
@@ -358,6 +360,7 @@
|
|
|
358
360
|
--search-icon-color: var(--neutral-300);
|
|
359
361
|
--search-input-bg-color: var(--neutral-600);
|
|
360
362
|
--search-loading-color: var(--teal-200);
|
|
363
|
+
--search-source-list-color: var(--neutral-700);
|
|
361
364
|
--search-submit-btn-bg-color: var(--teal-200);
|
|
362
365
|
--search-user-message-color: var(--teal-800);
|
|
363
366
|
--tabbed-content-bg-color: color(srgb 0.047058823529411764 0.16470588235294117 0.16470588235294117 / 0.2);
|
|
@@ -440,6 +443,7 @@
|
|
|
440
443
|
--search-icon-color: var(--neutral-400);
|
|
441
444
|
--search-input-bg-color: var(--neutral-100);
|
|
442
445
|
--search-loading-color: var(--teal-700);
|
|
446
|
+
--search-source-list-color: var(--neutral-100);
|
|
443
447
|
--search-submit-btn-bg-color: var(--teal-200);
|
|
444
448
|
--search-user-message-color: var(--teal-000);
|
|
445
449
|
--tabbed-content-bg-color: var(--neutral-000);
|