cistack 6.1.0 → 6.2.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/package.json +1 -1
- package/product-site/.github/workflows/pipeline.yml +14 -1
- package/product-site/components/CopyButton.tsx +54 -38
- package/product-site/components/HomeClient.tsx +490 -811
- package/product-site/components/InstallToggle.tsx +70 -34
- package/product-site/components/TerminalCard.tsx +9 -5
- package/product-site/components/TerminalCardMotion.tsx +21 -14
- package/product-site/components/site-motion.tsx +229 -0
- package/product-site/dictionaries/br.json +246 -107
- package/product-site/dictionaries/cn.json +246 -107
- package/product-site/dictionaries/de.json +237 -98
- package/product-site/dictionaries/en.json +228 -91
- package/product-site/dictionaries/es.json +240 -146
- package/product-site/dictionaries/fr.json +237 -143
- package/product-site/dictionaries/pt.json +246 -107
- package/product-site/package.json +1 -0
- package/product-site/scripts/validate-i18n.mjs +45 -0
- package/src/index.js +12 -13
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { AnimatePresence, m, useReducedMotion } from "framer-motion";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
|
|
6
|
+
import CopyButton from "@/components/CopyButton";
|
|
7
|
+
import { Separator } from "@/components/ui/separator";
|
|
5
8
|
import type { Dictionary } from "@/lib/dictionary-types";
|
|
6
9
|
|
|
10
|
+
import { SITE_EASE } from "@/components/site-motion";
|
|
11
|
+
|
|
7
12
|
type InstallMode = "npx" | "npm";
|
|
8
13
|
|
|
14
|
+
const installCopy: Record<InstallMode, string> = {
|
|
15
|
+
npx: "npx cistack",
|
|
16
|
+
npm: "npm install -g cistack",
|
|
17
|
+
};
|
|
18
|
+
|
|
9
19
|
const installModes: Record<
|
|
10
20
|
InstallMode,
|
|
11
21
|
{
|
|
12
|
-
command: string;
|
|
13
22
|
badgeClassName: string;
|
|
14
23
|
badgeLabel: keyof Dictionary["install_toggle"];
|
|
15
24
|
description: keyof Dictionary["install_toggle"];
|
|
16
25
|
}
|
|
17
26
|
> = {
|
|
18
27
|
npx: {
|
|
19
|
-
|
|
20
|
-
badgeClassName: "border-emerald-500/20 bg-emerald-500/10 text-emerald-500",
|
|
28
|
+
badgeClassName: "border-emerald-200 bg-emerald-50 text-emerald-800",
|
|
21
29
|
badgeLabel: "recommended_badge",
|
|
22
30
|
description: "npx_desc",
|
|
23
31
|
},
|
|
24
32
|
npm: {
|
|
25
|
-
|
|
26
|
-
badgeClassName: "border-zinc-700 bg-zinc-500/10 text-zinc-400",
|
|
33
|
+
badgeClassName: "border-zinc-200 bg-zinc-50 text-zinc-700",
|
|
27
34
|
badgeLabel: "global_badge",
|
|
28
35
|
description: "npm_desc",
|
|
29
36
|
},
|
|
@@ -32,55 +39,84 @@ const installModes: Record<
|
|
|
32
39
|
export default function InstallToggle({ dict }: { dict: Dictionary }) {
|
|
33
40
|
const [mode, setMode] = useState<InstallMode>("npx");
|
|
34
41
|
const selectedMode = installModes[mode];
|
|
42
|
+
const command = installCopy[mode];
|
|
43
|
+
const reduce = useReducedMotion();
|
|
35
44
|
|
|
36
45
|
return (
|
|
37
|
-
<div className="flex flex-col gap-
|
|
38
|
-
<div className="flex items-center">
|
|
39
|
-
<button
|
|
46
|
+
<div className="flex flex-col gap-0">
|
|
47
|
+
<div className="flex flex-wrap items-center gap-3 pb-3">
|
|
48
|
+
<m.button
|
|
40
49
|
type="button"
|
|
41
50
|
onClick={() => setMode("npx")}
|
|
42
|
-
|
|
51
|
+
whileTap={reduce ? {} : { scale: 0.98 }}
|
|
52
|
+
className={`text-sm font-semibold transition-colors ${
|
|
43
53
|
mode === "npx" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
|
|
44
54
|
}`}
|
|
45
55
|
>
|
|
46
56
|
npx
|
|
47
57
|
{mode === "npx" && (
|
|
48
|
-
<span className="ml-1.5 text-
|
|
49
|
-
{
|
|
50
|
-
- {dict.install_toggle.recommended}
|
|
58
|
+
<span className="ml-1.5 text-xs font-normal text-zinc-500">
|
|
59
|
+
— {dict.install_toggle.recommended}
|
|
51
60
|
</span>
|
|
52
61
|
)}
|
|
53
|
-
</button>
|
|
54
|
-
<
|
|
55
|
-
<button
|
|
62
|
+
</m.button>
|
|
63
|
+
<Separator orientation="vertical" className="h-4 bg-zinc-200" />
|
|
64
|
+
<m.button
|
|
56
65
|
type="button"
|
|
57
66
|
onClick={() => setMode("npm")}
|
|
58
|
-
|
|
67
|
+
whileTap={reduce ? {} : { scale: 0.98 }}
|
|
68
|
+
className={`text-sm font-semibold transition-colors ${
|
|
59
69
|
mode === "npm" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
|
|
60
70
|
}`}
|
|
61
71
|
>
|
|
62
72
|
{dict.install_toggle.npm_global}
|
|
63
|
-
</button>
|
|
73
|
+
</m.button>
|
|
74
|
+
<m.span
|
|
75
|
+
layout
|
|
76
|
+
className={`ms-auto rounded-sm border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide ${selectedMode.badgeClassName}`}
|
|
77
|
+
transition={{ type: "spring", stiffness: 420, damping: 28 }}
|
|
78
|
+
>
|
|
79
|
+
{dict.install_toggle[selectedMode.badgeLabel]}
|
|
80
|
+
</m.span>
|
|
64
81
|
</div>
|
|
65
82
|
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
<Separator className="bg-zinc-200" />
|
|
84
|
+
|
|
85
|
+
<m.div
|
|
86
|
+
key={command}
|
|
87
|
+
initial={reduce ? false : { opacity: 0, y: 6 }}
|
|
88
|
+
animate={reduce ? undefined : { opacity: 1, y: 0 }}
|
|
89
|
+
transition={{ duration: 0.35, ease: SITE_EASE }}
|
|
90
|
+
className="flex min-h-[3.25rem] items-stretch border bg-white"
|
|
91
|
+
>
|
|
92
|
+
<pre className="flex flex-1 items-center overflow-x-auto p-3 font-mono text-[13px] leading-snug text-zinc-900">
|
|
93
|
+
<code>{command}</code>
|
|
94
|
+
</pre>
|
|
95
|
+
<Separator orientation="vertical" className="bg-zinc-200" />
|
|
96
|
+
<div className="flex shrink-0 items-center px-3">
|
|
97
|
+
<CopyButton
|
|
98
|
+
text={command}
|
|
99
|
+
idleLabel={dict.copy_button.idle}
|
|
100
|
+
successLabel={dict.copy_button.success}
|
|
101
|
+
/>
|
|
76
102
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
</m.div>
|
|
104
|
+
|
|
105
|
+
<Separator className="bg-zinc-200" />
|
|
106
|
+
|
|
107
|
+
<div className="relative min-h-[4.5rem] overflow-hidden pt-4">
|
|
108
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
109
|
+
<m.p
|
|
110
|
+
key={mode}
|
|
111
|
+
initial={reduce ? false : { opacity: 0, y: 8 }}
|
|
112
|
+
animate={reduce ? undefined : { opacity: 1, y: 0 }}
|
|
113
|
+
exit={reduce ? undefined : { opacity: 0, y: -6 }}
|
|
114
|
+
transition={{ duration: 0.3, ease: SITE_EASE }}
|
|
115
|
+
className="text-sm leading-relaxed text-zinc-600"
|
|
116
|
+
>
|
|
117
|
+
{dict.install_toggle[selectedMode.description]}
|
|
118
|
+
</m.p>
|
|
119
|
+
</AnimatePresence>
|
|
84
120
|
</div>
|
|
85
121
|
</div>
|
|
86
122
|
);
|
|
@@ -6,7 +6,7 @@ import type { Dictionary } from "@/lib/dictionary-types";
|
|
|
6
6
|
|
|
7
7
|
function TerminalCardFallback({ version, dict }: { version: string; dict: Dictionary["terminal"] }) {
|
|
8
8
|
return (
|
|
9
|
-
<div className="flex h-[300px] w-full flex-col
|
|
9
|
+
<div className="flex h-[300px] w-full flex-col border border-zinc-200 bg-white sm:h-[350px] lg:h-[380px]">
|
|
10
10
|
<div className="flex shrink-0 items-center justify-between border-b border-zinc-200 bg-white px-4 py-3">
|
|
11
11
|
<div className="flex items-center gap-4">
|
|
12
12
|
<div className="flex items-center gap-2">
|
|
@@ -32,9 +32,11 @@ function TerminalCardFallback({ version, dict }: { version: string; dict: Dictio
|
|
|
32
32
|
</div>
|
|
33
33
|
<div className="space-y-1">
|
|
34
34
|
<div className="font-bold text-zinc-900">cistack v{version}</div>
|
|
35
|
-
<div>
|
|
36
|
-
<div>
|
|
37
|
-
<div>
|
|
35
|
+
<div>{dict.project_scanned}</div>
|
|
36
|
+
<div>{dict.stack_detected}</div>
|
|
37
|
+
<div>
|
|
38
|
+
{dict.detected_stack}: Next.js, React, TypeScript
|
|
39
|
+
</div>
|
|
38
40
|
</div>
|
|
39
41
|
</div>
|
|
40
42
|
</div>
|
|
@@ -44,9 +46,11 @@ function TerminalCardFallback({ version, dict }: { version: string; dict: Dictio
|
|
|
44
46
|
export default function TerminalCard({
|
|
45
47
|
dict,
|
|
46
48
|
version = "3.0.0",
|
|
49
|
+
copyLabels,
|
|
47
50
|
}: {
|
|
48
51
|
dict: Dictionary["terminal"];
|
|
49
52
|
version?: string;
|
|
53
|
+
copyLabels: { idle: string; success: string };
|
|
50
54
|
}) {
|
|
51
55
|
const TerminalCardMotion = useMemo(
|
|
52
56
|
() =>
|
|
@@ -57,5 +61,5 @@ export default function TerminalCard({
|
|
|
57
61
|
[version, dict]
|
|
58
62
|
);
|
|
59
63
|
|
|
60
|
-
return <TerminalCardMotion dict={dict} version={version} />;
|
|
64
|
+
return <TerminalCardMotion dict={dict} version={version} copyLabels={copyLabels} />;
|
|
61
65
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { m } from "framer-motion";
|
|
5
5
|
import { RefreshCcw } from "lucide-react";
|
|
6
6
|
|
|
7
7
|
import CopyButton from "@/components/CopyButton";
|
|
8
|
+
import { Separator } from "@/components/ui/separator";
|
|
8
9
|
import type { Dictionary } from "@/lib/dictionary-types";
|
|
9
10
|
|
|
10
11
|
const COMMAND = "npx cistack";
|
|
@@ -41,9 +42,11 @@ const lineColor: Record<OutputLine["type"], string> = {
|
|
|
41
42
|
export default function TerminalCardMotion({
|
|
42
43
|
dict,
|
|
43
44
|
version,
|
|
45
|
+
copyLabels,
|
|
44
46
|
}: {
|
|
45
47
|
dict: Dictionary["terminal"];
|
|
46
48
|
version: string;
|
|
49
|
+
copyLabels: { idle: string; success: string };
|
|
47
50
|
}) {
|
|
48
51
|
const [typedCommand, setTypedCommand] = useState("");
|
|
49
52
|
const [visibleLines, setVisibleLines] = useState(0);
|
|
@@ -231,34 +234,39 @@ export default function TerminalCardMotion({
|
|
|
231
234
|
};
|
|
232
235
|
|
|
233
236
|
return (
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<div className="flex shrink-0 items-center justify-between border-b border-zinc-100 bg-white px-4 py-3">
|
|
237
|
+
<div
|
|
238
|
+
key={animationKey}
|
|
239
|
+
className="flex h-[300px] w-full flex-col border border-zinc-200 bg-white sm:h-[350px] lg:h-[380px]"
|
|
240
|
+
>
|
|
241
|
+
<div className="flex shrink-0 items-center justify-between border-b border-zinc-200 bg-white px-4 py-3">
|
|
240
242
|
<div className="flex items-center gap-4">
|
|
241
243
|
<div className="flex items-center gap-2">
|
|
242
244
|
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
|
|
243
245
|
<span className="font-mono text-[12px] font-black tracking-[0.18em] text-zinc-600 uppercase">
|
|
244
|
-
|
|
246
|
+
{dict.label}
|
|
245
247
|
</span>
|
|
246
248
|
</div>
|
|
247
249
|
</div>
|
|
248
250
|
|
|
249
251
|
<div className="flex items-center gap-2">
|
|
250
|
-
<div className="flex items-center gap-
|
|
251
|
-
<span className="font-mono text-[13px] font-bold tracking-tight text-zinc-800">
|
|
252
|
+
<div className="flex items-center gap-0 border border-zinc-200 bg-white">
|
|
253
|
+
<span className="px-2.5 py-1 font-mono text-[13px] font-bold tracking-tight text-zinc-800">
|
|
252
254
|
{COMMAND}
|
|
253
255
|
</span>
|
|
254
|
-
<
|
|
255
|
-
<
|
|
256
|
+
<Separator orientation="vertical" className="h-6 bg-zinc-200" />
|
|
257
|
+
<div className="px-2">
|
|
258
|
+
<CopyButton
|
|
259
|
+
text={COMMAND}
|
|
260
|
+
idleLabel={copyLabels.idle}
|
|
261
|
+
successLabel={copyLabels.success}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
256
264
|
</div>
|
|
257
265
|
<m.button
|
|
258
266
|
type="button"
|
|
259
267
|
whileTap={{ scale: 0.9 }}
|
|
260
268
|
onClick={handleReplay}
|
|
261
|
-
className="
|
|
269
|
+
className="border border-transparent p-1.5 text-zinc-500 transition-colors hover:border-zinc-200 hover:text-zinc-900"
|
|
262
270
|
aria-label="Replay terminal animation"
|
|
263
271
|
>
|
|
264
272
|
<RefreshCcw size={14} />
|
|
@@ -312,6 +320,5 @@ export default function TerminalCardMotion({
|
|
|
312
320
|
</div>
|
|
313
321
|
</div>
|
|
314
322
|
</div>
|
|
315
|
-
</LazyMotion>
|
|
316
323
|
);
|
|
317
324
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LazyMotion,
|
|
5
|
+
domAnimation,
|
|
6
|
+
m,
|
|
7
|
+
useReducedMotion,
|
|
8
|
+
type Variants,
|
|
9
|
+
} from "framer-motion";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
export const SITE_EASE = [0.22, 1, 0.36, 1] as const;
|
|
13
|
+
|
|
14
|
+
export const scrollViewport = {
|
|
15
|
+
once: true,
|
|
16
|
+
margin: "-10% 0px -6% 0px",
|
|
17
|
+
amount: 0.15,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export function SiteMotionRoot({ children }: { children: ReactNode }) {
|
|
21
|
+
return (
|
|
22
|
+
<LazyMotion features={domAnimation} strict>
|
|
23
|
+
{children}
|
|
24
|
+
</LazyMotion>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Reveal({
|
|
29
|
+
children,
|
|
30
|
+
className,
|
|
31
|
+
delay = 0,
|
|
32
|
+
y = 28,
|
|
33
|
+
}: {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
delay?: number;
|
|
37
|
+
y?: number;
|
|
38
|
+
}) {
|
|
39
|
+
const reduce = useReducedMotion();
|
|
40
|
+
if (reduce) {
|
|
41
|
+
return <div className={className}>{children}</div>;
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<m.div
|
|
45
|
+
className={className}
|
|
46
|
+
initial={{ opacity: 0, y, filter: "blur(8px)" }}
|
|
47
|
+
whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
48
|
+
viewport={scrollViewport}
|
|
49
|
+
transition={{ duration: 0.62, ease: SITE_EASE, delay }}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</m.div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function MotionHeader({ children, className }: { children: ReactNode; className?: string }) {
|
|
57
|
+
const reduce = useReducedMotion();
|
|
58
|
+
if (reduce) {
|
|
59
|
+
return <header className={className}>{children}</header>;
|
|
60
|
+
}
|
|
61
|
+
return (
|
|
62
|
+
<m.header
|
|
63
|
+
className={className}
|
|
64
|
+
initial={{ opacity: 0, y: -18, filter: "blur(6px)" }}
|
|
65
|
+
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
66
|
+
transition={{ duration: 0.52, ease: SITE_EASE }}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</m.header>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const heroStagger: Variants = {
|
|
74
|
+
hidden: {},
|
|
75
|
+
show: {
|
|
76
|
+
transition: { staggerChildren: 0.09, delayChildren: 0.06 },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const heroItem: Variants = {
|
|
81
|
+
hidden: { opacity: 0, y: 26 },
|
|
82
|
+
show: {
|
|
83
|
+
opacity: 1,
|
|
84
|
+
y: 0,
|
|
85
|
+
transition: { duration: 0.55, ease: SITE_EASE },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function HeroStagger({ children, className }: { children: ReactNode; className?: string }) {
|
|
90
|
+
const reduce = useReducedMotion();
|
|
91
|
+
if (reduce) {
|
|
92
|
+
return <div className={className}>{children}</div>;
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
<m.div
|
|
96
|
+
className={className}
|
|
97
|
+
variants={heroStagger}
|
|
98
|
+
initial="hidden"
|
|
99
|
+
animate="show"
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</m.div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function HeroStaggerItem({ children, className }: { children: ReactNode; className?: string }) {
|
|
107
|
+
const reduce = useReducedMotion();
|
|
108
|
+
if (reduce) {
|
|
109
|
+
return <div className={className}>{children}</div>;
|
|
110
|
+
}
|
|
111
|
+
return (
|
|
112
|
+
<m.div className={className} variants={heroItem}>
|
|
113
|
+
{children}
|
|
114
|
+
</m.div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const listStagger: Variants = {
|
|
119
|
+
hidden: {},
|
|
120
|
+
show: {
|
|
121
|
+
transition: { staggerChildren: 0.05, delayChildren: 0.04 },
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const listItem: Variants = {
|
|
126
|
+
hidden: { opacity: 0, y: 10 },
|
|
127
|
+
show: {
|
|
128
|
+
opacity: 1,
|
|
129
|
+
y: 0,
|
|
130
|
+
transition: { duration: 0.42, ease: SITE_EASE },
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function StaggerList({
|
|
135
|
+
children,
|
|
136
|
+
className,
|
|
137
|
+
as: Component = "ul",
|
|
138
|
+
}: {
|
|
139
|
+
children: ReactNode;
|
|
140
|
+
className?: string;
|
|
141
|
+
as?: "ul" | "ol" | "div";
|
|
142
|
+
}) {
|
|
143
|
+
const reduce = useReducedMotion();
|
|
144
|
+
if (reduce) {
|
|
145
|
+
const Tag = Component;
|
|
146
|
+
return <Tag className={className}>{children}</Tag>;
|
|
147
|
+
}
|
|
148
|
+
const MotionTag = Component === "ul" ? m.ul : Component === "ol" ? m.ol : m.div;
|
|
149
|
+
return (
|
|
150
|
+
<MotionTag
|
|
151
|
+
className={className}
|
|
152
|
+
variants={listStagger}
|
|
153
|
+
initial="hidden"
|
|
154
|
+
whileInView="show"
|
|
155
|
+
viewport={scrollViewport}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</MotionTag>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function StaggerItem({ children, className }: { children: ReactNode; className?: string }) {
|
|
163
|
+
const reduce = useReducedMotion();
|
|
164
|
+
if (reduce) {
|
|
165
|
+
return <li className={className}>{children}</li>;
|
|
166
|
+
}
|
|
167
|
+
return (
|
|
168
|
+
<m.li className={className} variants={listItem}>
|
|
169
|
+
{children}
|
|
170
|
+
</m.li>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tagContainer: Variants = {
|
|
175
|
+
hidden: {},
|
|
176
|
+
show: {
|
|
177
|
+
transition: { staggerChildren: 0.035, delayChildren: 0.02 },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const tagItem: Variants = {
|
|
182
|
+
hidden: { opacity: 0, scale: 0.85, y: 6 },
|
|
183
|
+
show: {
|
|
184
|
+
opacity: 1,
|
|
185
|
+
scale: 1,
|
|
186
|
+
y: 0,
|
|
187
|
+
transition: { type: "spring", stiffness: 380, damping: 22 },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export function MotionTagList({ tags }: { tags: readonly string[] }) {
|
|
192
|
+
const reduce = useReducedMotion();
|
|
193
|
+
if (reduce) {
|
|
194
|
+
return (
|
|
195
|
+
<div className="flex flex-wrap gap-1.5">
|
|
196
|
+
{tags.map((tag) => (
|
|
197
|
+
<span
|
|
198
|
+
key={tag}
|
|
199
|
+
className="inline-flex border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700"
|
|
200
|
+
>
|
|
201
|
+
{tag}
|
|
202
|
+
</span>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return (
|
|
208
|
+
<m.div
|
|
209
|
+
className="flex flex-wrap gap-1.5"
|
|
210
|
+
variants={tagContainer}
|
|
211
|
+
initial="hidden"
|
|
212
|
+
whileInView="show"
|
|
213
|
+
viewport={scrollViewport}
|
|
214
|
+
>
|
|
215
|
+
{tags.map((tag) => (
|
|
216
|
+
<m.span
|
|
217
|
+
key={tag}
|
|
218
|
+
variants={tagItem}
|
|
219
|
+
className="inline-flex border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium text-zinc-700"
|
|
220
|
+
whileHover={{ y: -2, borderColor: "rgb(24 24 27)", transition: { duration: 0.2 } }}
|
|
221
|
+
>
|
|
222
|
+
{tag}
|
|
223
|
+
</m.span>
|
|
224
|
+
))}
|
|
225
|
+
</m.div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { m };
|