@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.
- package/dist/assets/AskNetlify.css +1 -0
- package/dist/assets/Orb.css +1 -0
- package/dist/assets/SearchLink.css +1 -1
- package/dist/assets/SearchModal.css +1 -1
- package/dist/chunks/compat.module.BjTzMHXJ.js +296 -0
- package/dist/chunks/hooks.module.DeYLzLnU.js +177 -0
- package/dist/chunks/{useTimeout.DOkYFG64.js → useTimeout.DYuvQwFN.js} +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/preact/AskNetlify/AskNetlify.d.ts +2 -0
- package/dist/components/preact/AskNetlify/AskNetlify.js +5292 -0
- package/dist/components/preact/Modal/Modal.js +1 -1
- package/dist/components/preact/Orb/Orb.d.ts +2 -0
- package/dist/components/preact/Orb/Orb.js +5 -0
- package/dist/components/preact/SearchModal/components/SearchInput.d.ts +2 -0
- package/dist/components/preact/SearchModal/components/SearchInput.js +2 -2
- package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +1 -1
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +81 -51
- package/dist/components/preact/SearchModal/components/SearchToggle/SearchToggle.js +1 -1
- package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
- package/dist/components/preact/SearchModal/components/SearchWindow.js +39 -34
- package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +6 -6
- package/dist/components/preact/SearchModal/hooks/useSearch.js +2 -2
- package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchLogger.js +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchParams.js +1 -1
- package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +1 -1
- package/dist/components/preact/SearchModal/types/search.types.d.ts +3 -0
- package/dist/components/preact/index.d.ts +1 -0
- package/dist/components/preact/index.js +7 -5
- package/package.json +4 -2
- package/packages/components/index.ts +1 -0
- package/packages/components/preact/AskNetlify/AskNetlify.css +162 -0
- package/packages/components/preact/AskNetlify/AskNetlify.tsx +123 -0
- package/packages/components/preact/Orb/Orb.css +94 -0
- package/packages/components/preact/Orb/Orb.tsx +7 -0
- package/packages/components/preact/SearchModal/components/SearchInput.tsx +9 -7
- package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.css +6 -1
- package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.css +67 -11
- package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +67 -30
- package/packages/components/preact/SearchModal/components/SearchWindow.tsx +27 -16
- package/packages/components/preact/SearchModal/hooks/useKeyboardNav.ts +2 -2
- package/packages/components/preact/SearchModal/types/search.types.tsx +2 -1
- package/packages/components/preact/index.ts +1 -0
- package/dist/chunks/compat.module.DT16W4Wr.js +0 -175
- 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
|
+
}
|
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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('
|
|
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';
|