@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,7 +1,8 @@
1
1
  interface AskNetlifyProps {
2
- setModalState?: (state?: {
2
+ kapaIntegrationId?: string;
3
+ setModalState?: (e: {
3
4
  current: string;
4
5
  }) => void;
5
6
  }
6
- export default function AskNetlify(props: AskNetlifyProps): import("preact").JSX.Element;
7
+ export default function AskNetlify(props: AskNetlifyProps): import("preact").JSX.Element | undefined;
7
8
  export {};
@@ -1,35 +1,36 @@
1
- import { u as t } from "../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
- import { A as a } from "../../../chunks/hooks.module.DGkdD5n9.js";
3
- import { C as s, K as i } from "../../../chunks/ChatInterface.BD7rlv4H.js";
4
- import { A as c } from "../../../chunks/analytics.Ch1vuuOl.js";
5
- import '../../../assets/AskNetlify.css';function k(o) {
6
- const e = a(new c());
7
- return /* @__PURE__ */ t(
8
- i,
9
- {
10
- integrationId: "3c0789a6-c6a8-4fbc-b0cc-235f9e4a6351",
11
- callbacks: {
12
- askAI: {
13
- onQuerySubmit: (r) => {
14
- var n;
15
- (n = e == null ? void 0 : e.current) == null || n.track("ask:new_query", {
16
- question: r.question,
17
- thread_id: r.threadId
18
- });
19
- },
20
- onAnswerGenerationCompleted: (r) => {
21
- var n;
22
- (n = e == null ? void 0 : e.current) == null || n.track("ask:answer_generated", {
23
- question_id: r.questionAnswerId,
24
- answer_length: r.answer.length
25
- });
1
+ import { u as o } from "../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
+ import { A as i } from "../../../chunks/hooks.module.DGkdD5n9.js";
3
+ import { C as s, K as a } from "../../../chunks/ChatInterface.EQAPs01R.js";
4
+ import { A as u } from "../../../chunks/analytics.Ch1vuuOl.js";
5
+ import '../../../assets/AskNetlify.css';function w(n) {
6
+ const r = i(new u());
7
+ if (n.kapaIntegrationId)
8
+ return /* @__PURE__ */ o(
9
+ a,
10
+ {
11
+ integrationId: n.kapaIntegrationId,
12
+ callbacks: {
13
+ askAI: {
14
+ onQuerySubmit: (e) => {
15
+ var t;
16
+ !e.question || !e.threadId || (t = r == null ? void 0 : r.current) == null || t.track("ask:new_query", {
17
+ question: e.question,
18
+ thread_id: e.threadId
19
+ });
20
+ },
21
+ onAnswerGenerationCompleted: (e) => {
22
+ var t;
23
+ !e.questionAnswerId || !e.answer || (t = r == null ? void 0 : r.current) == null || t.track("ask:answer_generated", {
24
+ question_id: e.questionAnswerId,
25
+ answer_length: e.answer.length
26
+ });
27
+ }
26
28
  }
27
- }
28
- },
29
- children: /* @__PURE__ */ t(s, { ...o })
30
- }
31
- );
29
+ },
30
+ children: /* @__PURE__ */ o(s, { setModalState: n.setModalState })
31
+ }
32
+ );
32
33
  }
33
34
  export {
34
- k as default
35
+ w as default
35
36
  };
@@ -1,5 +1,5 @@
1
1
  interface Props {
2
- setModalState?: (state?: {
2
+ setModalState?: (e: {
3
3
  current: string;
4
4
  }) => void;
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import "../../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
2
  import "../../../../chunks/hooks.module.DGkdD5n9.js";
3
- import { C as f } from "../../../../chunks/ChatInterface.BD7rlv4H.js";
3
+ import { C as f } from "../../../../chunks/ChatInterface.EQAPs01R.js";
4
4
  import "./QuestionList/questions.js";
5
5
  import "./QuestionList/QuestionList.js";
6
6
  import "./BackButton.js";
@@ -3,6 +3,7 @@ interface Props {
3
3
  children: ComponentChildren;
4
4
  className?: string;
5
5
  debug?: boolean;
6
+ kapaIntegrationId?: string;
6
7
  }
7
8
  declare function SearchModal(props?: Props): import("preact").JSX.Element;
8
9
  export default SearchModal;
@@ -1,67 +1,80 @@
1
1
  import { u as e } from "../../../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
- import { A as N, y as l, d as h } from "../../../../../chunks/hooks.module.DGkdD5n9.js";
3
- import w from "../../../Modal/Modal.js";
2
+ import { A as v, y as m, d as h } from "../../../../../chunks/hooks.module.DGkdD5n9.js";
3
+ import b from "../../../Modal/Modal.js";
4
4
  import p from "../../../KeyboardKey/KeyboardKey.js";
5
- import I from "../SearchWindow.js";
6
- import b from "../SearchInput.js";
7
- import { useSearch as M } from "../../hooks/useSearch.js";
8
- import { useSearchLogger as O } from "../../hooks/useSearchLogger.js";
9
- import { useKeyboardNav as P } from "../../hooks/useKeyboardNav.js";
5
+ import w from "../SearchWindow.js";
6
+ import A from "../SearchInput.js";
7
+ import M from "../SearchLink/SearchLink.js";
8
+ import { useSearch as O } from "../../hooks/useSearch.js";
9
+ import { useSearchLogger as P } from "../../hooks/useSearchLogger.js";
10
+ import { useKeyboardNav as C } from "../../hooks/useKeyboardNav.js";
10
11
  import { useSearchHotkeys as E } from "../../hooks/useSearchHotkeys.js";
11
12
  import { useSearchParams as K } from "../../hooks/useSearchParams.js";
12
- import { useSearchToggles as A } from "../../hooks/useSearchToggles.js";
13
- import C from "../../../AskNetlify/AskNetlify.js";
14
- import { Fragment as F } from "preact";
15
- import '../../../../../assets/SearchModal.css';function R(c) {
16
- const r = N(null);
17
- return l(() => {
13
+ import { useSearchToggles as F } from "../../hooks/useSearchToggles.js";
14
+ import R from "../../../AskNetlify/AskNetlify.js";
15
+ import { Fragment as I } from "preact";
16
+ import '../../../../../assets/SearchModal.css';function q(s) {
17
+ const r = v(null);
18
+ return m(() => {
18
19
  function i(t) {
19
- r.current && !r.current.contains(t.target) && c();
20
+ r.current && !r.current.contains(t.target) && s();
20
21
  }
21
22
  return document.addEventListener("mousedown", i), () => {
22
23
  document.removeEventListener("mousedown", i);
23
24
  };
24
- }, [c]), r;
25
+ }, [s]), r;
25
26
  }
26
- function X(c = { className: "", children: null, debug: !1 }) {
27
- const [r, i] = h(!1), [t, u] = h({ current: "search" });
28
- l(() => {
29
- let s;
30
- const o = () => {
31
- window.pagefind && (i(!0), s && clearInterval(s));
27
+ function Z(s = { className: "", children: void 0, debug: !1, kapaIntegrationId: void 0 }) {
28
+ const [r, i] = h(!1), [t, d] = h({ current: "search" });
29
+ m(() => {
30
+ let n;
31
+ const c = () => {
32
+ window.pagefind && (i(!0), n && clearInterval(n));
32
33
  };
33
- return o(), s = setInterval(o, 100), () => clearInterval(s);
34
+ return c(), n = setInterval(c, 100), () => clearInterval(n);
34
35
  }, []);
35
- const { children: k, className: S, debug: y, ...T } = c, { state: n, actions: g } = M(), [m, v] = h(!1), d = () => v(!0), f = () => v(!1), a = N(null), L = R(f);
36
- return O(n, y), A(r, d, u), K(r, g.setSearchTerm, d), E(r, m, d, f), P(r, a, m, f), l(() => {
37
- a.current && (m ? a.current.showModal() : a.current.close());
38
- }, [m]), l(() => {
39
- var o;
40
- const s = (o = a.current) == null ? void 0 : o.querySelector("input");
41
- s && s.focus(), t.current === "ask" ? setTimeout(() => {
36
+ const { children: N, className: k, debug: y, ...L } = s, { state: o, actions: g } = O(), [l, S] = h(!1), u = () => S(!0), f = () => S(!1), a = v(null), T = q(f);
37
+ return P(o, y), F(r, u, d), K(r, g.setSearchTerm, u), E(r, l, u, f), C(r, a, l, f), m(() => {
38
+ a.current && (l ? a.current.showModal() : a.current.close());
39
+ }, [l]), m(() => {
40
+ var c;
41
+ const n = (c = a.current) == null ? void 0 : c.querySelector("input");
42
+ n && n.focus(), t.current === "ask" ? setTimeout(() => {
42
43
  a.current.classList.add("n-search-modal-ask");
43
44
  }, 150) : t.current === "search" && setTimeout(() => {
44
45
  a.current.classList.remove("n-search-modal-ask");
45
46
  }, 150);
46
47
  }, [t.current]), /* @__PURE__ */ e(
47
- w,
48
+ b,
48
49
  {
49
50
  ref: a,
50
- className: `n-search-modal ${S || ""}`,
51
+ className: `n-search-modal ${k || ""}`,
51
52
  "data-state": t.current,
52
- ...T,
53
- children: /* @__PURE__ */ e("div", { ref: L, className: "n-search-container", children: [
54
- t.current === "ask" && /* @__PURE__ */ e(C, { setModalState: u }),
55
- t.current === "search" && /* @__PURE__ */ e(F, { children: [
56
- /* @__PURE__ */ e(b, { setSearchTerm: g.setSearchTerm, initialValue: n.searchTerm }),
53
+ ...L,
54
+ children: /* @__PURE__ */ e("div", { ref: T, className: "n-search-container", children: [
55
+ t.current === "ask" && /* @__PURE__ */ e(R, { setModalState: d, kapaIntegrationId: s.kapaIntegrationId }),
56
+ t.current === "search" && /* @__PURE__ */ e(I, { children: [
57
+ /* @__PURE__ */ e(A, { setSearchTerm: g.setSearchTerm, initialValue: o.searchTerm }),
57
58
  /* @__PURE__ */ e(
58
- I,
59
+ w,
59
60
  {
60
- setModalState: u,
61
- searchLoading: n.isLoading,
62
- searchTerm: n.searchTerm,
63
- results: n.results,
64
- children: k
61
+ searchLoading: o.isLoading,
62
+ searchTerm: o.searchTerm,
63
+ results: o.results,
64
+ children: /* @__PURE__ */ e(I, { children: [
65
+ s.kapaIntegrationId && /* @__PURE__ */ e(
66
+ M,
67
+ {
68
+ as: "button",
69
+ icon: "askNetlify",
70
+ label: "Ask Netlify AI",
71
+ onClick: () => {
72
+ d({ current: "ask" });
73
+ }
74
+ }
75
+ ),
76
+ N
77
+ ] })
65
78
  }
66
79
  ),
67
80
  /* @__PURE__ */ e("div", { className: "n-search-footer", children: [
@@ -82,5 +95,5 @@ function X(c = { className: "", children: null, debug: !1 }) {
82
95
  );
83
96
  }
84
97
  export {
85
- X as default
98
+ Z as default
86
99
  };
@@ -1,2 +1,2 @@
1
1
  import { SearchWindowProps } from '../types/search.types';
2
- export default function SearchWindow({ setModalState, searchLoading, searchTerm, results, children: DefaultWindow, }: SearchWindowProps): import("preact").JSX.Element;
2
+ export default function SearchWindow({ searchLoading, searchTerm, results, children: DefaultWindow, }: SearchWindowProps): import("preact").JSX.Element;
@@ -1,54 +1,42 @@
1
1
  import { u as e } from "../../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
2
- import { T as h } from "../../../../chunks/hooks.module.DGkdD5n9.js";
3
- import s from "./SearchHeading/SearchHeading.js";
2
+ import { T as o } from "../../../../chunks/hooks.module.DGkdD5n9.js";
3
+ import m from "./SearchHeading/SearchHeading.js";
4
4
  import a from "./SearchLink/SearchLink.js";
5
- import { Fragment as d } from "preact";
6
- const u = {
5
+ import { Fragment as h } from "preact";
6
+ const d = {
7
7
  light: "dark",
8
8
  dark: "light"
9
- }, g = (n) => {
10
- const r = document.getElementById("site-theme-select");
11
- r && (r.value = n), document.documentElement.setAttribute("data-theme", n), localStorage.setItem("theme", n);
12
- }, f = ({
9
+ }, u = (n) => {
10
+ const t = document.getElementById("site-theme-select");
11
+ t && (t.value = n), document.documentElement.setAttribute("data-theme", n), localStorage.setItem("theme", n);
12
+ }, g = ({
13
13
  searchLoading: n,
14
- searchTerm: r,
15
- results: t
14
+ searchTerm: t,
15
+ results: r
16
16
  }) => {
17
17
  if (n)
18
- return /* @__PURE__ */ e(s, { children: [
18
+ return /* @__PURE__ */ e(m, { children: [
19
19
  "Searching for ",
20
- /* @__PURE__ */ e("span", { className: "n-search-term", children: r }),
20
+ /* @__PURE__ */ e("span", { className: "n-search-term", children: t }),
21
21
  " ..."
22
22
  ] });
23
- if (!t || t.length === 0)
24
- return /* @__PURE__ */ e(s, { children: [
23
+ if (!r || r.length === 0)
24
+ return /* @__PURE__ */ e(m, { children: [
25
25
  "0 results for ",
26
- /* @__PURE__ */ e("span", { className: "n-search-term", children: r })
26
+ /* @__PURE__ */ e("span", { className: "n-search-term", children: t })
27
27
  ] });
28
- const c = h(() => t.map((l, i) => /* @__PURE__ */ e("li", { className: "n-results-list-item", children: /* @__PURE__ */ e(a, { ...l }) }, i)), [t]);
28
+ const c = o(() => r.map((i, l) => /* @__PURE__ */ e("li", { className: "n-results-list-item", children: /* @__PURE__ */ e(a, { ...i }) }, l)), [r]);
29
29
  return /* @__PURE__ */ e("ul", { className: "n-results-list", role: "list", children: c });
30
30
  };
31
- function A({
32
- setModalState: n,
33
- searchLoading: r,
31
+ function N({
32
+ searchLoading: n,
34
33
  searchTerm: t,
35
- results: c,
36
- children: l
34
+ results: r,
35
+ children: c
37
36
  }) {
38
- return /* @__PURE__ */ e("div", { className: "n-search-window", children: /* @__PURE__ */ e("div", { className: "n-search-links", children: t && t.length > 1 ? /* @__PURE__ */ e(f, { searchLoading: r, searchTerm: t, results: c }) : l ? /* @__PURE__ */ e(d, { children: [
39
- /* @__PURE__ */ e(
40
- a,
41
- {
42
- as: "button",
43
- icon: "askNetlify",
44
- label: "Ask Netlify AI",
45
- onClick: () => {
46
- n({ current: "ask" });
47
- }
48
- }
49
- ),
50
- l,
51
- /* @__PURE__ */ e(s, { children: "Actions" }),
37
+ return /* @__PURE__ */ e("div", { className: "n-search-window", children: /* @__PURE__ */ e("div", { className: "n-search-links", children: t && t.length > 1 ? /* @__PURE__ */ e(g, { searchLoading: n, searchTerm: t, results: r }) : c ? /* @__PURE__ */ e(h, { children: [
38
+ c,
39
+ /* @__PURE__ */ e(m, { children: "Actions" }),
52
40
  /* @__PURE__ */ e(
53
41
  a,
54
42
  {
@@ -56,13 +44,13 @@ function A({
56
44
  icon: "lightbulb",
57
45
  label: "Toggle theme",
58
46
  onClick: () => {
59
- const i = window.matchMedia("(prefers-color-scheme: dark)").matches, m = document.documentElement.getAttribute("data-theme"), o = i ? "dark" : m || "light";
60
- o && g(u[o]);
47
+ const i = window.matchMedia("(prefers-color-scheme: dark)").matches, l = document.documentElement.getAttribute("data-theme"), s = i ? "dark" : l || "light";
48
+ s && u(d[s]);
61
49
  }
62
50
  }
63
51
  )
64
52
  ] }) : null }) });
65
53
  }
66
54
  export {
67
- A as default
55
+ N as default
68
56
  };
@@ -10,9 +10,6 @@ export interface SearchLinkProps {
10
10
  onClick?: () => void;
11
11
  }
12
12
  export interface SearchWindowProps {
13
- setModalState: (e: {
14
- current: string;
15
- }) => void;
16
13
  searchLoading: boolean;
17
14
  searchTerm?: string;
18
15
  results?: SearchLinkProps[];
@@ -0,0 +1,11 @@
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 declare function fetchSvg(url: string): Promise<string>;
@@ -0,0 +1,17 @@
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 declare const kebabCase: (str: string) => string;
@@ -12,3 +12,16 @@ export interface ImageProps {
12
12
  width: string;
13
13
  height: string;
14
14
  }
15
+ export interface Testimonial {
16
+ quote?: string;
17
+ author?: string;
18
+ title?: string;
19
+ company?: string;
20
+ logoSrc?: string;
21
+ avatarSrc?: string;
22
+ cta?: {
23
+ id?: string;
24
+ url?: string;
25
+ label?: string;
26
+ };
27
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@netlify/spark-ui",
3
3
  "description": "Assets, design tokens, components, and utilities",
4
- "version": "1.9.1",
4
+ "version": "1.11.0",
5
5
  "type": "module",
6
6
  "main": "dist/components/preact/index.js",
7
7
  "types": "dist/components/preact/index.d.ts",
@@ -3,10 +3,20 @@ interface Props {
3
3
  quote?: string;
4
4
  author?: string;
5
5
  title?: string;
6
- variant?: 'default' | 'simple';
6
+ avatarSrc?: string;
7
+ class?: string;
8
+ variant?: 'default' | 'simple' | 'testimonial';
7
9
  }
8
10
 
9
- const { quote, variant = 'default', author, title, class: className, ...rest } = Astro.props;
11
+ const {
12
+ quote,
13
+ variant = 'default',
14
+ author,
15
+ title,
16
+ avatarSrc,
17
+ class: className,
18
+ ...rest
19
+ } = Astro.props as Props;
10
20
 
11
21
  /**
12
22
  * If quote is added via <slot />, trim surrounding whitespace so that quotation marks properly wrap inline with the text string.
@@ -25,8 +35,11 @@ if (Astro.slots.has('default')) {
25
35
  {
26
36
  author || title ? (
27
37
  <figcaption>
28
- {author && <span>{author}</span>}
29
- {title && <span>{title}</span>}
38
+ {avatarSrc && <img src={avatarSrc} alt={`Avatar of ${author}`} />}
39
+ <div class="n-blockquote-author-info">
40
+ {author && <span class="n-blockquote-author">{author}</span>}
41
+ {title && <span class="n-blockquote-title">{title}</span>}
42
+ </div>
30
43
  </figcaption>
31
44
  ) : null
32
45
  }
@@ -60,13 +73,15 @@ if (Astro.slots.has('default')) {
60
73
 
61
74
  blockquote::before {
62
75
  content: open-quote;
76
+ color: var(--blockquote-accent-color);
63
77
  }
64
78
 
65
79
  blockquote::after {
66
80
  content: close-quote;
81
+ color: var(--blockquote-accent-color);
67
82
  }
68
83
 
69
- figcaption {
84
+ .n-blockquote-author-info {
70
85
  display: flex;
71
86
  gap: 0.2em;
72
87
  font-size: var(--font-size-text-xs);
@@ -75,29 +90,67 @@ if (Astro.slots.has('default')) {
75
90
 
76
91
  /* Variants */
77
92
  [data-blockquote-variant='default'] {
78
- figcaption {
93
+ .n-blockquote-author-info {
79
94
  flex-direction: column;
80
95
  font-weight: var(--font-weight-regular);
81
96
  }
82
97
 
83
- figcaption > * {
98
+ .n-blockquote-author-info > * {
84
99
  display: block;
85
100
  font-family: var(--font-family-mono);
86
101
  text-transform: uppercase;
87
102
  }
88
103
 
89
- figcaption > *:last-child:not(:only-child) {
104
+ .n-blockquote-author-info > *:last-child:not(:only-child) {
90
105
  color: var(--color-text-weak);
91
106
  }
92
107
  }
93
108
 
94
109
  [data-blockquote-variant='simple'] {
95
- figcaption > * {
110
+ .n-blockquote-author-info > * {
96
111
  font-size: var(--font-size-text-sm);
97
112
  }
98
113
 
99
- figcaption > *:not(:last-child)::after {
114
+ .n-blockquote-author-info > *:not(:last-child)::after {
100
115
  content: ',';
101
116
  }
102
117
  }
118
+
119
+ [data-blockquote-variant='testimonial'] {
120
+ border-inline-start: none;
121
+
122
+ font-size: var(--font-size-display-lg);
123
+ font-weight: var(--font-weight-bold);
124
+ line-height: var(--line-height-heading);
125
+
126
+ .n-blockquote-author-info {
127
+ flex-direction: column;
128
+ font-weight: var(--font-weight-regular);
129
+ font-family: var(--font-family-mono);
130
+ }
131
+
132
+ figcaption {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: var(--_gap);
136
+ }
137
+
138
+ figcaption img {
139
+ aspect-ratio: 1 / 1;
140
+ object-fit: cover;
141
+ width: 3.125rem;
142
+ height: auto;
143
+ clip-path: circle();
144
+ }
145
+
146
+ .n-blockquote-author-info {
147
+ font-size: var(--font-size-text-sm);
148
+ font-weight: var(--font-weight-regular);
149
+ text-transform: uppercase;
150
+ }
151
+
152
+ .n-blockquote-title {
153
+ color: var(--color-text-weak);
154
+ }
155
+ }
103
156
  </style>
@@ -0,0 +1,110 @@
1
+ ---
2
+ import { fetchSvg } from '../utils/fetch-svg.ts';
3
+ import { kebabCase } from '../utils/kebab';
4
+ import Blockquote from './Blockquote.astro';
5
+ import Button from './Button.astro';
6
+ import { Testimonial } from '../utils/types';
7
+
8
+ import Tabs from '@netlify/spark-ui/components/Tabs.astro';
9
+ import Tab from '@netlify/spark-ui/components/Tab.astro';
10
+ import TabList from '@netlify/spark-ui/components/TabList.astro';
11
+ import TabPanel from '@netlify/spark-ui/components/TabPanel.astro';
12
+
13
+ interface Props {
14
+ items?: Testimonial[];
15
+ }
16
+
17
+ const { items } = Astro.props as Props;
18
+ ---
19
+
20
+ <Tabs class="n-testimonial-slider">
21
+ <TabList class="n-testimonial-logos n-cluster">
22
+ {
23
+ items?.map(({ company, logoSrc }, index) => (
24
+ <Tab
25
+ class="n-logo-btn"
26
+ data-id={company && kebabCase(company)}
27
+ isSelected={index === 0}
28
+ aria-label={`Show testimonial from ${company}`}
29
+ >
30
+ {logoSrc && <slot set:html={fetchSvg(logoSrc)} />}
31
+ </Tab>
32
+ ))
33
+ }
34
+ </TabList>
35
+ {
36
+ items?.map((testimonial, index) => (
37
+ <TabPanel>
38
+ <div class="n-testimonial-quote-body n-flow">
39
+ <Blockquote
40
+ variant="testimonial"
41
+ aria-hidden={index !== 0}
42
+ class={`n-testimonial-quote n-fadeInUp ${index === 0 ? 'is-active' : ''}`}
43
+ quote={testimonial.quote}
44
+ author={testimonial.author}
45
+ title={testimonial.title}
46
+ avatarSrc={testimonial.avatarSrc}
47
+ />
48
+ {testimonial.cta && (
49
+ <Button
50
+ id={`cta-${testimonial.cta.id}`}
51
+ href={testimonial.cta.url}
52
+ class="n-testimonial-cta n-fadeIn"
53
+ >
54
+ {testimonial.cta.label}
55
+ </Button>
56
+ )}
57
+ </div>
58
+ </TabPanel>
59
+ ))
60
+ }
61
+ </Tabs>
62
+
63
+ <style>
64
+ @import url('../styles/animations.css');
65
+
66
+ .n-testimonial-slider {
67
+ --_spacing: var(--space-xl);
68
+ }
69
+
70
+ .n-testimonial-quote-body {
71
+ --n-flow-space: var(--_spacing);
72
+ }
73
+
74
+ .n-testimonial-logos {
75
+ --n-cluster-column-gap: var(--space-l);
76
+ }
77
+
78
+ [role='tablist'] {
79
+ margin-block-end: var(--_spacing);
80
+ }
81
+
82
+ .n-testimonial-cta {
83
+ width: fit-content;
84
+ animation-delay: 200ms;
85
+ }
86
+
87
+ .n-logo-btn {
88
+ background: none;
89
+ border: none;
90
+ cursor: pointer;
91
+ display: flex;
92
+ place-items: center;
93
+ outline-offset: 0.5rem;
94
+ }
95
+
96
+ .n-logo-btn :global(svg) {
97
+ width: 6.25rem;
98
+ height: auto;
99
+ fill: var(--testimonial-logo-inactive-color);
100
+ transition: fill 300ms ease-out;
101
+ }
102
+
103
+ .n-logo-btn:hover :global(svg) {
104
+ fill: var(--testimonial-logo-hover-color);
105
+ }
106
+
107
+ .n-logo-btn[aria-selected='true'] :global(svg) {
108
+ fill: var(--blockquote-accent-color);
109
+ }
110
+ </style>