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,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,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
|
+
}
|