cistack 5.5.0 → 6.1.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 (63) hide show
  1. package/.github/dependabot.yml +42 -0
  2. package/.github/workflows/ci.yml +2 -1
  3. package/.github/workflows/pipeline.yml +250 -0
  4. package/README.md +4 -0
  5. package/package.json +7 -2
  6. package/product-site/.github/dependabot.yml +27 -0
  7. package/product-site/.github/workflows/pipeline.yml +202 -0
  8. package/product-site/.lighthouserc.json +22 -0
  9. package/product-site/README.md +1 -0
  10. package/product-site/app/[lang]/layout.tsx +95 -0
  11. package/product-site/app/[lang]/page.tsx +19 -0
  12. package/product-site/app/favicon.ico +0 -0
  13. package/product-site/app/globals.css +228 -0
  14. package/product-site/app/manifest.ts +20 -0
  15. package/product-site/app/robots.ts +12 -0
  16. package/product-site/app/sitemap.ts +12 -0
  17. package/product-site/components/CanvasText.tsx +219 -0
  18. package/product-site/components/CopyButton.tsx +85 -0
  19. package/product-site/components/HomeClient.tsx +985 -0
  20. package/product-site/components/InstallToggle.tsx +87 -0
  21. package/product-site/components/MotionRevealClient.tsx +53 -0
  22. package/product-site/components/TerminalCard.tsx +61 -0
  23. package/product-site/components/TerminalCardMotion.tsx +317 -0
  24. package/product-site/components/ui/accordion.tsx +74 -0
  25. package/product-site/components/ui/badge.tsx +52 -0
  26. package/product-site/components/ui/button.tsx +60 -0
  27. package/product-site/components/ui/card.tsx +103 -0
  28. package/product-site/components/ui/checkbox.tsx +29 -0
  29. package/product-site/components/ui/separator.tsx +25 -0
  30. package/product-site/components/ui/table.tsx +116 -0
  31. package/product-site/components/ui/tabs.tsx +82 -0
  32. package/product-site/components.json +25 -0
  33. package/product-site/dictionaries/br.json +137 -0
  34. package/product-site/dictionaries/cn.json +137 -0
  35. package/product-site/dictionaries/de.json +137 -0
  36. package/product-site/dictionaries/en.json +137 -0
  37. package/product-site/dictionaries/es.json +182 -0
  38. package/product-site/dictionaries/fr.json +182 -0
  39. package/product-site/dictionaries/pt.json +137 -0
  40. package/product-site/eslint.config.mjs +18 -0
  41. package/product-site/lib/dictionaries.ts +18 -0
  42. package/product-site/lib/dictionary-types.ts +3 -0
  43. package/product-site/lib/utils.ts +6 -0
  44. package/product-site/middleware.ts +39 -0
  45. package/product-site/next.config.mjs +14 -0
  46. package/product-site/package-lock.json +14201 -0
  47. package/product-site/package.json +41 -0
  48. package/product-site/postcss.config.mjs +7 -0
  49. package/product-site/public/file.svg +1 -0
  50. package/product-site/public/globe.svg +1 -0
  51. package/product-site/public/next.svg +1 -0
  52. package/product-site/public/og-image.png +0 -0
  53. package/product-site/public/vercel.svg +1 -0
  54. package/product-site/public/window.svg +1 -0
  55. package/product-site/scripts/sync-i18n.mjs +58 -0
  56. package/product-site/tsconfig.json +34 -0
  57. package/product-site/types/negotiator.d.ts +14 -0
  58. package/product-site/vercel.json +5 -0
  59. package/src/analyzers/codebase.js +53 -0
  60. package/src/detectors/framework.js +6 -6
  61. package/src/detectors/testing.js +37 -5
  62. package/src/generators/workflow.js +3 -63
  63. package/tests/run.js +38 -63
@@ -0,0 +1,19 @@
1
+ import { notFound } from "next/navigation";
2
+ import { getDictionary, hasLocale, Locale } from "@/lib/dictionaries";
3
+ import HomeClient from "@/components/HomeClient";
4
+
5
+ export default async function Page({
6
+ params,
7
+ }: {
8
+ params: Promise<{ lang: string }>;
9
+ }) {
10
+ const { lang } = await params;
11
+
12
+ if (!hasLocale(lang)) {
13
+ notFound();
14
+ }
15
+
16
+ const dict = await getDictionary(lang as Locale);
17
+
18
+ return <HomeClient dict={dict} lang={lang as Locale} />;
19
+ }
Binary file
@@ -0,0 +1,228 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --color-background: var(--background);
9
+ --color-foreground: var(--foreground);
10
+ --font-sans: var(--font-dm-sans), ui-sans-serif, system-ui, sans-serif;
11
+ --font-mono: var(--font-fira-code), 'Fira Code', monospace;
12
+ --font-heading: var(--font-dm-sans), ui-sans-serif, system-ui, sans-serif;
13
+ --color-sidebar-ring: var(--sidebar-ring);
14
+ --color-sidebar-border: var(--sidebar-border);
15
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
16
+ --color-sidebar-accent: var(--sidebar-accent);
17
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
18
+ --color-sidebar-primary: var(--sidebar-primary);
19
+ --color-sidebar-foreground: var(--sidebar-foreground);
20
+ --color-sidebar: var(--sidebar);
21
+ --color-chart-5: var(--chart-5);
22
+ --color-chart-4: var(--chart-4);
23
+ --color-chart-3: var(--chart-3);
24
+ --color-chart-2: var(--chart-2);
25
+ --color-chart-1: var(--chart-1);
26
+ --color-ring: var(--ring);
27
+ --color-input: var(--input);
28
+ --color-border: var(--border);
29
+ --color-destructive: var(--destructive);
30
+ --color-accent-foreground: var(--accent-foreground);
31
+ --color-accent: var(--accent);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-muted: var(--muted);
34
+ --color-secondary-foreground: var(--secondary-foreground);
35
+ --color-secondary: var(--secondary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-primary: var(--primary);
38
+ --color-popover-foreground: var(--popover-foreground);
39
+ --color-popover: var(--popover);
40
+ --color-card-foreground: var(--card-foreground);
41
+ --color-card: var(--card);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ *, *::before, *::after {
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ html {
56
+ -webkit-font-smoothing: antialiased;
57
+ -moz-osx-font-smoothing: grayscale;
58
+ }
59
+
60
+ body {
61
+ font-family: var(--font-dm-sans), ui-sans-serif, system-ui, sans-serif;
62
+ min-height: 100vh;
63
+ }
64
+
65
+ .font-mono,
66
+ code,
67
+ kbd,
68
+ samp,
69
+ pre {
70
+ font-family: var(--font-fira-code), 'Fira Code', monospace !important;
71
+ font-variant-ligatures: discretionary-ligatures;
72
+ }
73
+
74
+ /* Subtle custom scrollbar */
75
+ .custom-scrollbar::-webkit-scrollbar {
76
+ width: 4px;
77
+ }
78
+ .custom-scrollbar::-webkit-scrollbar-track {
79
+ background: transparent;
80
+ }
81
+ .custom-scrollbar::-webkit-scrollbar-thumb {
82
+ background: #e4e4e7;
83
+ border-radius: 999px;
84
+ }
85
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
86
+ background: #d4d4d8;
87
+ }
88
+
89
+ .page-root :is(.text-\[9px\], .text-\[10px\], .text-\[11px\], .text-\[12px\], .text-xs).text-zinc-200 {
90
+ color: var(--color-zinc-500);
91
+ }
92
+
93
+ .page-root :is(.text-\[9px\], .text-\[10px\], .text-\[11px\], .text-\[12px\], .text-xs).text-zinc-300 {
94
+ color: var(--color-zinc-600);
95
+ }
96
+
97
+ .page-root :is(.text-\[9px\], .text-\[10px\], .text-\[11px\], .text-\[12px\], .text-xs).text-zinc-400 {
98
+ color: var(--color-zinc-700);
99
+ }
100
+
101
+ .page-root :is(.text-\[9px\], .text-\[10px\], .text-\[11px\], .text-\[12px\], .text-xs).text-zinc-500 {
102
+ color: var(--color-zinc-800);
103
+ }
104
+
105
+ @media (max-width: 767px) {
106
+ .page-root :is(.text-\[9px\], .text-\[10px\], .text-\[11px\], .text-\[12px\]) {
107
+ font-size: 0.875rem;
108
+ line-height: 1.4;
109
+ }
110
+
111
+ .page-root .text-xs {
112
+ font-size: 0.9375rem;
113
+ line-height: 1.5;
114
+ }
115
+ }
116
+
117
+ .page-root .text-zinc-300 {
118
+ color: var(--color-zinc-500);
119
+ }
120
+
121
+ .page-root .text-zinc-400 {
122
+ color: var(--color-zinc-600);
123
+ }
124
+
125
+ .page-root .text-zinc-500 {
126
+ color: var(--color-zinc-700);
127
+ }
128
+
129
+ .page-root .bg-zinc-950 .text-zinc-300 {
130
+ color: var(--color-zinc-300);
131
+ }
132
+
133
+ .page-root .bg-zinc-950 .text-zinc-200 {
134
+ color: var(--color-zinc-200);
135
+ }
136
+
137
+ .page-root .bg-zinc-950 .text-zinc-400 {
138
+ color: var(--color-zinc-300);
139
+ }
140
+
141
+ .page-root .bg-zinc-950 .text-zinc-500 {
142
+ color: var(--color-zinc-400);
143
+ }
144
+
145
+ .page-root .bg-zinc-950 .text-zinc-700 {
146
+ color: var(--color-zinc-400);
147
+ }
148
+
149
+ :root {
150
+ --background: oklch(1 0 0);
151
+ --foreground: oklch(0.145 0 0);
152
+ --card: oklch(1 0 0);
153
+ --card-foreground: oklch(0.145 0 0);
154
+ --popover: oklch(1 0 0);
155
+ --popover-foreground: oklch(0.145 0 0);
156
+ --primary: oklch(0.205 0 0);
157
+ --primary-foreground: oklch(0.985 0 0);
158
+ --secondary: oklch(0.97 0 0);
159
+ --secondary-foreground: oklch(0.205 0 0);
160
+ --muted: oklch(0.97 0 0);
161
+ --muted-foreground: oklch(0.556 0 0);
162
+ --accent: oklch(0.97 0 0);
163
+ --accent-foreground: oklch(0.205 0 0);
164
+ --destructive: oklch(0.577 0.245 27.325);
165
+ --border: oklch(0.922 0 0);
166
+ --input: oklch(0.922 0 0);
167
+ --ring: oklch(0.708 0 0);
168
+ --chart-1: oklch(0.87 0 0);
169
+ --chart-2: oklch(0.556 0 0);
170
+ --chart-3: oklch(0.439 0 0);
171
+ --chart-4: oklch(0.371 0 0);
172
+ --chart-5: oklch(0.269 0 0);
173
+ --radius: 0.625rem;
174
+ --sidebar: oklch(0.985 0 0);
175
+ --sidebar-foreground: oklch(0.145 0 0);
176
+ --sidebar-primary: oklch(0.205 0 0);
177
+ --sidebar-primary-foreground: oklch(0.985 0 0);
178
+ --sidebar-accent: oklch(0.97 0 0);
179
+ --sidebar-accent-foreground: oklch(0.205 0 0);
180
+ --sidebar-border: oklch(0.922 0 0);
181
+ --sidebar-ring: oklch(0.708 0 0);
182
+ }
183
+
184
+ .dark {
185
+ --background: oklch(0.145 0 0);
186
+ --foreground: oklch(0.985 0 0);
187
+ --card: oklch(0.205 0 0);
188
+ --card-foreground: oklch(0.985 0 0);
189
+ --popover: oklch(0.205 0 0);
190
+ --popover-foreground: oklch(0.985 0 0);
191
+ --primary: oklch(0.922 0 0);
192
+ --primary-foreground: oklch(0.205 0 0);
193
+ --secondary: oklch(0.269 0 0);
194
+ --secondary-foreground: oklch(0.985 0 0);
195
+ --muted: oklch(0.269 0 0);
196
+ --muted-foreground: oklch(0.708 0 0);
197
+ --accent: oklch(0.269 0 0);
198
+ --accent-foreground: oklch(0.985 0 0);
199
+ --destructive: oklch(0.704 0.191 22.216);
200
+ --border: oklch(1 0 0 / 10%);
201
+ --input: oklch(1 0 0 / 15%);
202
+ --ring: oklch(0.556 0 0);
203
+ --chart-1: oklch(0.87 0 0);
204
+ --chart-2: oklch(0.556 0 0);
205
+ --chart-3: oklch(0.439 0 0);
206
+ --chart-4: oklch(0.371 0 0);
207
+ --chart-5: oklch(0.269 0 0);
208
+ --sidebar: oklch(0.205 0 0);
209
+ --sidebar-foreground: oklch(0.985 0 0);
210
+ --sidebar-primary: oklch(0.488 0.243 264.376);
211
+ --sidebar-primary-foreground: oklch(0.985 0 0);
212
+ --sidebar-accent: oklch(0.269 0 0);
213
+ --sidebar-accent-foreground: oklch(0.985 0 0);
214
+ --sidebar-border: oklch(1 0 0 / 10%);
215
+ --sidebar-ring: oklch(0.556 0 0);
216
+ }
217
+
218
+ @layer base {
219
+ * {
220
+ @apply border-border outline-ring/50;
221
+ }
222
+ body {
223
+ @apply bg-background text-foreground;
224
+ }
225
+ html {
226
+ @apply font-sans;
227
+ }
228
+ }
@@ -0,0 +1,20 @@
1
+ import { MetadataRoute } from 'next'
2
+
3
+ export default function manifest(): MetadataRoute.Manifest {
4
+ return {
5
+ name: 'cistack | Automated GitHub Actions',
6
+ short_name: 'cistack',
7
+ description: 'Deep-scans your repository to generate production-ready GitHub Actions workflows.',
8
+ start_url: '/',
9
+ display: 'standalone',
10
+ background_color: '#ffffff',
11
+ theme_color: '#000000',
12
+ icons: [
13
+ {
14
+ src: '/favicon.ico',
15
+ sizes: 'any',
16
+ type: 'image/x-icon',
17
+ },
18
+ ],
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import { MetadataRoute } from 'next'
2
+
3
+ export default function robots(): MetadataRoute.Robots {
4
+ return {
5
+ rules: {
6
+ userAgent: '*',
7
+ allow: '/',
8
+ disallow: '/private/',
9
+ },
10
+ sitemap: 'https://cistack.edwinvakayil.info/sitemap.xml',
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import { MetadataRoute } from 'next'
2
+
3
+ export default function sitemap(): MetadataRoute.Sitemap {
4
+ return [
5
+ {
6
+ url: 'https://cistack.edwinvakayil.info',
7
+ lastModified: new Date(),
8
+ changeFrequency: 'weekly',
9
+ priority: 1,
10
+ },
11
+ ]
12
+ }
@@ -0,0 +1,219 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState, useCallback } from "react";
4
+
5
+ function cn(...classes: (string | undefined | null | false)[]) {
6
+ return classes.filter(Boolean).join(" ");
7
+ }
8
+
9
+ interface CanvasTextProps {
10
+ text: string;
11
+ className?: string;
12
+ backgroundClassName?: string;
13
+ colors?: string[];
14
+ animationDuration?: number;
15
+ lineWidth?: number;
16
+ lineGap?: number;
17
+ curveIntensity?: number;
18
+ overlay?: boolean;
19
+ }
20
+
21
+ function resolveColor(color: string): string {
22
+ if (color.startsWith("var(")) {
23
+ const varName = color.slice(4, -1).trim();
24
+ const resolved = getComputedStyle(document.documentElement)
25
+ .getPropertyValue(varName)
26
+ .trim();
27
+ return resolved || color;
28
+ }
29
+ return color;
30
+ }
31
+
32
+ export function CanvasText({
33
+ text,
34
+ className = "",
35
+ backgroundClassName = "bg-white dark:bg-neutral-950",
36
+ colors = ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#ffeaa7", "#dfe6e9"],
37
+ animationDuration = 5,
38
+ lineWidth = 1.5,
39
+ lineGap = 10,
40
+ curveIntensity = 60,
41
+ overlay = false,
42
+ }: CanvasTextProps) {
43
+ const canvasRef = useRef<HTMLCanvasElement>(null);
44
+ const textRef = useRef<HTMLSpanElement>(null);
45
+ const bgRef = useRef<HTMLSpanElement>(null);
46
+ const animationRef = useRef<number>(0);
47
+ const startTimeRef = useRef<number>(0);
48
+ const [bgColor, setBgColor] = useState("#0a0a0a");
49
+ const [resolvedColors, setResolvedColors] = useState<string[]>([]);
50
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
51
+ const [font, setFont] = useState("");
52
+
53
+ const updateColors = useCallback(() => {
54
+ if (bgRef.current) {
55
+ const computed = window.getComputedStyle(bgRef.current);
56
+ setBgColor(computed.backgroundColor);
57
+ }
58
+ const resolved = colors.map(resolveColor);
59
+ setResolvedColors(resolved);
60
+ }, [colors]);
61
+
62
+ useEffect(() => {
63
+ updateColors();
64
+
65
+ const observer = new MutationObserver(updateColors);
66
+ observer.observe(document.documentElement, {
67
+ attributes: true,
68
+ attributeFilter: ["class"],
69
+ });
70
+
71
+ return () => observer.disconnect();
72
+ }, [updateColors]);
73
+
74
+ useEffect(() => {
75
+ const textEl = textRef.current;
76
+ if (!textEl) return;
77
+
78
+ const updateDimensions = () => {
79
+ const rect = textEl.getBoundingClientRect();
80
+ const computed = window.getComputedStyle(textEl);
81
+ setDimensions({
82
+ width: Math.ceil(rect.width) + 8 || 400,
83
+ height: Math.ceil(rect.height) || 200,
84
+ });
85
+ setFont(
86
+ `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`,
87
+ );
88
+ };
89
+
90
+ updateDimensions();
91
+
92
+ const resizeObserver = new ResizeObserver(updateDimensions);
93
+ resizeObserver.observe(textEl);
94
+
95
+ return () => resizeObserver.disconnect();
96
+ }, [text, className]);
97
+
98
+ useEffect(() => {
99
+ const canvas = canvasRef.current;
100
+ if (
101
+ !canvas ||
102
+ resolvedColors.length === 0 ||
103
+ dimensions.width === 0 ||
104
+ !font
105
+ )
106
+ return;
107
+
108
+ const ctx = canvas.getContext("2d", { alpha: true });
109
+ if (!ctx) return;
110
+
111
+ const { width, height } = dimensions;
112
+ const dpr = window.devicePixelRatio || 1;
113
+
114
+ canvas.width = width * dpr;
115
+ canvas.height = height * dpr;
116
+
117
+ ctx.font = font;
118
+ const paddingMetrics = ctx.measureText("My");
119
+ const ascent = paddingMetrics.fontBoundingBoxAscent ?? paddingMetrics.actualBoundingBoxAscent;
120
+ const descent = paddingMetrics.fontBoundingBoxDescent ?? paddingMetrics.actualBoundingBoxDescent;
121
+ const baselineY = (height + ascent - descent) / 2;
122
+
123
+ const numLines = Math.floor(height / lineGap) + 10;
124
+ startTimeRef.current = performance.now();
125
+
126
+ const animate = (currentTime: number) => {
127
+ const elapsed = (currentTime - startTimeRef.current) / 1000;
128
+ const phase = (elapsed / animationDuration) * Math.PI * 2;
129
+
130
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
131
+ ctx.clearRect(0, 0, width, height);
132
+
133
+ ctx.globalCompositeOperation = "source-over";
134
+ ctx.font = font;
135
+ ctx.textBaseline = "alphabetic";
136
+ ctx.textAlign = "left";
137
+ ctx.fillStyle = "#000";
138
+ ctx.fillText(text, 0, baselineY);
139
+
140
+ ctx.globalCompositeOperation = "source-in";
141
+ ctx.fillStyle = bgColor;
142
+ ctx.fillRect(0, 0, width, height);
143
+
144
+ ctx.globalCompositeOperation = "source-atop";
145
+ for (let i = 0; i < numLines; i++) {
146
+ const y = i * lineGap;
147
+
148
+ const curve1 = Math.sin(phase) * curveIntensity;
149
+ const curve2 = Math.sin(phase + 0.5) * curveIntensity * 0.6;
150
+
151
+ const colorIndex = i % resolvedColors.length;
152
+ ctx.strokeStyle = resolvedColors[colorIndex];
153
+ ctx.lineWidth = lineWidth;
154
+
155
+ ctx.beginPath();
156
+ ctx.moveTo(0, y);
157
+ ctx.bezierCurveTo(
158
+ width * 0.33,
159
+ y + curve1,
160
+ width * 0.66,
161
+ y + curve2,
162
+ width,
163
+ y,
164
+ );
165
+ ctx.stroke();
166
+ }
167
+
168
+ animationRef.current = requestAnimationFrame(animate);
169
+ };
170
+
171
+ animationRef.current = requestAnimationFrame(animate);
172
+
173
+ return () => {
174
+ cancelAnimationFrame(animationRef.current);
175
+ };
176
+ }, [
177
+ text,
178
+ font,
179
+ bgColor,
180
+ resolvedColors,
181
+ animationDuration,
182
+ lineWidth,
183
+ lineGap,
184
+ curveIntensity,
185
+ dimensions,
186
+ ]);
187
+
188
+ return (
189
+ <span
190
+ className={cn(
191
+ "relative inline-block",
192
+ overlay && "absolute inset-0",
193
+ className,
194
+ )}
195
+ >
196
+ <span
197
+ ref={bgRef}
198
+ className={cn(
199
+ "pointer-events-none absolute h-0 w-0 opacity-0",
200
+ backgroundClassName,
201
+ )}
202
+ aria-hidden="true"
203
+ />
204
+ <span ref={textRef} className="invisible inline-block whitespace-nowrap" aria-hidden="true">
205
+ {text}
206
+ </span>
207
+ <canvas
208
+ ref={canvasRef}
209
+ className="pointer-events-none absolute top-0 left-0"
210
+ style={{
211
+ width: dimensions.width || "auto",
212
+ height: dimensions.height || "auto",
213
+ }}
214
+ aria-label={text}
215
+ role="img"
216
+ />
217
+ </span>
218
+ );
219
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ type CopyButtonVariant = "hero" | "terminal";
6
+
7
+ interface CopyButtonProps {
8
+ text: string;
9
+ variant?: CopyButtonVariant;
10
+ className?: string;
11
+ }
12
+
13
+ const copyLabels: Record<CopyButtonVariant, { idle: string; success: string }> = {
14
+ hero: {
15
+ idle: "COPY",
16
+ success: "COPIED",
17
+ },
18
+ terminal: {
19
+ idle: "Copy",
20
+ success: "Copied",
21
+ },
22
+ };
23
+
24
+ export default function CopyButton({
25
+ text,
26
+ variant = "hero",
27
+ className = "",
28
+ }: CopyButtonProps) {
29
+ const [copied, setCopied] = useState(false);
30
+ const timeoutRef = useRef<number | null>(null);
31
+
32
+ useEffect(() => {
33
+ return () => {
34
+ if (timeoutRef.current !== null) {
35
+ window.clearTimeout(timeoutRef.current);
36
+ }
37
+ };
38
+ }, []);
39
+
40
+ const handleCopy = async () => {
41
+ try {
42
+ await navigator.clipboard.writeText(text);
43
+ setCopied(true);
44
+
45
+ if (timeoutRef.current !== null) {
46
+ window.clearTimeout(timeoutRef.current);
47
+ }
48
+
49
+ timeoutRef.current = window.setTimeout(() => {
50
+ setCopied(false);
51
+ }, 2000);
52
+ } catch (error) {
53
+ console.error("Unable to copy command", error);
54
+ }
55
+ };
56
+
57
+ if (variant === "terminal") {
58
+ return (
59
+ <button
60
+ type="button"
61
+ onClick={handleCopy}
62
+ className={`text-[12px] font-semibold text-zinc-600 transition-colors hover:text-zinc-900 ${className}`}
63
+ aria-label="Copy command"
64
+ >
65
+ {copied ? copyLabels.terminal.success : copyLabels.terminal.idle}
66
+ </button>
67
+ );
68
+ }
69
+
70
+ return (
71
+ <button
72
+ type="button"
73
+ onClick={handleCopy}
74
+ className={`flex h-auto items-center justify-between gap-6 rounded-sm border border-zinc-800 bg-zinc-950 px-6 py-3.5 text-[14px] text-white shadow-[0_8px_30px_rgb(0,0,0,0.12)] transition-all hover:bg-black hover:shadow-zinc-300/50 ${className}`}
75
+ aria-label="Copy install command"
76
+ >
77
+ <span className="font-mono font-bold tracking-tight text-emerald-400 transition-colors">
78
+ {text}
79
+ </span>
80
+ <span className="ml-2 border-l border-zinc-800 pl-6 text-[12px] font-black uppercase tracking-[0.18em] text-zinc-300">
81
+ {copied ? copyLabels.hero.success : copyLabels.hero.idle}
82
+ </span>
83
+ </button>
84
+ );
85
+ }