@netlify/spark-ui 1.9.1 → 1.11.0

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 (29) hide show
  1. package/dist/assets/AskNetlify.css +1 -1
  2. package/dist/chunks/{ChatInterface.BD7rlv4H.js → ChatInterface.EQAPs01R.js} +411 -392
  3. package/dist/components/preact/AskNetlify/AskNetlify.d.ts +3 -2
  4. package/dist/components/preact/AskNetlify/AskNetlify.js +32 -31
  5. package/dist/components/preact/AskNetlify/components/ChatInterface.d.ts +1 -1
  6. package/dist/components/preact/AskNetlify/components/ChatInterface.js +1 -1
  7. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.d.ts +1 -0
  8. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +56 -43
  9. package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
  10. package/dist/components/preact/SearchModal/components/SearchWindow.js +26 -38
  11. package/dist/components/preact/SearchModal/types/search.types.d.ts +0 -3
  12. package/dist/utils/fetch-svg.d.ts +11 -0
  13. package/dist/utils/kebab.d.ts +17 -0
  14. package/dist/utils/types.d.ts +13 -0
  15. package/package.json +1 -1
  16. package/packages/components/Blockquote.astro +63 -10
  17. package/packages/components/TestimonialSlider.astro +110 -0
  18. package/packages/components/preact/AskNetlify/AskNetlify.css +2 -15
  19. package/packages/components/preact/AskNetlify/AskNetlify.tsx +11 -3
  20. package/packages/components/preact/AskNetlify/components/ChatInterface.tsx +1 -1
  21. package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +21 -4
  22. package/packages/components/preact/SearchModal/components/SearchWindow.tsx +0 -10
  23. package/packages/components/preact/SearchModal/types/search.types.tsx +0 -1
  24. package/packages/styles/animations.css +26 -0
  25. package/packages/styles/tokens.css +12 -0
  26. package/packages/utils/fetch-svg.ts +28 -0
  27. package/packages/utils/kebab.ts +22 -0
  28. package/packages/utils/types.ts +14 -0
  29. package/packages/components/Testimonial.astro +0 -0
@@ -1,3 +1,5 @@
1
+ @import url('../../../styles/animations.css');
2
+
1
3
  /* ----------------------------- */
2
4
  /* Layout Containers */
3
5
  /* ----------------------------- */
@@ -242,21 +244,6 @@
242
244
  /* ----------------------------- */
243
245
  /* Animations and Keyframes */
244
246
  /* ----------------------------- */
245
- @keyframes fadeIn {
246
- 0% {
247
- opacity: 0;
248
- }
249
- 100% {
250
- opacity: 1;
251
- }
252
- }
253
-
254
- @keyframes fadeInUp {
255
- to {
256
- opacity: 1;
257
- transform: translateY(0);
258
- }
259
- }
260
247
 
261
248
  @keyframes askEyeBlink {
262
249
  0%,
@@ -5,23 +5,31 @@ import ChatInterface from './components/ChatInterface';
5
5
  import './AskNetlify.css';
6
6
 
7
7
  interface AskNetlifyProps {
8
- setModalState?: (state?: { current: string }) => void;
8
+ kapaIntegrationId?: string;
9
+ setModalState?: (e: { current: string }) => void;
9
10
  }
10
11
 
11
12
  export default function AskNetlify(props: AskNetlifyProps) {
12
13
  const analyticsRef = useRef<Analytics>(new Analytics());
14
+
15
+ if (!props.kapaIntegrationId) return;
16
+
13
17
  return (
14
18
  <KapaProvider
15
- integrationId="3c0789a6-c6a8-4fbc-b0cc-235f9e4a6351"
19
+ integrationId={props.kapaIntegrationId}
16
20
  callbacks={{
17
21
  askAI: {
18
22
  onQuerySubmit: data => {
23
+ if (!data.question || !data.threadId) return;
24
+
19
25
  analyticsRef?.current?.track('ask:new_query', {
20
26
  question: data.question,
21
27
  thread_id: data.threadId,
22
28
  });
23
29
  },
24
30
  onAnswerGenerationCompleted: data => {
31
+ if (!data.questionAnswerId || !data.answer) return;
32
+
25
33
  analyticsRef?.current?.track('ask:answer_generated', {
26
34
  question_id: data.questionAnswerId,
27
35
  answer_length: data.answer.length,
@@ -30,7 +38,7 @@ export default function AskNetlify(props: AskNetlifyProps) {
30
38
  },
31
39
  }}
32
40
  >
33
- <ChatInterface {...props} />
41
+ <ChatInterface setModalState={props.setModalState} />
34
42
  </KapaProvider>
35
43
  );
36
44
  }
@@ -6,7 +6,7 @@ import { BackButton } from './BackButton';
6
6
  import { ChatMessages } from './ChatMessages';
7
7
 
8
8
  interface Props {
9
- setModalState?: (state?: { current: string }) => void;
9
+ setModalState?: (e: { current: string }) => void;
10
10
  }
11
11
 
12
12
  export default function ChatInterface({ setModalState }: Props) {
@@ -5,6 +5,7 @@ import Modal from '../../../../preact/Modal/Modal';
5
5
  import KeyboardKey from '../../../../preact/KeyboardKey/KeyboardKey';
6
6
  import SearchWindow from '../../components/SearchWindow';
7
7
  import SearchInput from '../../components/SearchInput';
8
+ import SearchLink from '../../components/SearchLink/SearchLink';
8
9
  import { useClickOutside } from '../../../../../utils/useClickOutside';
9
10
  import {
10
11
  useSearch,
@@ -23,9 +24,12 @@ interface Props {
23
24
  children: ComponentChildren;
24
25
  className?: string;
25
26
  debug?: boolean;
27
+ kapaIntegrationId?: string;
26
28
  }
27
29
 
28
- function SearchModal(props: Props = { className: '', children: null, debug: false }) {
30
+ function SearchModal(
31
+ props: Props = { className: '', children: undefined, debug: false, kapaIntegrationId: undefined }
32
+ ) {
29
33
  const [isPagefindLoaded, setIsPagefindLoaded] = useState(false);
30
34
  const [modalState, setModalState] = useState({ current: 'search' });
31
35
 
@@ -93,18 +97,31 @@ function SearchModal(props: Props = { className: '', children: null, debug: fals
93
97
  {...rest}
94
98
  >
95
99
  <div ref={containerRef} className="n-search-container">
96
- {modalState.current === 'ask' && <AskNetlify setModalState={setModalState} />}
100
+ {modalState.current === 'ask' && (
101
+ <AskNetlify setModalState={setModalState} kapaIntegrationId={props.kapaIntegrationId} />
102
+ )}
97
103
  {modalState.current === 'search' && (
98
104
  <>
99
105
  <SearchInput setSearchTerm={actions.setSearchTerm} initialValue={state.searchTerm} />
100
106
 
101
107
  <SearchWindow
102
- setModalState={setModalState}
103
108
  searchLoading={state.isLoading}
104
109
  searchTerm={state.searchTerm}
105
110
  results={state.results}
106
111
  >
107
- {children}
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
+ </>
108
125
  </SearchWindow>
109
126
 
110
127
  <div className="n-search-footer">
@@ -45,7 +45,6 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({
45
45
 
46
46
  /* --- Main Search Results Component --- */
47
47
  export default function SearchWindow({
48
- setModalState,
49
48
  searchLoading,
50
49
  searchTerm,
51
50
  results,
@@ -58,15 +57,6 @@ export default function SearchWindow({
58
57
  <SearchResults searchLoading={searchLoading} searchTerm={searchTerm} results={results} />
59
58
  ) : DefaultWindow ? (
60
59
  <>
61
- <SearchLink
62
- as="button"
63
- icon="askNetlify"
64
- label="Ask Netlify AI"
65
- onClick={() => {
66
- setModalState({ current: 'ask' });
67
- }}
68
- />
69
-
70
60
  {DefaultWindow}
71
61
 
72
62
  <SearchHeading>Actions</SearchHeading>
@@ -12,7 +12,6 @@ export interface SearchLinkProps {
12
12
  }
13
13
 
14
14
  export interface SearchWindowProps {
15
- setModalState: (e: { current: string }) => void;
16
15
  searchLoading: boolean;
17
16
  searchTerm?: string;
18
17
  results?: SearchLinkProps[];
@@ -0,0 +1,26 @@
1
+ .n-fadeIn {
2
+ opacity: 0;
3
+ animation: fadeIn var(--animation-duration, 300ms) ease-out forwards;
4
+ }
5
+
6
+ .n-fadeInUp {
7
+ opacity: 0;
8
+ transform: translateY(var(--animation-transformY, 5px));
9
+ animation: fadeInUp var(--animation-duration, 300ms) ease-out forwards;
10
+ }
11
+
12
+ @keyframes fadeIn {
13
+ 0% {
14
+ opacity: 0;
15
+ }
16
+ 100% {
17
+ opacity: 1;
18
+ }
19
+ }
20
+
21
+ @keyframes fadeInUp {
22
+ to {
23
+ opacity: 1;
24
+ transform: translateY(0);
25
+ }
26
+ }
@@ -7,6 +7,7 @@
7
7
  --badge-bg-color: var(--neutral-100);
8
8
  --badge-border-color: var(--neutral-300);
9
9
  --badge-text-color: var(--neutral-500);
10
+ --blockquote-accent-color: var(--blue-700);
10
11
  --blue-000: color(srgb 0.9294117647058824 0.9568627450980393 1);
11
12
  --blue-100: color(srgb 0.803921568627451 0.8862745098039215 1);
12
13
  --blue-200: color(srgb 0.7098039215686275 0.8235294117647058 0.984313725490196);
@@ -196,6 +197,8 @@
196
197
  --teal-700: color(srgb 0.00392156862745098 0.4117647058823529 0.40784313725490196);
197
198
  --teal-800: color(srgb 0.00392156862745098 0.2823529411764706 0.2784313725490196);
198
199
  --teal-900: color(srgb 0.047058823529411764 0.16470588235294117 0.16470588235294117);
200
+ --testimonial-logo-hover-color: var(--neutral-500);
201
+ --testimonial-logo-inactive-color: var(--neutral-400);
199
202
  --title-text-color: color(srgb 0.32941176470588235 0.35294117647058826 0.3803921568627451 / 0.7490196078431373);
200
203
  }
201
204
 
@@ -205,6 +208,7 @@
205
208
  --badge-bg-color: var(--neutral-700);
206
209
  --badge-border-color: var(--neutral-600);
207
210
  --badge-text-color: var(--neutral-100);
211
+ --blockquote-accent-color: var(--teal-200);
208
212
  --button-outline-bg-color: color(srgb 0 0 0 / 0);
209
213
  --button-outline-bg-color-hover: color(srgb 0.6509803921568628 1 0.9921568627450981 / 0.09803921568627451);
210
214
  --button-outline-stroke-color: var(--teal-200);
@@ -277,6 +281,8 @@
277
281
  --tabbed-content-border-color: var(--color-icon);
278
282
  --tabbed-content-code-bg-color: var(--neutral-900);
279
283
  --tabbed-content-tab-text-color-active: var(--color-icon);
284
+ --testimonial-logo-hover-color: var(--neutral-300);
285
+ --testimonial-logo-inactive-color: var(--neutral-400);
280
286
  --title-text-color: color(srgb 0.8352941176470589 1 0.996078431372549 / 0.7490196078431373);
281
287
  }
282
288
  }
@@ -286,6 +292,7 @@
286
292
  --badge-bg-color: var(--neutral-700);
287
293
  --badge-border-color: var(--neutral-600);
288
294
  --badge-text-color: var(--neutral-100);
295
+ --blockquote-accent-color: var(--teal-200);
289
296
  --button-outline-bg-color: color(srgb 0 0 0 / 0);
290
297
  --button-outline-bg-color-hover: color(srgb 0.6509803921568628 1 0.9921568627450981 / 0.09803921568627451);
291
298
  --button-outline-stroke-color: var(--teal-200);
@@ -358,6 +365,8 @@
358
365
  --tabbed-content-border-color: var(--color-icon);
359
366
  --tabbed-content-code-bg-color: var(--neutral-900);
360
367
  --tabbed-content-tab-text-color-active: var(--color-icon);
368
+ --testimonial-logo-hover-color: var(--neutral-300);
369
+ --testimonial-logo-inactive-color: var(--neutral-400);
361
370
  --title-text-color: color(srgb 0.8352941176470589 1 0.996078431372549 / 0.7490196078431373);
362
371
  }
363
372
 
@@ -366,6 +375,7 @@
366
375
  --badge-bg-color: var(--neutral-100);
367
376
  --badge-border-color: var(--neutral-300);
368
377
  --badge-text-color: var(--neutral-500);
378
+ --blockquote-accent-color: var(--blue-700);
369
379
  --button-outline-bg-color: color(srgb 0 0 0 / 0);
370
380
  --button-outline-bg-color-hover: color(srgb 0.19607843137254902 0.9019607843137255 0.8862745098039215 / 0.2);
371
381
  --button-outline-stroke-color: var(--neutral-800);
@@ -437,6 +447,8 @@
437
447
  --tabbed-content-border-color: var(--color-icon);
438
448
  --tabbed-content-code-bg-color: var(--neutral-900);
439
449
  --tabbed-content-tab-text-color-active: var(--color-icon);
450
+ --testimonial-logo-hover-color: var(--neutral-500);
451
+ --testimonial-logo-inactive-color: var(--neutral-400);
440
452
  --title-text-color: color(srgb 0.32941176470588235 0.35294117647058826 0.3803921568627451 / 0.7490196078431373);
441
453
  }
442
454
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Fetches an SVG from a remote URL and strips any `fill` attributes
3
+ * to allow styling the SVG via CSS (e.g., using `currentColor` or masks).
4
+ *
5
+ * If the fetch fails or the response is not OK, logs the error and returns an empty string.
6
+ *
7
+ * @param {string} url - The URL of the SVG to fetch.
8
+ * @returns {Promise<string>} The cleaned SVG markup with `fill` attributes removed,
9
+ * or an empty string if the fetch fails.
10
+ */
11
+ export async function fetchSvg(url: string): Promise<string> {
12
+ function stripFillAttributes(svg: string): string {
13
+ return svg.replace(/fill\s*=\s*["'][^"']*["']/gi, '');
14
+ }
15
+
16
+ try {
17
+ const res = await fetch(url);
18
+ if (!res.ok) {
19
+ console.error(`[fetchSvg] Failed to fetch: ${url}`);
20
+ return '';
21
+ }
22
+ const raw = await res.text();
23
+ return stripFillAttributes(raw);
24
+ } catch (err) {
25
+ console.error(`[fetchSvg] Error fetching ${url}:`, err);
26
+ return '';
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Converts a given string to kebab-case.
3
+ *
4
+ * - Converts camelCase to kebab-case.
5
+ * - Replaces spaces and underscores with dashes.
6
+ * - Removes all non-alphanumeric characters except dashes.
7
+ * - Converts the entire string to lowercase.
8
+ *
9
+ * @param {string} str - The input string to convert.
10
+ * @returns {string} The kebab-cased version of the input string.
11
+ *
12
+ * @example
13
+ * kebabCase('HelloWorld') // 'hello-world'
14
+ * kebabCase('some_text Here') // 'some-text-here'
15
+ * kebabCase('already-kebab-case') // 'already-kebab-case'
16
+ */
17
+ export const kebabCase = (str: string) =>
18
+ str
19
+ .replace(/([a-z])([A-Z])/g, '$1-$2') // camelCase → kebab-case
20
+ .replace(/[\s_]+/g, '-') // spaces & underscores → dashes
21
+ .replace(/[^a-zA-Z0-9-]/g, '') // remove non-alphanumeric except dash
22
+ .toLowerCase();
@@ -14,3 +14,17 @@ export interface ImageProps {
14
14
  width: string;
15
15
  height: string;
16
16
  }
17
+
18
+ export interface Testimonial {
19
+ quote?: string;
20
+ author?: string;
21
+ title?: string;
22
+ company?: string;
23
+ logoSrc?: string;
24
+ avatarSrc?: string;
25
+ cta?: {
26
+ id?: string;
27
+ url?: string;
28
+ label?: string;
29
+ };
30
+ }
File without changes