@netlify/spark-ui 1.4.5 → 1.5.0-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 (52) 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.BNbXPLVY.js +298 -0
  6. package/dist/chunks/hooks.module.DGkdD5n9.js +177 -0
  7. package/dist/chunks/{useTimeout.YR4eKwj7.js → useTimeout.Cq4aqnDe.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 +5666 -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.d.ts +1 -1
  17. package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +35 -17
  18. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +81 -51
  19. package/dist/components/preact/SearchModal/components/SearchToggle/SearchToggle.js +1 -1
  20. package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
  21. package/dist/components/preact/SearchModal/components/SearchWindow.js +48 -30
  22. package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +6 -6
  23. package/dist/components/preact/SearchModal/hooks/useSearch.js +2 -2
  24. package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +1 -1
  25. package/dist/components/preact/SearchModal/hooks/useSearchLogger.js +1 -1
  26. package/dist/components/preact/SearchModal/hooks/useSearchParams.js +1 -1
  27. package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +1 -1
  28. package/dist/components/preact/SearchModal/types/search.types.d.ts +6 -1
  29. package/dist/components/preact/index.d.ts +1 -0
  30. package/dist/components/preact/index.js +7 -5
  31. package/dist/utils/theme.d.ts +3 -0
  32. package/package.json +4 -2
  33. package/packages/components/CodeBlock.astro +1 -1
  34. package/packages/components/index.ts +1 -0
  35. package/packages/components/preact/AskNetlify/AskNetlify.css +202 -0
  36. package/packages/components/preact/AskNetlify/AskNetlify.tsx +155 -0
  37. package/packages/components/preact/Orb/Orb.css +94 -0
  38. package/packages/components/preact/Orb/Orb.tsx +7 -0
  39. package/packages/components/preact/SearchModal/components/SearchInput.tsx +9 -7
  40. package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.css +7 -2
  41. package/packages/components/preact/SearchModal/components/SearchLink/SearchLink.tsx +27 -7
  42. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.css +81 -18
  43. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +67 -30
  44. package/packages/components/preact/SearchModal/components/SearchWindow.tsx +30 -17
  45. package/packages/components/preact/SearchModal/hooks/useKeyboardNav.ts +2 -2
  46. package/packages/components/preact/SearchModal/types/search.types.tsx +5 -2
  47. package/packages/components/preact/index.ts +1 -0
  48. package/packages/styles/tokens.css +28 -0
  49. package/packages/utils/theme.ts +12 -0
  50. package/dist/chunks/compat.module.DJD78v1z.js +0 -175
  51. package/dist/chunks/hooks.module.D9IrxdEh.js +0 -138
  52. package/packages/themes/poimandres.json +0 -311
@@ -0,0 +1,202 @@
1
+ .chat-container {
2
+ opacity: 0;
3
+ animation: appearContainer 120ms ease-in-out forwards;
4
+ animation-delay: 140ms;
5
+ }
6
+
7
+ @keyframes appearContainer {
8
+ 0% {
9
+ opacity: 0;
10
+ }
11
+ 100% {
12
+ opacity: 1;
13
+ }
14
+ }
15
+
16
+ .conversation {
17
+ height: calc(var(--n-search-container-height) + var(--n-search-footer-height));
18
+ padding: var(--n-search-padding-inline);
19
+ scroll-behavior: smooth;
20
+ overflow-y: auto;
21
+ display: grid;
22
+ align-items: flex-end;
23
+ gap: var(--space-s);
24
+ }
25
+
26
+ .question {
27
+ border: none;
28
+ color: var(--color-text-1);
29
+ font-weight: var(--font-weight-regular);
30
+ font-size: var(--step--1);
31
+ border-radius: var(--radius-pill);
32
+ transform: translateX(-0.75rem);
33
+ }
34
+
35
+ .question-list li {
36
+ opacity: 0;
37
+ transform: translateY(10px);
38
+ animation: fadeInUp 0.5s forwards;
39
+ }
40
+
41
+ .question-list li:nth-child(1) {
42
+ animation-delay: 0.5s;
43
+ }
44
+
45
+ .question-list li:nth-child(2) {
46
+ animation-delay: 0.3s;
47
+ }
48
+
49
+ .question-list li:nth-child(3) {
50
+ animation-delay: 0.1s;
51
+ }
52
+
53
+ @keyframes fadeInUp {
54
+ to {
55
+ opacity: 1;
56
+ transform: translateY(0);
57
+ }
58
+ }
59
+
60
+ .message {
61
+ display: grid;
62
+ gap: var(--space-s);
63
+ }
64
+
65
+ .message-bubble {
66
+ padding: var(--space-xs) var(--space-s);
67
+ background: var(--search-answer-message-color);
68
+ border-radius: var(--radius-l);
69
+ width: fit-content;
70
+ max-width: 85%;
71
+ font-size: var(--step-0);
72
+ }
73
+
74
+ .prose {
75
+ font-size: var(--step--0);
76
+
77
+ > * + * {
78
+ margin-top: var(--flow-space, var(--space-xs));
79
+ }
80
+
81
+ h1,
82
+ h2,
83
+ h3 {
84
+ font-size: var(--step-1);
85
+ }
86
+
87
+ h4,
88
+ h5,
89
+ h6 {
90
+ font-size: var(--step-0);
91
+ }
92
+
93
+ pre {
94
+ background: var(--neutral-light-700);
95
+ border-radius: var(--radius-m);
96
+ max-width: 100%;
97
+ box-sizing: border-box;
98
+ }
99
+
100
+ code {
101
+ overflow-x: auto;
102
+ }
103
+
104
+ code[class*='language-'],
105
+ pre[class*='language-'] {
106
+ display: block;
107
+ padding: var(--space-s);
108
+ }
109
+ }
110
+
111
+ .user-message {
112
+ margin-inline-start: auto;
113
+ background: var(--search-user-message-color);
114
+ }
115
+
116
+ .form-wrapper {
117
+ display: flex;
118
+ align-items: center;
119
+ position: relative;
120
+ height: var(--n-search-input-height);
121
+ }
122
+
123
+ .form-wrapper::after {
124
+ content: '';
125
+ display: block;
126
+ width: 100%;
127
+ height: 1px;
128
+ background: var(--color-bg-3);
129
+ position: absolute;
130
+ top: 0;
131
+ left: 0;
132
+ }
133
+
134
+ .form-input {
135
+ border: none;
136
+ outline: none;
137
+ padding-inline: var(--n-search-padding-inline);
138
+ }
139
+
140
+ .form-submit {
141
+ border: none;
142
+ padding: 0.7rem;
143
+ border-radius: var(--radius-pill);
144
+ margin-inline-end: var(--space-2xs);
145
+ border: none;
146
+ background: var(--color-link);
147
+ color: var(--neutral-000);
148
+ transition: background-color 150ms ease-out;
149
+ }
150
+
151
+ .form-submit > svg {
152
+ width: 1rem;
153
+ height: auto;
154
+ aspect-ratio: 1 / 1;
155
+ border-radius: 1.5px;
156
+ transition: opacity 150ms ease-out;
157
+ }
158
+
159
+ .form-submit[disabled] {
160
+ background: var(--neutral-500);
161
+
162
+ & > * {
163
+ opacity: 0.5;
164
+ }
165
+ }
166
+
167
+ .button-back {
168
+ width: fit-content;
169
+ background: transparent;
170
+ position: absolute;
171
+ z-index: 1;
172
+ padding: 0.5rem 1rem;
173
+ background: var(--search-back-btn-bg-color);
174
+ margin: 1rem;
175
+ border: 2px solid var(--color-bg-3);
176
+ border-radius: var(--radius-pill);
177
+ font-size: var(--step--1);
178
+ border: 1px solid;
179
+ cursor: pointer;
180
+ border-color: var(--color-bg-3);
181
+ color: var(--color-text-1);
182
+ border-width: 0.5px;
183
+ }
184
+
185
+ .pulsing {
186
+ display: block;
187
+ width: 1rem;
188
+ height: 1rem;
189
+ clip-path: circle(40%);
190
+ background: var(--search-loading-color);
191
+ animation: pulsing 2.5s infinite ease-in-out;
192
+ }
193
+
194
+ @keyframes pulsing {
195
+ 0%,
196
+ 100% {
197
+ transform: scale(0.9);
198
+ }
199
+ 50% {
200
+ transform: scale(1.05);
201
+ }
202
+ }
@@ -0,0 +1,155 @@
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 QandAMap = {
30
+ '487cda5f-e852-4223-85dd-d18ab58dc7a9': "Why can't I add my domain to Netlify DNS?",
31
+ 'd133a822-dbe1-4870-8a94-212c5841f194': 'Why is my website not loading?',
32
+ 'e477f4dd-7159-4c31-8a18-4dfc7e84d927': 'How do I set up environment variables?',
33
+ };
34
+
35
+ const handleSubmit = e => {
36
+ e.preventDefault();
37
+
38
+ const id = e.target.getAttribute('data-id');
39
+
40
+ if (id) {
41
+ const question = QandAMap[id];
42
+ if (question.trim()) {
43
+ submitQuery(question.trim());
44
+ }
45
+ setMessage('');
46
+ }
47
+
48
+ if (message.trim()) {
49
+ submitQuery(message.trim());
50
+ setMessage('');
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ const el = chatRef.current as HTMLElement | null;
56
+ if (el) el.scrollTop = el.scrollHeight;
57
+ }, [conversation]);
58
+
59
+ return (
60
+ <div className="chat-container">
61
+ <button
62
+ class="button button-back"
63
+ data-button-variant="outline"
64
+ onClick={() => setModalState({ current: 'search' })}
65
+ aria-label={'Return to search'}
66
+ >
67
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
68
+ <path
69
+ 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"
70
+ fill="currentColor"
71
+ />
72
+ </svg>
73
+ Search
74
+ </button>
75
+ <div ref={chatRef} className="conversation n-search-window">
76
+ {!conversation.length && (
77
+ <ul class="question-list" role="list">
78
+ {Object.entries(QandAMap).map(([id, question]) => (
79
+ <li>
80
+ <button class="question" onClick={handleSubmit} data-id={id}>
81
+ {question}
82
+ </button>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ )}
87
+
88
+ {conversation.map((qa, index) => (
89
+ <div key={qa.id || `qa-${index}`} className="message">
90
+ <div className="message-right">
91
+ <div className="message-bubble user-message">
92
+ <p>{qa.question}</p>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="message-left">
97
+ {qa.answer ? (
98
+ <div className="message-bubble bot-message">
99
+ <div
100
+ className="message-answer prose flow"
101
+ dangerouslySetInnerHTML={{
102
+ __html: marked.parse(qa.answer),
103
+ }}
104
+ />
105
+ </div>
106
+ ) : (
107
+ <span class="pulsing"></span>
108
+ )}
109
+ </div>
110
+ </div>
111
+ ))}
112
+ </div>
113
+
114
+ <form onSubmit={handleSubmit} className="form-wrapper n-search-input-wrapper">
115
+ <input
116
+ type="text"
117
+ className="form-input n-search-input"
118
+ value={message}
119
+ onChange={e => setMessage(e.target.value)}
120
+ placeholder="Ask a question..."
121
+ disabled={isGeneratingAnswer}
122
+ />
123
+ {/* <button type="submit" className="form-submit" disabled={isGeneratingAnswer}>
124
+ Send
125
+ </button> */}
126
+
127
+ {isGeneratingAnswer ? (
128
+ <button
129
+ type="button"
130
+ className="form-submit"
131
+ onClick={stopGeneration}
132
+ aria-label="Stop generation"
133
+ >
134
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
135
+ <rect x="0" y="0" width="16" height="16" />
136
+ </svg>
137
+ </button>
138
+ ) : (
139
+ <button
140
+ type="submit"
141
+ className="form-submit"
142
+ disabled={!message.trim() || isPreparingAnswer}
143
+ aria-label="Send message"
144
+ >
145
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor">
146
+ <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" />
147
+ </svg>
148
+ </button>
149
+ )}
150
+ </form>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ 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 {
@@ -30,7 +35,7 @@
30
35
 
31
36
  .n-search-link-title {
32
37
  font-weight: var(--font-weight-medium);
33
- color: var(--color-text);
38
+ color: var(--color-text-1);
34
39
  line-height: 1.5;
35
40
  }
36
41
 
@@ -4,20 +4,40 @@ import './SearchLink.css';
4
4
 
5
5
  import Icon from '../../../Icon';
6
6
 
7
- const SearchLink = ({ icon, label, url, excerpt, resource }: SearchLinkProps) => {
8
- const { handleClick } = useSearchLogger()
9
-
7
+ const SearchLink = ({
8
+ as: Component = 'a',
9
+ icon,
10
+ label,
11
+ url,
12
+ excerpt,
13
+ resource,
14
+ onClick,
15
+ }: SearchLinkProps) => {
16
+ const { handleClick: log } = useSearchLogger();
17
+
10
18
  return (
11
- <a className="n-search-link" href={url} onClick={handleClick}>
19
+ <Component
20
+ className="n-search-link"
21
+ href={url}
22
+ onClick={(e: Event) => {
23
+ onClick && onClick();
24
+ log(e);
25
+ }}
26
+ >
12
27
  {icon && (
13
28
  <span className="n-search-link-icon">
14
29
  <Icon name={icon} />
15
30
  </span>
16
31
  )}
17
32
  <div className="n-search-link-body">
18
-
19
33
  <p className="n-search-link-title">
20
- <span className="n-search-link-label">{label}</span> {resource && <span className="n-search-link-badge" data-resource={resource}>{resource}</span>}</p>
34
+ <span className="n-search-link-label">{label}</span>{' '}
35
+ {resource && (
36
+ <span className="n-search-link-badge" data-resource={resource}>
37
+ {resource}
38
+ </span>
39
+ )}
40
+ </p>
21
41
  {excerpt && (
22
42
  <p className="n-search-link-excerpt" dangerouslySetInnerHTML={{ __html: excerpt }} />
23
43
  )}
@@ -25,7 +45,7 @@ const SearchLink = ({ icon, label, url, excerpt, resource }: SearchLinkProps) =>
25
45
  <div className="n-search-link-icon">
26
46
  <Icon name="caretRight" />
27
47
  </div>
28
- </a>
48
+ </Component>
29
49
  );
30
50
  };
31
51