@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.
Files changed (62) hide show
  1. package/dist/assets/AskNetlify.css +1 -1
  2. package/dist/assets/SearchLink.css +1 -1
  3. package/dist/assets/SearchModal.css +1 -1
  4. package/dist/chunks/{ChatInterface.EQAPs01R.js → AskNetlify.lrswYyKn.js} +119 -110
  5. package/dist/components/preact/AskNetlify/AskNetlify.d.ts +3 -3
  6. package/dist/components/preact/AskNetlify/AskNetlify.js +8 -34
  7. package/dist/components/preact/AskNetlify/components/ChatMessages.d.ts +2 -1
  8. package/dist/components/preact/AskNetlify/components/ChatMessages.js +25 -21
  9. package/dist/components/preact/AskNetlify/components/SourcesList.js +17 -13
  10. package/dist/components/preact/AskNetlify/hooks/useAskAnalytics.d.ts +23 -0
  11. package/dist/components/preact/AskNetlify/hooks/useAskAnalytics.js +22 -0
  12. package/dist/components/preact/Icon/index.js +6 -6
  13. package/dist/components/preact/SearchModal/components/SearchInput.js +18 -8
  14. package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.d.ts +1 -1
  15. package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +23 -22
  16. package/dist/components/preact/SearchModal/components/SearchLinkThemeToggle.d.ts +1 -0
  17. package/dist/components/preact/SearchModal/components/SearchLinkThemeToggle.js +26 -0
  18. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +89 -84
  19. package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
  20. package/dist/components/preact/SearchModal/components/SearchWindow.js +55 -48
  21. package/dist/components/preact/SearchModal/hooks/index.d.ts +1 -1
  22. package/dist/components/preact/SearchModal/hooks/index.js +8 -8
  23. package/dist/components/preact/SearchModal/hooks/useKeyboardNav.d.ts +1 -1
  24. package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +11 -12
  25. package/dist/components/preact/SearchModal/hooks/useSearch.js +19 -19
  26. package/dist/components/preact/SearchModal/hooks/{useSearchLogger.d.ts → useSearchAnalytics.d.ts} +2 -2
  27. package/dist/components/preact/SearchModal/hooks/{useSearchLogger.js → useSearchAnalytics.js} +5 -5
  28. package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.d.ts +1 -1
  29. package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +8 -9
  30. package/dist/components/preact/SearchModal/hooks/useSearchParams.d.ts +1 -1
  31. package/dist/components/preact/SearchModal/hooks/useSearchParams.js +14 -7
  32. package/dist/components/preact/SearchModal/hooks/useSearchToggles.d.ts +1 -1
  33. package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +7 -8
  34. package/dist/components/preact/SearchModal/types/search.types.d.ts +11 -4
  35. package/dist/components/preact/index.js +3 -3
  36. package/package.json +1 -1
  37. package/packages/components/preact/AskNetlify/AskNetlify.css +41 -1
  38. package/packages/components/preact/AskNetlify/AskNetlify.tsx +98 -34
  39. package/packages/components/preact/AskNetlify/components/ChatMessages.tsx +9 -7
  40. package/packages/components/preact/AskNetlify/components/SourcesList.tsx +24 -16
  41. package/packages/components/preact/AskNetlify/hooks/useAskAnalytics.tsx +50 -0
  42. package/packages/components/preact/Icon/index.tsx +2 -2
  43. package/packages/components/preact/SearchModal/components/SearchInput.tsx +10 -0
  44. package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.css +5 -0
  45. package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.tsx +6 -4
  46. package/packages/components/preact/SearchModal/components/SearchLinkThemeToggle.tsx +19 -0
  47. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.css +33 -3
  48. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +73 -89
  49. package/packages/components/preact/SearchModal/components/SearchWindow.tsx +64 -46
  50. package/packages/components/preact/SearchModal/hooks/index.ts +1 -1
  51. package/packages/components/preact/SearchModal/hooks/useKeyboardNav.ts +1 -3
  52. package/packages/components/preact/SearchModal/hooks/useSearch.ts +20 -16
  53. package/packages/components/preact/SearchModal/hooks/{useSearchLogger.ts → useSearchAnalytics.ts} +9 -10
  54. package/packages/components/preact/SearchModal/hooks/useSearchHotkeys.ts +2 -4
  55. package/packages/components/preact/SearchModal/hooks/useSearchParams.ts +24 -2
  56. package/packages/components/preact/SearchModal/hooks/useSearchToggles.ts +1 -3
  57. package/packages/components/preact/SearchModal/types/search.types.tsx +7 -4
  58. package/packages/styles/loaders.css +18 -0
  59. package/packages/styles/tokens.css +4 -0
  60. package/dist/components/preact/AskNetlify/components/ChatInterface.d.ts +0 -7
  61. package/dist/components/preact/AskNetlify/components/ChatInterface.js +0 -10
  62. 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
- useSearchLogger,
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
- useSearchLogger(state, debug);
66
- useSearchToggles(isPagefindLoaded, openSearch, setModalState);
67
- useSearchParams(isPagefindLoaded, actions.setSearchTerm, openSearch);
68
- useSearchHotkeys(isPagefindLoaded, searchOpen, openSearch, closeSearch);
69
- useKeyboardNav(isPagefindLoaded, modalRef, searchOpen, closeSearch);
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
- <Modal
94
- ref={modalRef}
95
- className={`n-search-modal ${className ? className : ''}`}
96
- data-state={modalState.current}
97
- {...rest}
65
+ <KapaProvider
66
+ integrationId={props.kapaIntegrationId || ''}
67
+ callbacks={{
68
+ askAI: {
69
+ onQuerySubmit: handleNewQuery,
70
+ onAnswerGenerationCompleted: handleAnswer,
71
+ },
72
+ }}
98
73
  >
99
- <div ref={containerRef} className="n-search-container">
100
- {modalState.current === 'ask' && (
101
- <AskNetlify setModalState={setModalState} kapaIntegrationId={props.kapaIntegrationId} />
102
- )}
103
- {modalState.current === 'search' && (
104
- <>
105
- <SearchInput setSearchTerm={actions.setSearchTerm} initialValue={state.searchTerm} />
106
-
107
- <SearchWindow
108
- searchLoading={state.isLoading}
109
- searchTerm={state.searchTerm}
110
- results={state.results}
111
- >
112
- <>
113
- {props.kapaIntegrationId && (
114
- <SearchLink
115
- as="button"
116
- icon="askNetlify"
117
- label="Ask Netlify AI"
118
- onClick={() => {
119
- setModalState({ current: 'ask' });
120
- }}
121
- />
122
- )}
123
- {children}
124
- </>
125
- </SearchWindow>
126
-
127
- <div className="n-search-footer">
128
- <p className="n-search-footer-tip">
129
- <KeyboardKey keyNames={['Esc']} />
130
- to close
131
- </p>
132
- <p className="n-search-footer-tip">
133
- <KeyboardKey keyNames={['Tab']} />
134
- <span>or</span>
135
- <KeyboardKey keyNames={['↑', '↓']} />
136
- to navigate
137
- </p>
138
- </div>
139
- </>
140
- )}
141
- </div>
142
- </Modal>
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
- import { handleThemeChange, ThemeMap, Theme } from '../../../../utils/theme';
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
- if (searchLoading) {
16
- return (
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
- <ul className="n-results-list" role="list">
41
- {renderedResults}
42
- </ul>
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
- {searchTerm && searchTerm.length > 1 ? (
57
- <SearchResults searchLoading={searchLoading} searchTerm={searchTerm} results={results} />
58
- ) : DefaultWindow ? (
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
- <SearchLink
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
- ) : null}
93
+ )}
76
94
  </div>
77
95
  </div>
78
96
  );
@@ -1,5 +1,5 @@
1
1
  export { useSearch } from '../hooks/useSearch';
2
- export { useSearchLogger } from './useSearchLogger';
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
- }, [isPagefindLoaded, containerRef, isOpen, onClose]);
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
- submitSearch(query);
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 = search?.results && await Promise.all(
42
- search?.results?.slice(0, 15).map(async (result: any) => {
43
- const data = await result.data();
44
- const removeAnchorLink = (text: string) => text.replace("#", '')
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
- return {
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
  };
@@ -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 } = useSearchLogger(searchState);
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 useSearchLogger(state?: SearchState, debug?: boolean) {
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(isPagefindLoaded: boolean, isOpen: boolean, onOpen: () => void, onClose: () => void) {
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
- }, [isPagefindLoaded, isOpen, onOpen, onClose]);
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(isPagefindLoaded: boolean, setSearchTerm: (term: string) => void, onOpen: () => void) {
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
- }, [isPagefindLoaded, onOpen]);
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: IconName;
7
- label: string;
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?: string;
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?: string;
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);
@@ -1,7 +0,0 @@
1
- interface Props {
2
- setModalState?: (e: {
3
- current: string;
4
- }) => void;
5
- }
6
- export default function ChatInterface({ setModalState }: Props): import("preact").JSX.Element;
7
- export {};