cistack 6.0.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/.github/dependabot.yml +42 -0
- package/.github/workflows/ci.yml +2 -1
- package/.github/workflows/pipeline.yml +250 -0
- package/README.md +4 -0
- package/package.json +7 -2
- package/product-site/.github/dependabot.yml +27 -0
- package/product-site/.github/workflows/pipeline.yml +215 -0
- package/product-site/.lighthouserc.json +22 -0
- package/product-site/README.md +1 -0
- package/product-site/app/[lang]/layout.tsx +95 -0
- package/product-site/app/[lang]/page.tsx +19 -0
- package/product-site/app/favicon.ico +0 -0
- package/product-site/app/globals.css +228 -0
- package/product-site/app/manifest.ts +20 -0
- package/product-site/app/robots.ts +12 -0
- package/product-site/app/sitemap.ts +12 -0
- package/product-site/components/CanvasText.tsx +219 -0
- package/product-site/components/CopyButton.tsx +101 -0
- package/product-site/components/HomeClient.tsx +664 -0
- package/product-site/components/InstallToggle.tsx +123 -0
- package/product-site/components/MotionRevealClient.tsx +53 -0
- package/product-site/components/TerminalCard.tsx +65 -0
- package/product-site/components/TerminalCardMotion.tsx +324 -0
- package/product-site/components/site-motion.tsx +229 -0
- package/product-site/components/ui/accordion.tsx +74 -0
- package/product-site/components/ui/badge.tsx +52 -0
- package/product-site/components/ui/button.tsx +60 -0
- package/product-site/components/ui/card.tsx +103 -0
- package/product-site/components/ui/checkbox.tsx +29 -0
- package/product-site/components/ui/separator.tsx +25 -0
- package/product-site/components/ui/table.tsx +116 -0
- package/product-site/components/ui/tabs.tsx +82 -0
- package/product-site/components.json +25 -0
- package/product-site/dictionaries/br.json +276 -0
- package/product-site/dictionaries/cn.json +276 -0
- package/product-site/dictionaries/de.json +276 -0
- package/product-site/dictionaries/en.json +274 -0
- package/product-site/dictionaries/es.json +276 -0
- package/product-site/dictionaries/fr.json +276 -0
- package/product-site/dictionaries/pt.json +276 -0
- package/product-site/eslint.config.mjs +18 -0
- package/product-site/lib/dictionaries.ts +18 -0
- package/product-site/lib/dictionary-types.ts +3 -0
- package/product-site/lib/utils.ts +6 -0
- package/product-site/middleware.ts +39 -0
- package/product-site/next.config.mjs +14 -0
- package/product-site/package-lock.json +14201 -0
- package/product-site/package.json +42 -0
- package/product-site/postcss.config.mjs +7 -0
- package/product-site/public/file.svg +1 -0
- package/product-site/public/globe.svg +1 -0
- package/product-site/public/next.svg +1 -0
- package/product-site/public/og-image.png +0 -0
- package/product-site/public/vercel.svg +1 -0
- package/product-site/public/window.svg +1 -0
- package/product-site/scripts/sync-i18n.mjs +58 -0
- package/product-site/scripts/validate-i18n.mjs +45 -0
- package/product-site/tsconfig.json +34 -0
- package/product-site/types/negotiator.d.ts +14 -0
- package/product-site/vercel.json +5 -0
- package/src/index.js +12 -13
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, m, useReducedMotion } from "framer-motion";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
import CopyButton from "@/components/CopyButton";
|
|
7
|
+
import { Separator } from "@/components/ui/separator";
|
|
8
|
+
import type { Dictionary } from "@/lib/dictionary-types";
|
|
9
|
+
|
|
10
|
+
import { SITE_EASE } from "@/components/site-motion";
|
|
11
|
+
|
|
12
|
+
type InstallMode = "npx" | "npm";
|
|
13
|
+
|
|
14
|
+
const installCopy: Record<InstallMode, string> = {
|
|
15
|
+
npx: "npx cistack",
|
|
16
|
+
npm: "npm install -g cistack",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const installModes: Record<
|
|
20
|
+
InstallMode,
|
|
21
|
+
{
|
|
22
|
+
badgeClassName: string;
|
|
23
|
+
badgeLabel: keyof Dictionary["install_toggle"];
|
|
24
|
+
description: keyof Dictionary["install_toggle"];
|
|
25
|
+
}
|
|
26
|
+
> = {
|
|
27
|
+
npx: {
|
|
28
|
+
badgeClassName: "border-emerald-200 bg-emerald-50 text-emerald-800",
|
|
29
|
+
badgeLabel: "recommended_badge",
|
|
30
|
+
description: "npx_desc",
|
|
31
|
+
},
|
|
32
|
+
npm: {
|
|
33
|
+
badgeClassName: "border-zinc-200 bg-zinc-50 text-zinc-700",
|
|
34
|
+
badgeLabel: "global_badge",
|
|
35
|
+
description: "npm_desc",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function InstallToggle({ dict }: { dict: Dictionary }) {
|
|
40
|
+
const [mode, setMode] = useState<InstallMode>("npx");
|
|
41
|
+
const selectedMode = installModes[mode];
|
|
42
|
+
const command = installCopy[mode];
|
|
43
|
+
const reduce = useReducedMotion();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-0">
|
|
47
|
+
<div className="flex flex-wrap items-center gap-3 pb-3">
|
|
48
|
+
<m.button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => setMode("npx")}
|
|
51
|
+
whileTap={reduce ? {} : { scale: 0.98 }}
|
|
52
|
+
className={`text-sm font-semibold transition-colors ${
|
|
53
|
+
mode === "npx" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
|
|
54
|
+
}`}
|
|
55
|
+
>
|
|
56
|
+
npx
|
|
57
|
+
{mode === "npx" && (
|
|
58
|
+
<span className="ml-1.5 text-xs font-normal text-zinc-500">
|
|
59
|
+
— {dict.install_toggle.recommended}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</m.button>
|
|
63
|
+
<Separator orientation="vertical" className="h-4 bg-zinc-200" />
|
|
64
|
+
<m.button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => setMode("npm")}
|
|
67
|
+
whileTap={reduce ? {} : { scale: 0.98 }}
|
|
68
|
+
className={`text-sm font-semibold transition-colors ${
|
|
69
|
+
mode === "npm" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
{dict.install_toggle.npm_global}
|
|
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>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
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
|
+
/>
|
|
102
|
+
</div>
|
|
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>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { LazyMotion, domAnimation, m } from "framer-motion";
|
|
5
|
+
|
|
6
|
+
type MotionTag = "div" | "nav" | "footer";
|
|
7
|
+
|
|
8
|
+
interface MotionRevealClientProps {
|
|
9
|
+
as?: MotionTag;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
delay?: number;
|
|
13
|
+
duration?: number;
|
|
14
|
+
immediate?: boolean;
|
|
15
|
+
initialScale?: number;
|
|
16
|
+
initialY?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const motionTags = {
|
|
20
|
+
div: m.div,
|
|
21
|
+
nav: m.nav,
|
|
22
|
+
footer: m.footer,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export default function MotionRevealClient({
|
|
26
|
+
as = "div",
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
delay = 0,
|
|
30
|
+
duration = 0.8,
|
|
31
|
+
immediate = false,
|
|
32
|
+
initialScale = 1,
|
|
33
|
+
initialY = 20,
|
|
34
|
+
}: MotionRevealClientProps) {
|
|
35
|
+
const MotionTag = motionTags[as];
|
|
36
|
+
const transition = { duration, delay };
|
|
37
|
+
const target = { opacity: 1, y: 0, scale: 1 };
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<LazyMotion features={domAnimation}>
|
|
41
|
+
<MotionTag
|
|
42
|
+
className={className}
|
|
43
|
+
initial={{ opacity: 0, y: initialY, scale: initialScale }}
|
|
44
|
+
{...(immediate
|
|
45
|
+
? { animate: target }
|
|
46
|
+
: { whileInView: target, viewport: { once: true, amount: 0.2 } })}
|
|
47
|
+
transition={transition}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</MotionTag>
|
|
51
|
+
</LazyMotion>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import type { Dictionary } from "@/lib/dictionary-types";
|
|
6
|
+
|
|
7
|
+
function TerminalCardFallback({ version, dict }: { version: string; dict: Dictionary["terminal"] }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex h-[300px] w-full flex-col border border-zinc-200 bg-white sm:h-[350px] lg:h-[380px]">
|
|
10
|
+
<div className="flex shrink-0 items-center justify-between border-b border-zinc-200 bg-white px-4 py-3">
|
|
11
|
+
<div className="flex items-center gap-4">
|
|
12
|
+
<div className="flex items-center gap-2">
|
|
13
|
+
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-600" />
|
|
14
|
+
<span className="text-[12px] font-bold tracking-[0.14em] text-zinc-600 uppercase">
|
|
15
|
+
{dict?.processing || "Processing Output..."}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="rounded-sm border border-zinc-200 bg-zinc-50 px-2.5 py-1 font-mono text-[13px] font-bold tracking-tight text-zinc-800">
|
|
20
|
+
npx cistack
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
className="flex-1 bg-white p-6 pt-4 font-mono text-[12px] leading-relaxed tracking-tight text-zinc-700 sm:text-[13px]"
|
|
26
|
+
style={{ fontFamily: "var(--font-mono)" }}
|
|
27
|
+
>
|
|
28
|
+
<div className="mb-2 flex items-center gap-2">
|
|
29
|
+
<span className="font-bold text-zinc-600">$</span>
|
|
30
|
+
<span className="font-bold text-zinc-900">npx cistack</span>
|
|
31
|
+
<span className="inline-block h-4 w-1.5 animate-pulse bg-emerald-600" />
|
|
32
|
+
</div>
|
|
33
|
+
<div className="space-y-1">
|
|
34
|
+
<div className="font-bold text-zinc-900">cistack v{version}</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>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function TerminalCard({
|
|
47
|
+
dict,
|
|
48
|
+
version = "3.0.0",
|
|
49
|
+
copyLabels,
|
|
50
|
+
}: {
|
|
51
|
+
dict: Dictionary["terminal"];
|
|
52
|
+
version?: string;
|
|
53
|
+
copyLabels: { idle: string; success: string };
|
|
54
|
+
}) {
|
|
55
|
+
const TerminalCardMotion = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
dynamic(() => import("@/components/TerminalCardMotion"), {
|
|
58
|
+
ssr: false,
|
|
59
|
+
loading: () => <TerminalCardFallback version={version} dict={dict} />,
|
|
60
|
+
}),
|
|
61
|
+
[version, dict]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return <TerminalCardMotion dict={dict} version={version} copyLabels={copyLabels} />;
|
|
65
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { m } from "framer-motion";
|
|
5
|
+
import { RefreshCcw } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import CopyButton from "@/components/CopyButton";
|
|
8
|
+
import { Separator } from "@/components/ui/separator";
|
|
9
|
+
import type { Dictionary } from "@/lib/dictionary-types";
|
|
10
|
+
|
|
11
|
+
const COMMAND = "npx cistack";
|
|
12
|
+
|
|
13
|
+
interface OutputLine {
|
|
14
|
+
text: string;
|
|
15
|
+
type:
|
|
16
|
+
| "success"
|
|
17
|
+
| "info"
|
|
18
|
+
| "heading"
|
|
19
|
+
| "detail"
|
|
20
|
+
| "merged"
|
|
21
|
+
| "bullet"
|
|
22
|
+
| "written"
|
|
23
|
+
| "done"
|
|
24
|
+
| "path"
|
|
25
|
+
| "blank";
|
|
26
|
+
delay: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lineColor: Record<OutputLine["type"], string> = {
|
|
30
|
+
success: "text-emerald-500",
|
|
31
|
+
info: "text-zinc-700",
|
|
32
|
+
heading: "font-bold text-zinc-900",
|
|
33
|
+
detail: "text-zinc-700",
|
|
34
|
+
merged: "text-amber-500",
|
|
35
|
+
bullet: "font-medium text-zinc-600",
|
|
36
|
+
written: "text-emerald-600",
|
|
37
|
+
done: "font-bold text-zinc-950",
|
|
38
|
+
path: "text-zinc-600",
|
|
39
|
+
blank: "",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function TerminalCardMotion({
|
|
43
|
+
dict,
|
|
44
|
+
version,
|
|
45
|
+
copyLabels,
|
|
46
|
+
}: {
|
|
47
|
+
dict: Dictionary["terminal"];
|
|
48
|
+
version: string;
|
|
49
|
+
copyLabels: { idle: string; success: string };
|
|
50
|
+
}) {
|
|
51
|
+
const [typedCommand, setTypedCommand] = useState("");
|
|
52
|
+
const [visibleLines, setVisibleLines] = useState(0);
|
|
53
|
+
const [phase, setPhase] = useState<"typing" | "output" | "done">("typing");
|
|
54
|
+
const [animationKey, setAnimationKey] = useState(0);
|
|
55
|
+
|
|
56
|
+
const outputLines = useMemo<OutputLine[]>(() => {
|
|
57
|
+
return [
|
|
58
|
+
{ text: ` cistack v${version}`, type: "heading", delay: 100 },
|
|
59
|
+
{ text: ` ${"─".repeat(24)}`, type: "detail", delay: 200 },
|
|
60
|
+
{ text: "", type: "blank", delay: 250 },
|
|
61
|
+
{ text: dict.project_scanned || "Project scanned", type: "success", delay: 500 },
|
|
62
|
+
{ text: dict.stack_detected || "Stack detected", type: "success", delay: 800 },
|
|
63
|
+
{ text: "", type: "blank", delay: 850 },
|
|
64
|
+
{
|
|
65
|
+
text: ` ${dict.detected_stack || "Detected Stack"}`,
|
|
66
|
+
type: "heading",
|
|
67
|
+
delay: 1000,
|
|
68
|
+
},
|
|
69
|
+
{ text: ` ${"─".repeat(48)}`, type: "detail", delay: 1100 },
|
|
70
|
+
{
|
|
71
|
+
text: ` ${dict.languages || "Languages:"} TypeScript`,
|
|
72
|
+
type: "info",
|
|
73
|
+
delay: 1300,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
text: ` ${dict.frameworks || "Frameworks:"} Next.js, React`,
|
|
77
|
+
type: "info",
|
|
78
|
+
delay: 1450,
|
|
79
|
+
},
|
|
80
|
+
{ text: ` ${dict.hosting || "Hosting:"} Vercel`, type: "info", delay: 1600 },
|
|
81
|
+
{ text: ` ${dict.testing || "Testing:"} none`, type: "info", delay: 1750 },
|
|
82
|
+
{
|
|
83
|
+
text: ` ${dict.release_tool || "Release tool:"} none`,
|
|
84
|
+
type: "info",
|
|
85
|
+
delay: 1900,
|
|
86
|
+
},
|
|
87
|
+
{ text: "", type: "blank", delay: 1950 },
|
|
88
|
+
{
|
|
89
|
+
text:
|
|
90
|
+
dict.look_correct ||
|
|
91
|
+
"Does this look correct? Generate pipeline with these settings? Yes",
|
|
92
|
+
type: "detail",
|
|
93
|
+
delay: 2200,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
text: dict.generated_workflows || "Generated 3 CI workflow(s)",
|
|
97
|
+
type: "success",
|
|
98
|
+
delay: 2600,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
text: ` ${dict.smart_merged || "Smart-merged: ci.yml"}`,
|
|
102
|
+
type: "merged",
|
|
103
|
+
delay: 2800,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
text: ` • ${dict.updated_on || 'updated top-level "on"'}`,
|
|
107
|
+
type: "bullet",
|
|
108
|
+
delay: 2900,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
text: ` • ${dict.updated_concurrency || 'updated top-level "concurrency"'}`,
|
|
112
|
+
type: "bullet",
|
|
113
|
+
delay: 2950,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
text: ` • ${dict.added_lint || 'added job "lint"'}`,
|
|
117
|
+
type: "bullet",
|
|
118
|
+
delay: 3000,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
text: ` • ${dict.updated_build || 'job "build" → updated "name"'}`,
|
|
122
|
+
type: "bullet",
|
|
123
|
+
delay: 3050,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
text: ` • ${dict.updated_needs || 'job "build" → updated "needs"'}`,
|
|
127
|
+
type: "bullet",
|
|
128
|
+
delay: 3100,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
text: ` • ${
|
|
132
|
+
dict.added_checkout || 'job "build" → added step "Checkout code"'
|
|
133
|
+
}`,
|
|
134
|
+
type: "bullet",
|
|
135
|
+
delay: 3150,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
text: ` • ${dict.added_node || 'job "build" → added step "Set up Node.js"'}`,
|
|
139
|
+
type: "bullet",
|
|
140
|
+
delay: 3200,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
text: ` • ${dict.updated_step_build || 'job "build" → updated step "Build"'}`,
|
|
144
|
+
type: "bullet",
|
|
145
|
+
delay: 3250,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
text: ` • ${
|
|
149
|
+
dict.added_upload || 'job "build" → added step "Upload build artifact"'
|
|
150
|
+
}`,
|
|
151
|
+
type: "bullet",
|
|
152
|
+
delay: 3300,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
text: ` ✔ ${dict.written_deploy || "Written: deploy.yml"}`,
|
|
156
|
+
type: "written",
|
|
157
|
+
delay: 3500,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
text: ` ✔ ${dict.written_security || "Written: security.yml"}`,
|
|
161
|
+
type: "written",
|
|
162
|
+
delay: 3650,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
text: ` ✔ ${
|
|
166
|
+
dict.written_dependabot || "Written: .github/dependabot.yml"
|
|
167
|
+
}`,
|
|
168
|
+
type: "written",
|
|
169
|
+
delay: 3800,
|
|
170
|
+
},
|
|
171
|
+
{ text: "", type: "blank", delay: 3850 },
|
|
172
|
+
{
|
|
173
|
+
text: ` ${dict.done_msg || "Done! Your GitHub Actions pipeline is ready."}`,
|
|
174
|
+
type: "done",
|
|
175
|
+
delay: 4100,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
text: ` ${dict.workflows_path || "Workflows → cistack/.github/workflows"}`,
|
|
179
|
+
type: "path",
|
|
180
|
+
delay: 4300,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
text: ` ${
|
|
184
|
+
dict.dependabot_path || "Dependabot → cistack/.github/dependabot.yml"
|
|
185
|
+
}`,
|
|
186
|
+
type: "path",
|
|
187
|
+
delay: 4450,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
}, [dict, version]);
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (phase !== "typing") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (typedCommand.length < COMMAND.length) {
|
|
198
|
+
const timeout = window.setTimeout(() => {
|
|
199
|
+
setTypedCommand(COMMAND.slice(0, typedCommand.length + 1));
|
|
200
|
+
}, 60 + Math.random() * 60);
|
|
201
|
+
|
|
202
|
+
return () => window.clearTimeout(timeout);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const timeout = window.setTimeout(() => setPhase("output"), 400);
|
|
206
|
+
return () => window.clearTimeout(timeout);
|
|
207
|
+
}, [typedCommand, phase, animationKey]);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (phase !== "output") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (visibleLines >= outputLines.length) {
|
|
215
|
+
const timeout = window.setTimeout(() => setPhase("done"), 0);
|
|
216
|
+
return () => window.clearTimeout(timeout);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const currentDelay =
|
|
220
|
+
outputLines[visibleLines].delay -
|
|
221
|
+
(visibleLines > 0 ? outputLines[visibleLines - 1].delay : 0);
|
|
222
|
+
const timeout = window.setTimeout(() => {
|
|
223
|
+
setVisibleLines((current) => current + 1);
|
|
224
|
+
}, Math.max(currentDelay, 30));
|
|
225
|
+
|
|
226
|
+
return () => window.clearTimeout(timeout);
|
|
227
|
+
}, [visibleLines, phase, outputLines]);
|
|
228
|
+
|
|
229
|
+
const handleReplay = () => {
|
|
230
|
+
setTypedCommand("");
|
|
231
|
+
setVisibleLines(0);
|
|
232
|
+
setPhase("typing");
|
|
233
|
+
setAnimationKey((current) => current + 1);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return (
|
|
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">
|
|
242
|
+
<div className="flex items-center gap-4">
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
|
|
245
|
+
<span className="font-mono text-[12px] font-black tracking-[0.18em] text-zinc-600 uppercase">
|
|
246
|
+
{dict.label}
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
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">
|
|
254
|
+
{COMMAND}
|
|
255
|
+
</span>
|
|
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>
|
|
264
|
+
</div>
|
|
265
|
+
<m.button
|
|
266
|
+
type="button"
|
|
267
|
+
whileTap={{ scale: 0.9 }}
|
|
268
|
+
onClick={handleReplay}
|
|
269
|
+
className="border border-transparent p-1.5 text-zinc-500 transition-colors hover:border-zinc-200 hover:text-zinc-900"
|
|
270
|
+
aria-label="Replay terminal animation"
|
|
271
|
+
>
|
|
272
|
+
<RefreshCcw size={14} />
|
|
273
|
+
</m.button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div
|
|
278
|
+
className="custom-scrollbar flex-1 overflow-y-auto bg-white p-6 pt-4 font-mono text-[12px] leading-relaxed tracking-tight selection:bg-zinc-900 selection:text-white sm:text-[13px]"
|
|
279
|
+
style={{ fontFamily: "var(--font-mono)" }}
|
|
280
|
+
>
|
|
281
|
+
<div className="flex flex-col gap-1.5">
|
|
282
|
+
<div className="mb-2 flex items-center gap-2">
|
|
283
|
+
<span className="font-bold text-zinc-600">$</span>
|
|
284
|
+
<span className="font-bold text-zinc-900">{typedCommand}</span>
|
|
285
|
+
{phase === "typing" && (
|
|
286
|
+
<m.span
|
|
287
|
+
animate={{ opacity: [1, 0] }}
|
|
288
|
+
transition={{ duration: 0.8, repeat: Number.POSITIVE_INFINITY }}
|
|
289
|
+
className="inline-block h-4 w-1.5 bg-emerald-500"
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{phase !== "typing" && (
|
|
295
|
+
<div className="space-y-0.5">
|
|
296
|
+
{outputLines.slice(0, visibleLines).map((line, index) => (
|
|
297
|
+
<m.div
|
|
298
|
+
key={`${animationKey}-${index}`}
|
|
299
|
+
initial={{ opacity: 0, x: -4 }}
|
|
300
|
+
animate={{ opacity: 1, x: 0 }}
|
|
301
|
+
transition={{ duration: 0.2 }}
|
|
302
|
+
className={`whitespace-pre-wrap break-words ${lineColor[line.type]}`}
|
|
303
|
+
style={{
|
|
304
|
+
minHeight: line.type === "blank" ? "0.75rem" : undefined,
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
{line.text}
|
|
308
|
+
</m.div>
|
|
309
|
+
))}
|
|
310
|
+
{phase === "output" && (
|
|
311
|
+
<div className="mt-2 flex items-center gap-2">
|
|
312
|
+
<div className="h-3 w-1 animate-pulse bg-zinc-300" />
|
|
313
|
+
<span className="text-[12px] font-bold tracking-[0.14em] text-zinc-600 uppercase">
|
|
314
|
+
{dict.processing || "Processing Output..."}
|
|
315
|
+
</span>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|