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.
Files changed (61) 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 +215 -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 +101 -0
  19. package/product-site/components/HomeClient.tsx +664 -0
  20. package/product-site/components/InstallToggle.tsx +123 -0
  21. package/product-site/components/MotionRevealClient.tsx +53 -0
  22. package/product-site/components/TerminalCard.tsx +65 -0
  23. package/product-site/components/TerminalCardMotion.tsx +324 -0
  24. package/product-site/components/site-motion.tsx +229 -0
  25. package/product-site/components/ui/accordion.tsx +74 -0
  26. package/product-site/components/ui/badge.tsx +52 -0
  27. package/product-site/components/ui/button.tsx +60 -0
  28. package/product-site/components/ui/card.tsx +103 -0
  29. package/product-site/components/ui/checkbox.tsx +29 -0
  30. package/product-site/components/ui/separator.tsx +25 -0
  31. package/product-site/components/ui/table.tsx +116 -0
  32. package/product-site/components/ui/tabs.tsx +82 -0
  33. package/product-site/components.json +25 -0
  34. package/product-site/dictionaries/br.json +276 -0
  35. package/product-site/dictionaries/cn.json +276 -0
  36. package/product-site/dictionaries/de.json +276 -0
  37. package/product-site/dictionaries/en.json +274 -0
  38. package/product-site/dictionaries/es.json +276 -0
  39. package/product-site/dictionaries/fr.json +276 -0
  40. package/product-site/dictionaries/pt.json +276 -0
  41. package/product-site/eslint.config.mjs +18 -0
  42. package/product-site/lib/dictionaries.ts +18 -0
  43. package/product-site/lib/dictionary-types.ts +3 -0
  44. package/product-site/lib/utils.ts +6 -0
  45. package/product-site/middleware.ts +39 -0
  46. package/product-site/next.config.mjs +14 -0
  47. package/product-site/package-lock.json +14201 -0
  48. package/product-site/package.json +42 -0
  49. package/product-site/postcss.config.mjs +7 -0
  50. package/product-site/public/file.svg +1 -0
  51. package/product-site/public/globe.svg +1 -0
  52. package/product-site/public/next.svg +1 -0
  53. package/product-site/public/og-image.png +0 -0
  54. package/product-site/public/vercel.svg +1 -0
  55. package/product-site/public/window.svg +1 -0
  56. package/product-site/scripts/sync-i18n.mjs +58 -0
  57. package/product-site/scripts/validate-i18n.mjs +45 -0
  58. package/product-site/tsconfig.json +34 -0
  59. package/product-site/types/negotiator.d.ts +14 -0
  60. package/product-site/vercel.json +5 -0
  61. package/src/index.js +12 -13
@@ -0,0 +1,95 @@
1
+ import type { Metadata } from "next";
2
+ import { DM_Sans, Geist_Mono, Fira_Code } from "next/font/google";
3
+ import "../globals.css";
4
+
5
+ const dmSans = DM_Sans({
6
+ variable: "--font-dm-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ const firaCode = Fira_Code({
16
+ variable: "--font-fira-code",
17
+ subsets: ["latin"],
18
+ });
19
+
20
+ export const metadata: Metadata = {
21
+ metadataBase: new URL("https://cistack.edwinvakayil.info"),
22
+ title: {
23
+ default: "cistack | Automated GitHub Actions for Your Stack",
24
+ template: "%s | cistack"
25
+ },
26
+ description: "cistack deep-scans your repository to generate production-ready GitHub Actions workflows instantly. Supports 30+ frameworks and 12+ platforms with security-first defaults.",
27
+ keywords: ["github actions", "automation", "ci/cd", "devops", "workflow generator", "nextjs", "docker", "vercel", "aws", "firebase", "automated testing", "pipeline automation", "github workflow", "devops tools"],
28
+ authors: [{ name: "Edwin Vakayil", url: "https://www.edwinvakayil.info/" }],
29
+ creator: "Edwin Vakayil",
30
+ publisher: "Edwin Vakayil",
31
+ alternates: {
32
+ canonical: "/",
33
+ },
34
+ openGraph: {
35
+ title: "cistack | Automated GitHub Actions for Your Stack",
36
+ description: "Deep-scans your codebase to generate production-grade CI/CD pipelines in seconds. Support for 30+ frameworks.",
37
+ url: "https://cistack.edwinvakayil.info",
38
+ siteName: "cistack",
39
+ images: [
40
+ {
41
+ url: "/og-image.png",
42
+ width: 1200,
43
+ height: 630,
44
+ alt: "cistack - Automated GitHub Actions",
45
+ },
46
+ ],
47
+ locale: "en_US",
48
+ type: "website",
49
+ },
50
+ twitter: {
51
+ card: "summary_large_image",
52
+ title: "cistack | Professional Workflow Automation",
53
+ description: "Generate hardened GitHub Actions for any stack instantly. 30+ frameworks supported.",
54
+ creator: "@edwinvakayil",
55
+ images: ["/og-image.png"],
56
+ },
57
+ robots: {
58
+ index: true,
59
+ follow: true,
60
+ googleBot: {
61
+ index: true,
62
+ follow: true,
63
+ 'max-video-preview': -1,
64
+ 'max-image-preview': 'large',
65
+ 'max-snippet': -1,
66
+ },
67
+ },
68
+ category: 'technology',
69
+ };
70
+
71
+ export async function generateStaticParams() {
72
+ return [{ lang: 'en' }, { lang: 'fr' }, { lang: 'es' }, { lang: 'pt' }, { lang: 'br' }, { lang: 'de' }, { lang: 'cn' }];
73
+ }
74
+
75
+ export default async function RootLayout({
76
+ children,
77
+ params
78
+ }: {
79
+ children: React.ReactNode;
80
+ params: Promise<{ lang: string }>;
81
+ }) {
82
+ const { lang } = await params;
83
+ return (
84
+ <html
85
+ lang={lang}
86
+ className={`${dmSans.variable} ${geistMono.variable} ${firaCode.variable} h-full antialiased`}
87
+ >
88
+ <head>
89
+ <link rel="preconnect" href="https://registry.npmjs.org" />
90
+ <link rel="preconnect" href="https://api.npmjs.org" />
91
+ </head>
92
+ <body className="min-h-full flex flex-col">{children}</body>
93
+ </html>
94
+ );
95
+ }
@@ -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
+ }