@netlify/spark-ui 1.0.2 → 1.0.3-alpha.1

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 (45) hide show
  1. package/dist/assets/AskNetlify.css +1 -0
  2. package/dist/assets/Orb.css +1 -0
  3. package/dist/assets/SearchLink.css +1 -1
  4. package/dist/assets/SearchModal.css +1 -1
  5. package/dist/chunks/compat.module.BjTzMHXJ.js +296 -0
  6. package/dist/chunks/hooks.module.DeYLzLnU.js +177 -0
  7. package/dist/chunks/{useTimeout.DOkYFG64.js → useTimeout.DYuvQwFN.js} +1 -1
  8. package/dist/components/index.d.ts +1 -0
  9. package/dist/components/preact/AskNetlify/AskNetlify.d.ts +2 -0
  10. package/dist/components/preact/AskNetlify/AskNetlify.js +5292 -0
  11. package/dist/components/preact/Modal/Modal.js +1 -1
  12. package/dist/components/preact/Orb/Orb.d.ts +2 -0
  13. package/dist/components/preact/Orb/Orb.js +5 -0
  14. package/dist/components/preact/SearchModal/components/SearchInput.d.ts +2 -0
  15. package/dist/components/preact/SearchModal/components/SearchInput.js +2 -2
  16. package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +1 -1
  17. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +81 -51
  18. package/dist/components/preact/SearchModal/components/SearchToggle/SearchToggle.js +1 -1
  19. package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
  20. package/dist/components/preact/SearchModal/components/SearchWindow.js +39 -34
  21. package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +6 -6
  22. package/dist/components/preact/SearchModal/hooks/useSearch.js +2 -2
  23. package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +1 -1
  24. package/dist/components/preact/SearchModal/hooks/useSearchLogger.js +1 -1
  25. package/dist/components/preact/SearchModal/hooks/useSearchParams.js +1 -1
  26. package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +1 -1
  27. package/dist/components/preact/SearchModal/types/search.types.d.ts +3 -0
  28. package/dist/components/preact/index.d.ts +1 -0
  29. package/dist/components/preact/index.js +7 -5
  30. package/package.json +4 -2
  31. package/packages/components/index.ts +1 -0
  32. package/packages/components/preact/AskNetlify/AskNetlify.css +162 -0
  33. package/packages/components/preact/AskNetlify/AskNetlify.tsx +123 -0
  34. package/packages/components/preact/Orb/Orb.css +94 -0
  35. package/packages/components/preact/Orb/Orb.tsx +7 -0
  36. package/packages/components/preact/SearchModal/components/SearchInput.tsx +9 -7
  37. package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.css +6 -1
  38. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.css +67 -11
  39. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +67 -30
  40. package/packages/components/preact/SearchModal/components/SearchWindow.tsx +27 -16
  41. package/packages/components/preact/SearchModal/hooks/useKeyboardNav.ts +2 -2
  42. package/packages/components/preact/SearchModal/types/search.types.tsx +2 -1
  43. package/packages/components/preact/index.ts +1 -0
  44. package/dist/chunks/compat.module.DT16W4Wr.js +0 -175
  45. package/dist/chunks/hooks.module.CNV9mxng.js +0 -138
@@ -0,0 +1,123 @@
1
+ import { useState, useRef, useEffect } from 'preact/hooks';
2
+ import { marked } from 'marked';
3
+ import { KapaProvider, useChat } from '@kapaai/react-sdk';
4
+
5
+ import './AskNetlify.css';
6
+
7
+ function AskNetlify(props) {
8
+ return (
9
+ <KapaProvider
10
+ integrationId="3c0789a6-c6a8-4fbc-b0cc-235f9e4a6351"
11
+ callbacks={{
12
+ askAI: {
13
+ onQuerySubmit: data => console.log('Question asked:', data.question),
14
+ },
15
+ }}
16
+ >
17
+ <ChatInterface {...props} />
18
+ </KapaProvider>
19
+ );
20
+ }
21
+
22
+ function ChatInterface({ setModalState }) {
23
+ const [message, setMessage] = useState('');
24
+ const { conversation, submitQuery, isGeneratingAnswer, isPreparingAnswer, stopGeneration } =
25
+ useChat();
26
+
27
+ const chatRef = useRef(null);
28
+
29
+ const handleSubmit = e => {
30
+ e.preventDefault();
31
+ if (message.trim()) {
32
+ submitQuery(message);
33
+ setMessage('');
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ const el = chatRef.current as HTMLElement | null;
39
+ if (el) el.scrollTop = el.scrollHeight;
40
+ }, [conversation]);
41
+
42
+ return (
43
+ <div className="chat-container">
44
+ <button
45
+ class="button button-back"
46
+ data-button-variant="outline"
47
+ onClick={() => setModalState({ current: 'search' })}
48
+ aria-label={'Return to search'}
49
+ >
50
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
51
+ <path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
52
+ </svg>
53
+ Search
54
+ </button>
55
+ <div ref={chatRef} className="conversation n-search-window">
56
+ {conversation.map((qa, index) => (
57
+ <div key={qa.id || `qa-${index}`} className="message">
58
+ <div className="message-right">
59
+ <div className="message-bubble user-message">
60
+ <p>{qa.question}</p>
61
+ </div>
62
+ </div>
63
+
64
+ <div className="message-left">
65
+ {qa.answer ? (
66
+ <div className="message-bubble bot-message">
67
+ <div
68
+ className="message-answer prose flow"
69
+ dangerouslySetInnerHTML={{
70
+ __html: marked.parse(qa.answer),
71
+ }}
72
+ />
73
+ </div>
74
+ ) : (
75
+ <span class="pulsing"></span>
76
+ )}
77
+ </div>
78
+ </div>
79
+ ))}
80
+ </div>
81
+
82
+ <form onSubmit={handleSubmit} className="form-wrapper n-search-input-wrapper">
83
+ <input
84
+ type="text"
85
+ className="form-input n-search-input"
86
+ value={message}
87
+ onChange={e => setMessage(e.target.value)}
88
+ placeholder="Ask a question..."
89
+ disabled={isGeneratingAnswer}
90
+ />
91
+ {/* <button type="submit" className="form-submit" disabled={isGeneratingAnswer}>
92
+ Send
93
+ </button> */}
94
+
95
+ {isGeneratingAnswer ? (
96
+ <button
97
+ type="button"
98
+ className="form-submit"
99
+ onClick={stopGeneration}
100
+ aria-label="Stop generation"
101
+ >
102
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
103
+ <rect x="0" y="0" width="16" height="16" />
104
+ </svg>
105
+ </button>
106
+ ) : (
107
+ <button
108
+ type="submit"
109
+ className="form-submit"
110
+ disabled={!message.trim() || isPreparingAnswer}
111
+ aria-label="Send message"
112
+ >
113
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor">
114
+ <path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z" />
115
+ </svg>
116
+ </button>
117
+ )}
118
+ </form>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ export default AskNetlify;
@@ -0,0 +1,94 @@
1
+ @property --angle {
2
+ syntax: '<angle>';
3
+ inherits: false;
4
+ initial-value: 0deg;
5
+ }
6
+
7
+ :root {
8
+ --bg: oklch(12.9% 0.042 264.695);
9
+ --c1: oklch(44.4% 0.177 26.899);
10
+ --c2: oklch(85.2% 0.199 91.936);
11
+ --c3: oklch(58.8% 0.158 241.966);
12
+ }
13
+
14
+ .orb {
15
+ display: grid;
16
+ grid-template-areas: 'stack';
17
+ inline-size: min(80vmin, 100%);
18
+ aspect-ratio: 1;
19
+ overflow: hidden;
20
+ border-radius: 50%;
21
+
22
+ &::before,
23
+ &::after {
24
+ content: '';
25
+ display: block;
26
+ grid-area: stack;
27
+ width: 100%;
28
+ height: 100%;
29
+ border-radius: 50%;
30
+ transform: translateZ(0);
31
+ }
32
+
33
+ &::before {
34
+ background:
35
+ conic-gradient(
36
+ from calc(var(--angle) * 2) at 25% 70%,
37
+ var(--c3),
38
+ transparent 20% 80%,
39
+ var(--c3)
40
+ ),
41
+ conic-gradient(
42
+ from calc(var(--angle) * 2) at 45% 75%,
43
+ var(--c2),
44
+ transparent 30% 60%,
45
+ var(--c2)
46
+ ),
47
+ conic-gradient(
48
+ from calc(var(--angle) * -3) at 80% 20%,
49
+ var(--c1),
50
+ transparent 40% 60%,
51
+ var(--c1)
52
+ ),
53
+ conic-gradient(
54
+ from calc(var(--angle) * 2) at 15% 5%,
55
+ var(--c2),
56
+ transparent 10% 90%,
57
+ var(--c2)
58
+ ),
59
+ conic-gradient(
60
+ from calc(var(--angle) * 1) at 20% 80%,
61
+ var(--c1),
62
+ transparent 10% 90%,
63
+ var(--c1)
64
+ ),
65
+ conic-gradient(
66
+ from calc(var(--angle) * -2) at 85% 10%,
67
+ var(--c3),
68
+ transparent 20% 80%,
69
+ var(--c3)
70
+ );
71
+ box-shadow: inset var(--bg) 0 0 5vmin 1vmin;
72
+ filter: blur(3vmin) contrast(5);
73
+ animation: rotate 20s linear infinite;
74
+ }
75
+
76
+ &::after {
77
+ --dot: 1.5px;
78
+ background-image: radial-gradient(
79
+ circle at center,
80
+ var(--bg) var(--dot),
81
+ transparent var(--dot)
82
+ );
83
+ background-size: calc(var(--dot) * 2) calc(var(--dot) * 2);
84
+ mask-image: radial-gradient(black 25%, transparent 75%);
85
+ backdrop-filter: blur(8vmin) contrast(10);
86
+ mix-blend-mode: overlay;
87
+ }
88
+ }
89
+
90
+ @keyframes rotate {
91
+ to {
92
+ --angle: 360deg;
93
+ }
94
+ }
@@ -0,0 +1,7 @@
1
+ import './Orb.css';
2
+
3
+ const Orb = () => {
4
+ return <div class="orb"></div>;
5
+ };
6
+
7
+ export default Orb;
@@ -6,20 +6,22 @@ interface Props {
6
6
  setSearchTerm: (term: string) => void;
7
7
  placeholder?: string;
8
8
  initialValue?: string;
9
+ isGeneratingAnswer?: boolean;
10
+ isPreparingAnswer?: boolean;
9
11
  }
10
12
 
11
13
  const SearchInput = (props: Props) => {
12
14
  const [value, setValue] = useState<string>('');
13
15
 
14
- const { reset, run } = useTimeout((query: string) => {
15
- props.setSearchTerm(query);
16
- }, 300);
17
-
18
- const handleInput = (e: InputEvent) => {
16
+ const { reset, run } = useTimeout((query: string) => {
17
+ props.setSearchTerm(query);
18
+ }, 300);
19
+
20
+ const handleInput = (e: InputEvent) => {
19
21
  const target = e.target as HTMLInputElement;
20
22
  setValue(target.value);
21
- reset()
22
- run(target.value)
23
+ reset();
24
+ run(target.value);
23
25
  };
24
26
 
25
27
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  .n-search-link {
2
2
  --n-search-icon-size: 1em;
3
- all: unset;
3
+ /* all: unset; */
4
+ box-sizing: border-box;
4
5
  display: flex;
5
6
  justify-content: space-between;
6
7
  flex-wrap: wrap;
@@ -11,6 +12,10 @@
11
12
  font-weight: var(--font-weight-regular);
12
13
  text-decoration: none;
13
14
  cursor: pointer;
15
+ width: 100%;
16
+ border: none;
17
+ text-align: left;
18
+ outline: none;
14
19
  }
15
20
 
16
21
  .n-search-link-body {
@@ -4,15 +4,18 @@
4
4
 
5
5
  /* Root Variables */
6
6
  .n-search-modal {
7
- --n-search-padding-inline: 0.5em;
7
+ --n-search-padding-inline: var(--space-s);
8
8
  --n-search-color-text: var(--color-text, currentColor);
9
9
  --n-search-color-text-secondary: var(--color-text-weak, currentColor);
10
10
  --n-search-color-bg: var(--color-bg);
11
11
  --n-search-color-bg-2: var(--color-bg-2);
12
12
  --n-search-color-border: var(--color-bg-3);
13
- --n-search-border-radius: 8px;
13
+ --n-search-border-radius: 1.3rem;
14
14
  --n-search-max-w: 600px;
15
15
  --n-search-text-highlight: var(--color-text-accent);
16
+ --n-search-input-height: 50px;
17
+ --n-search-footer-height: 40px;
18
+ --n-search-container-height: 400px;
16
19
  }
17
20
 
18
21
  .n-search-modal {
@@ -23,11 +26,30 @@
23
26
  margin: auto;
24
27
  }
25
28
 
29
+ .n-search-modal::after {
30
+ content: '';
31
+ display: block;
32
+ width: 100%;
33
+ height: 100%;
34
+ background: blue;
35
+ position: absolute;
36
+ z-index: 0;
37
+ inset: 0;
38
+ transform-origin: center;
39
+ transform: scale(0);
40
+ border-radius: var(--radius-m);
41
+ transition: transform 300ms ease-in-out;
42
+ }
43
+
44
+ .n-search-modal-ask {
45
+ --n-search-max-w: 800px;
46
+ }
47
+
26
48
  .n-search-container {
27
49
  --mark-bg: var(--n-search-text-highlight);
28
50
  display: grid;
29
51
  width: min(var(--n-search-max-w), 100%);
30
- min-height: 470px;
52
+ /* min-height: 530px; */
31
53
  background-color: var(--n-search-color-bg);
32
54
  border-radius: var(--n-search-border-radius);
33
55
  overflow: hidden;
@@ -43,13 +65,15 @@
43
65
  }
44
66
 
45
67
  .n-search-input-wrapper {
46
- all: unset;
68
+ /* all: unset; */
69
+ position: relative;
47
70
  display: flex;
48
71
  width: 100%;
49
- border-bottom: 1px solid var(--n-search-color-border);
72
+ height: var(--n-search-input-height);
50
73
  }
51
74
 
52
75
  .n-search-input {
76
+ position: relative;
53
77
  display: inline-flex;
54
78
  box-sizing: border-box;
55
79
  background: var(--n-search-color-bg);
@@ -63,13 +87,25 @@
63
87
  color: var(--color-text);
64
88
  }
65
89
 
90
+ .n-search-input-wrapper::after,
91
+ .n-search-footer::after {
92
+ content: '';
93
+ display: block;
94
+ width: 200%;
95
+ height: 1px;
96
+ background: var(--n-search-color-border);
97
+ position: absolute;
98
+ bottom: 0;
99
+ left: 0;
100
+ }
101
+
66
102
  .n-search-input::placeholder {
67
103
  color: var(--n-search-color-text);
68
104
  }
69
105
 
70
106
  .n-search-label {
71
107
  display: flex;
72
- padding-inline: var(--n-search-padding-inline);
108
+ padding-inline-start: var(--n-search-padding-inline);
73
109
  align-items: center;
74
110
  }
75
111
 
@@ -92,7 +128,7 @@
92
128
 
93
129
  /* Search Results */
94
130
  .n-search-window {
95
- height: 400px;
131
+ height: var(--n-search-container-height);
96
132
  overflow-x: hidden;
97
133
  overflow-y: auto;
98
134
  overscroll-behavior: contain;
@@ -115,11 +151,15 @@ ul[role='list'] {
115
151
 
116
152
  /* Footer */
117
153
  .n-search-footer {
118
- border-top: 1px solid var(--n-search-color-border);
119
- padding: var(--space-2xs) var(--space-s);
120
- background: var(--color-bg-2);
154
+ position: relative;
121
155
  display: flex;
122
156
  gap: var(--space-m);
157
+ padding: var(--space-2xs) var(--space-s);
158
+ height: var(--n-search-footer-height);
159
+ }
160
+
161
+ .n-search-footer::after {
162
+ top: 0;
123
163
  }
124
164
 
125
165
  .n-search-footer-tip {
@@ -134,7 +174,8 @@ ul[role='list'] {
134
174
  .n-search-modal {
135
175
  transition:
136
176
  display 0.2s allow-discrete,
137
- overlay 0.2s allow-discrete;
177
+ overlay 0.2s allow-discrete,
178
+ width 0.3s ease-in-out;
138
179
  animation: closeSearch 0.2s forwards;
139
180
  }
140
181
 
@@ -142,6 +183,21 @@ ul[role='list'] {
142
183
  animation: openSearch 0.2s forwards;
143
184
  }
144
185
 
186
+ .n-search-modal[data-state='search'] > * {
187
+ opacity: 0;
188
+ animation: appearContainer 300ms ease-in-out forwards;
189
+ animation-delay: 300ms;
190
+ }
191
+
192
+ @keyframes appearContainer {
193
+ 0% {
194
+ opacity: 0;
195
+ }
196
+ 100% {
197
+ opacity: 1;
198
+ }
199
+ }
200
+
145
201
  @keyframes openSearch {
146
202
  from {
147
203
  opacity: 0;
@@ -14,21 +14,26 @@ import {
14
14
  useSearchToggles,
15
15
  useSearchLogger,
16
16
  } from '../../hooks';
17
+ import { useTimeout } from '@utils/useTimeout';
18
+
19
+ import AskNetlify from '../../../../preact/AskNetlify/AskNetlify';
17
20
 
18
21
  import './SearchModal.css';
19
22
 
20
23
  interface Props {
21
24
  children: ComponentChildren;
22
25
  className?: string;
23
- debug?: boolean
26
+ debug?: boolean;
24
27
  }
25
28
 
26
- function SearchModal(props: Props = {className: '', children: null, debug: false }) {
29
+ function SearchModal(props: Props = { className: '', children: null, debug: false }) {
27
30
  const [isPagefindLoaded, setIsPagefindLoaded] = useState(false);
31
+ const [modalState, setModalState] = useState({ current: 'search' });
32
+ const [askNetlifyQuery, setAskNetlifyQuery] = useState('');
28
33
 
29
34
  // Poll for the existence of window.pagefind.
30
35
  useEffect(() => {
31
- let intervalId:number;
36
+ let intervalId: number;
32
37
 
33
38
  const checkForPagefind = () => {
34
39
  if (window.pagefind) {
@@ -45,19 +50,18 @@ function SearchModal(props: Props = {className: '', children: null, debug: false
45
50
  return () => clearInterval(intervalId);
46
51
  }, []);
47
52
 
48
-
49
53
  const { children, className, debug, ...rest } = props;
50
54
  const { state, actions } = useSearch();
51
- const [searchOpen, setSearchOpen] = useState(false);
52
55
 
56
+ const [searchOpen, setSearchOpen] = useState(false);
53
57
  const openSearch = () => setSearchOpen(true);
54
58
  const closeSearch = () => setSearchOpen(false);
55
59
 
56
60
  const modalRef = useRef<HTMLDialogElement>(null);
57
61
  const containerRef = useClickOutside<HTMLDivElement>(closeSearch);
58
62
 
59
- useSearchLogger(state, debug)
60
- useSearchToggles(isPagefindLoaded, openSearch);
63
+ useSearchLogger(state, debug);
64
+ useSearchToggles(isPagefindLoaded, openSearch);
61
65
  useSearchParams(isPagefindLoaded, actions.setSearchTerm, openSearch);
62
66
  useSearchHotkeys(isPagefindLoaded, searchOpen, openSearch, closeSearch);
63
67
  useKeyboardNav(isPagefindLoaded, modalRef, searchOpen, closeSearch);
@@ -67,31 +71,64 @@ function SearchModal(props: Props = {className: '', children: null, debug: false
67
71
  searchOpen ? modalRef.current.showModal() : modalRef.current.close();
68
72
  }, [searchOpen]);
69
73
 
74
+ useEffect(() => {
75
+ const input = modalRef.current?.querySelector('input');
76
+
77
+ if (input) input.focus();
78
+
79
+ if (modalState.current === 'ask') {
80
+ setTimeout(() => {
81
+ modalRef.current.classList.add('n-search-modal-ask');
82
+ }, 150);
83
+ } else if (modalState.current === 'search') {
84
+ setTimeout(() => {
85
+ modalRef.current.classList.remove('n-search-modal-ask');
86
+ }, 150);
87
+ }
88
+ }, [modalState.current]);
89
+
70
90
  return (
71
- <Modal ref={modalRef} className={`n-search-modal ${className ? className : ''}`} {...rest}>
91
+ <Modal
92
+ ref={modalRef}
93
+ className={`n-search-modal ${className ? className : ''}`}
94
+ data-state={modalState.current}
95
+ {...rest}
96
+ >
72
97
  <div ref={containerRef} className="n-search-container">
73
- <SearchInput setSearchTerm={actions.setSearchTerm} initialValue={state.searchTerm} />
74
-
75
- <SearchWindow
76
- searchLoading={state.isLoading}
77
- searchTerm={state.searchTerm}
78
- results={state.results}
79
- >
80
- {children}
81
- </SearchWindow>
82
-
83
- <div className="n-search-footer">
84
- <p className="n-search-footer-tip">
85
- <KeyboardKey keyNames={['Esc']} />
86
- to close
87
- </p>
88
- <p className="n-search-footer-tip">
89
- <KeyboardKey keyNames={['Tab']} />
90
- <span>or</span>
91
- <KeyboardKey keyNames={['↑', '↓']} />
92
- to navigate
93
- </p>
94
- </div>
98
+ {modalState.current === 'ask' && (
99
+ <AskNetlify query={askNetlifyQuery} setModalState={setModalState} />
100
+ )}
101
+ {modalState.current === 'search' && (
102
+ <>
103
+ <SearchInput
104
+ setSearchTerm={actions.setSearchTerm}
105
+ setAskNetlifyQuery={setAskNetlifyQuery}
106
+ initialValue={state.searchTerm}
107
+ />
108
+
109
+ <SearchWindow
110
+ setModalState={setModalState}
111
+ searchLoading={state.isLoading}
112
+ searchTerm={state.searchTerm}
113
+ results={state.results}
114
+ >
115
+ {children}
116
+ </SearchWindow>
117
+
118
+ <div className="n-search-footer">
119
+ <p className="n-search-footer-tip">
120
+ <KeyboardKey keyNames={['Esc']} />
121
+ to closed
122
+ </p>
123
+ <p className="n-search-footer-tip">
124
+ <KeyboardKey keyNames={['Tab']} />
125
+ <span>or</span>
126
+ <KeyboardKey keyNames={['↑', '↓']} />
127
+ to navigate
128
+ </p>
129
+ </div>
130
+ </>
131
+ )}
95
132
  </div>
96
133
  </Modal>
97
134
  );
@@ -4,6 +4,8 @@ import { useMemo } from 'preact/hooks';
4
4
  import SearchHeading from './SearchHeading/SearchHeading';
5
5
  import SearchLink from './SearchLink/SearchLink';
6
6
 
7
+ import Icon from '@components/preact/Icon';
8
+
7
9
  /* --- Search Results Window (for active searches) --- */
8
10
  const SearchResults: FunctionComponent<SearchResultsProps> = ({
9
11
  searchLoading,
@@ -20,9 +22,9 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({
20
22
 
21
23
  if (!results || results.length === 0) {
22
24
  return (
23
- <SearchHeading>
24
- 0 results for <span className="n-search-term">{searchTerm}</span>
25
- </SearchHeading>
25
+ <SearchHeading>
26
+ 0 results for <span className="n-search-term">{searchTerm}</span>
27
+ </SearchHeading>
26
28
  );
27
29
  }
28
30
 
@@ -43,29 +45,38 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({
43
45
 
44
46
  /* --- Main Search Results Component --- */
45
47
  export default function SearchWindow({
48
+ setModalState,
46
49
  searchLoading,
47
50
  searchTerm,
48
51
  results,
49
52
  children: DefaultWindow,
50
53
  }: SearchWindowProps) {
51
- const askNetlify: SearchLinkProps = {
52
- icon: 'sparkles',
53
- label: 'Ask Netlify AI',
54
- url: 'https://docs.netlify.com/ask-netlify/',
55
- };
56
-
57
54
  return (
58
55
  <div className="n-search-window">
59
56
  <div className="search-links">
60
- <SearchLink {...askNetlify} />
61
57
  {searchTerm && searchTerm.length > 1 ? (
62
- <SearchResults
63
- searchLoading={searchLoading}
64
- searchTerm={searchTerm}
65
- results={results}
66
- />
58
+ <SearchResults searchLoading={searchLoading} searchTerm={searchTerm} results={results} />
67
59
  ) : DefaultWindow ? (
68
- DefaultWindow
60
+ <>
61
+ <button
62
+ class="btn-ask-netlify n-search-link"
63
+ onClick={() => {
64
+ setModalState({ current: 'ask' });
65
+ }}
66
+ >
67
+ <span className="n-search-link-icon">
68
+ <Icon name={'netlify'} />
69
+ </span>
70
+ <div className="n-search-link-body">
71
+ <p className="n-search-link-title">Ask Netlify AI</p>
72
+ </div>
73
+ <div className="n-search-link-icon">
74
+ <Icon name="caretRight" />
75
+ </div>
76
+ </button>
77
+
78
+ {DefaultWindow}
79
+ </>
69
80
  ) : null}
70
81
  </div>
71
82
  </div>
@@ -21,7 +21,7 @@ export function useKeyboardNav(
21
21
  onClose: () => void
22
22
  ) {
23
23
  useEffect(() => {
24
- if (!isPagefindLoaded) return
24
+ if (!isPagefindLoaded) return;
25
25
  const handleKeydown = (e: KeyboardEvent) => {
26
26
  if (!isOpen) return;
27
27
 
@@ -35,7 +35,7 @@ export function useKeyboardNav(
35
35
  const container = containerRef.current;
36
36
  if (!container) return;
37
37
 
38
- const links = Array.from(container.querySelectorAll('a'));
38
+ const links = Array.from(container.querySelectorAll('.n-search-link'));
39
39
  const currentIndex = links.findIndex(link => link === document.activeElement);
40
40
  const nextIndex =
41
41
  e.key === 'ArrowDown'
@@ -10,6 +10,7 @@ export interface SearchLinkProps {
10
10
  }
11
11
 
12
12
  export interface SearchWindowProps {
13
+ setModalState: (e: { current: string }) => void;
13
14
  searchLoading: boolean;
14
15
  searchTerm?: string;
15
16
  results?: SearchLinkProps[];
@@ -33,4 +34,4 @@ export interface SearchGTMObjectProps {
33
34
  resultUrl?: string;
34
35
  query?: string;
35
36
  debug_mode?: boolean;
36
- }
37
+ }
@@ -4,6 +4,7 @@ export { default as Icon } from './Icon';
4
4
  export { default as SearchLink } from './SearchModal/components/SearchLink/SearchLink';
5
5
  export { default as SearchHeading } from './SearchModal/components/SearchHeading/SearchHeading';
6
6
  export { default as SearchToggle } from './SearchModal/components/SearchToggle/SearchToggle';
7
+ export { default as AskNetlify } from "./AskNetlify/AskNetlify"
7
8
  export { SearchModal } from './SearchModal';
8
9
 
9
10
  export { type IconName } from './Icon';