@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.
- package/dist/assets/AskNetlify.css +1 -1
- package/dist/chunks/{ChatInterface.BD7rlv4H.js → ChatInterface.EQAPs01R.js} +411 -392
- package/dist/components/preact/AskNetlify/AskNetlify.d.ts +3 -2
- package/dist/components/preact/AskNetlify/AskNetlify.js +32 -31
- package/dist/components/preact/AskNetlify/components/ChatInterface.d.ts +1 -1
- package/dist/components/preact/AskNetlify/components/ChatInterface.js +1 -1
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.d.ts +1 -0
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +56 -43
- package/dist/components/preact/SearchModal/components/SearchWindow.d.ts +1 -1
- package/dist/components/preact/SearchModal/components/SearchWindow.js +26 -38
- package/dist/components/preact/SearchModal/types/search.types.d.ts +0 -3
- package/dist/utils/fetch-svg.d.ts +11 -0
- package/dist/utils/kebab.d.ts +17 -0
- package/dist/utils/types.d.ts +13 -0
- package/package.json +1 -1
- package/packages/components/Blockquote.astro +63 -10
- package/packages/components/TestimonialSlider.astro +110 -0
- package/packages/components/preact/AskNetlify/AskNetlify.css +2 -15
- package/packages/components/preact/AskNetlify/AskNetlify.tsx +11 -3
- package/packages/components/preact/AskNetlify/components/ChatInterface.tsx +1 -1
- package/packages/components/preact/SearchModal/components/SearchModal/SearchModal.tsx +21 -4
- package/packages/components/preact/SearchModal/components/SearchWindow.tsx +0 -10
- package/packages/components/preact/SearchModal/types/search.types.tsx +0 -1
- package/packages/styles/animations.css +26 -0
- package/packages/styles/tokens.css +12 -0
- package/packages/utils/fetch-svg.ts +28 -0
- package/packages/utils/kebab.ts +22 -0
- package/packages/utils/types.ts +14 -0
- package/packages/components/Testimonial.astro +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
interface AskNetlifyProps {
|
|
2
|
-
|
|
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
|
|
2
|
-
import { A as
|
|
3
|
-
import { C as s, K as
|
|
4
|
-
import { A as
|
|
5
|
-
import '../../../assets/AskNetlify.css';function
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
question: r.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
);
|
|
29
|
+
},
|
|
30
|
+
children: /* @__PURE__ */ o(s, { setModalState: n.setModalState })
|
|
31
|
+
}
|
|
32
|
+
);
|
|
32
33
|
}
|
|
33
34
|
export {
|
|
34
|
-
|
|
35
|
+
w as default
|
|
35
36
|
};
|
|
@@ -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.
|
|
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";
|
|
@@ -1,67 +1,80 @@
|
|
|
1
1
|
import { u as e } from "../../../../../chunks/jsxRuntime.module.Bzuv3cXw.js";
|
|
2
|
-
import { A as
|
|
3
|
-
import
|
|
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
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
13
|
-
import
|
|
14
|
-
import { Fragment as
|
|
15
|
-
import '../../../../../assets/SearchModal.css';function
|
|
16
|
-
const r =
|
|
17
|
-
return
|
|
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) &&
|
|
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
|
-
}, [
|
|
25
|
+
}, [s]), r;
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
-
const [r, i] = h(!1), [t,
|
|
28
|
-
|
|
29
|
-
let
|
|
30
|
-
const
|
|
31
|
-
window.pagefind && (i(!0),
|
|
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
|
|
34
|
+
return c(), n = setInterval(c, 100), () => clearInterval(n);
|
|
34
35
|
}, []);
|
|
35
|
-
const { children:
|
|
36
|
-
return
|
|
37
|
-
a.current && (
|
|
38
|
-
}, [
|
|
39
|
-
var
|
|
40
|
-
const
|
|
41
|
-
|
|
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
|
-
|
|
48
|
+
b,
|
|
48
49
|
{
|
|
49
50
|
ref: a,
|
|
50
|
-
className: `n-search-modal ${
|
|
51
|
+
className: `n-search-modal ${k || ""}`,
|
|
51
52
|
"data-state": t.current,
|
|
52
|
-
...
|
|
53
|
-
children: /* @__PURE__ */ e("div", { ref:
|
|
54
|
-
t.current === "ask" && /* @__PURE__ */ e(
|
|
55
|
-
t.current === "search" && /* @__PURE__ */ e(
|
|
56
|
-
/* @__PURE__ */ e(
|
|
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
|
-
|
|
59
|
+
w,
|
|
59
60
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
98
|
+
Z as default
|
|
86
99
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { SearchWindowProps } from '../types/search.types';
|
|
2
|
-
export default function SearchWindow({
|
|
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
|
|
3
|
-
import
|
|
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
|
|
6
|
-
const
|
|
5
|
+
import { Fragment as h } from "preact";
|
|
6
|
+
const d = {
|
|
7
7
|
light: "dark",
|
|
8
8
|
dark: "light"
|
|
9
|
-
},
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
},
|
|
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:
|
|
15
|
-
results:
|
|
14
|
+
searchTerm: t,
|
|
15
|
+
results: r
|
|
16
16
|
}) => {
|
|
17
17
|
if (n)
|
|
18
|
-
return /* @__PURE__ */ e(
|
|
18
|
+
return /* @__PURE__ */ e(m, { children: [
|
|
19
19
|
"Searching for ",
|
|
20
|
-
/* @__PURE__ */ e("span", { className: "n-search-term", children:
|
|
20
|
+
/* @__PURE__ */ e("span", { className: "n-search-term", children: t }),
|
|
21
21
|
" ..."
|
|
22
22
|
] });
|
|
23
|
-
if (!
|
|
24
|
-
return /* @__PURE__ */ e(
|
|
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:
|
|
26
|
+
/* @__PURE__ */ e("span", { className: "n-search-term", children: t })
|
|
27
27
|
] });
|
|
28
|
-
const c =
|
|
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
|
|
32
|
-
|
|
33
|
-
searchLoading: r,
|
|
31
|
+
function N({
|
|
32
|
+
searchLoading: n,
|
|
34
33
|
searchTerm: t,
|
|
35
|
-
results:
|
|
36
|
-
children:
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
60
|
-
|
|
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
|
-
|
|
55
|
+
N as default
|
|
68
56
|
};
|
|
@@ -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;
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -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
|
@@ -3,10 +3,20 @@ interface Props {
|
|
|
3
3
|
quote?: string;
|
|
4
4
|
author?: string;
|
|
5
5
|
title?: string;
|
|
6
|
-
|
|
6
|
+
avatarSrc?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
variant?: 'default' | 'simple' | 'testimonial';
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
const {
|
|
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
|
-
{
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
.n-blockquote-author-info {
|
|
79
94
|
flex-direction: column;
|
|
80
95
|
font-weight: var(--font-weight-regular);
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
.n-blockquote-author-info > * {
|
|
96
111
|
font-size: var(--font-size-text-sm);
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
|
|
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>
|