cistack 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,34 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
- import {
5
- ArrowUpRight,
6
- Box,
7
- Check,
8
- Globe,
9
- Package,
10
- Shield,
11
- Terminal,
12
- } from "lucide-react";
4
+ import { useReducedMotion } from "framer-motion";
5
+ import { Globe, Package, Terminal } from "lucide-react";
13
6
  import { useEffect, useState } from "react";
14
7
 
15
8
  import CopyButton from "@/components/CopyButton";
16
9
  import InstallToggle from "@/components/InstallToggle";
17
10
  import TerminalCard from "@/components/TerminalCard";
18
- import { Badge } from "@/components/ui/badge";
11
+ import {
12
+ Accordion,
13
+ AccordionContent,
14
+ AccordionItem,
15
+ AccordionTrigger,
16
+ } from "@/components/ui/accordion";
17
+ import {
18
+ HeroStagger,
19
+ HeroStaggerItem,
20
+ MotionHeader,
21
+ MotionTagList,
22
+ Reveal,
23
+ SiteMotionRoot,
24
+ StaggerItem,
25
+ StaggerList,
26
+ m,
27
+ scrollViewport,
28
+ SITE_EASE,
29
+ } from "@/components/site-motion";
30
+ import { Separator } from "@/components/ui/separator";
19
31
  import type { Dictionary } from "@/lib/dictionary-types";
20
- import { motion } from "framer-motion";
21
32
 
22
33
  interface GithubIconProps {
23
34
  size?: number;
@@ -42,158 +53,6 @@ const localeOptions = [
42
53
  { code: "cn", label: "简体中文" },
43
54
  ] as const;
44
55
 
45
- const detectionPanels = [
46
- {
47
- id: "p1",
48
- idx: "S_01",
49
- name: "Firebase",
50
- signal: "firebase.json, firebase-tools dep",
51
- description:
52
- "Automatic Firebase Hosting detection with automated multi-project branch logic.",
53
- Icon: Package,
54
- },
55
- {
56
- id: "p2",
57
- idx: "S_02",
58
- name: "Vercel",
59
- signal: "vercel.json, .vercel dir, vercel dep",
60
- description:
61
- "Vercel deploy-gate generation with branch-aware environment provisioning.",
62
- Icon: Shield,
63
- },
64
- {
65
- id: "p3",
66
- idx: "S_03",
67
- name: "Netlify",
68
- signal: "netlify.toml, _redirects, netlify-cli dep",
69
- description:
70
- "Netlify edge detection with custom header and redirect validation.",
71
- Icon: Terminal,
72
- },
73
- {
74
- id: "p4",
75
- idx: "S_04",
76
- name: "GitHub Pages",
77
- signal: "gh-pages dep, github.io package.json",
78
- description:
79
- "Static site hosting recognition for the native GitHub Pages deployment service.",
80
- Icon: Globe,
81
- },
82
- {
83
- id: "p5",
84
- idx: "S_05",
85
- name: "AWS Cloud",
86
- signal: "serverless.yml, appspec.yml, cdk.json, aws-sdk dep",
87
- description:
88
- "Infrastructure-as-Code (IaC) recognition for AWS CDK, EC2, and Serverless.",
89
- Icon: Box,
90
- },
91
- {
92
- id: "p6",
93
- idx: "S_06",
94
- name: "GCP App Engine",
95
- signal: "app.yaml",
96
- description:
97
- "Automated deployment scanning for Google Cloud Platform App Engine targets.",
98
- Icon: Globe,
99
- },
100
- {
101
- id: "p7",
102
- idx: "S_07",
103
- name: "Azure",
104
- signal: "azure/pipelines.yml, @azure/* deps",
105
- description:
106
- "Azure App Service and native Azure pipeline definition identification.",
107
- Icon: Package,
108
- },
109
- {
110
- id: "p8",
111
- idx: "S_08",
112
- name: "Heroku",
113
- signal: "Procfile, heroku.yml",
114
- description:
115
- "Classic PaaS deployment via Heroku registry and git push logic.",
116
- Icon: Box,
117
- },
118
- {
119
- id: "p9",
120
- idx: "S_09",
121
- name: "Render / Railway",
122
- signal: "render.yaml, railway.json, railway.toml",
123
- description:
124
- "Modern PaaS detection utilizing automated app service descriptors.",
125
- Icon: Terminal,
126
- },
127
- {
128
- id: "p10",
129
- idx: "S_10",
130
- name: "Docker",
131
- signal: "Dockerfile, docker-compose.yml",
132
- description:
133
- "Containerization recognition with Buildx layer caching and GHCR authentication.",
134
- Icon: Box,
135
- },
136
- ] as const;
137
-
138
- const frameworkCoverage = [
139
- "Next.js",
140
- "Nuxt",
141
- "SvelteKit",
142
- "Remix",
143
- "Astro",
144
- "Vite",
145
- "React",
146
- "Vue",
147
- "Angular",
148
- "Svelte",
149
- "Gatsby",
150
- "Express",
151
- "Fastify",
152
- "NestJS",
153
- "Hono",
154
- "Koa",
155
- "tRPC",
156
- "Django",
157
- "Flask",
158
- "FastAPI",
159
- "Ruby on Rails",
160
- "Spring Boot",
161
- "Laravel",
162
- "Go (gin)",
163
- "Rust (Cargo)",
164
- ] as const;
165
-
166
- const testingTools = [
167
- "Jest",
168
- "Vitest",
169
- "Mocha",
170
- "Cypress",
171
- "Playwright",
172
- "Pytest",
173
- "RSpec",
174
- "Go Test",
175
- "Cargo Test",
176
- "PHPUnit",
177
- "JUnit/Maven",
178
- "Storybook",
179
- ] as const;
180
-
181
- const exampleStacks = [
182
- {
183
- name: "Next.js + Vercel",
184
- description: "ci.yml (lint, test, build), deploy.yml (vercel deploy), security.yml",
185
- },
186
- {
187
- name: "React + Firebase",
188
- description:
189
- "ci.yml, deploy.yml (firebase deploy --only hosting), security.yml",
190
- },
191
- {
192
- name: "Node.js API + Docker",
193
- description: "ci.yml, docker.yml (GHCR push), security.yml",
194
- },
195
- ] as const;
196
-
197
56
  const GithubIcon = ({ size = 24, className = "" }: GithubIconProps) => (
198
57
  <svg
199
58
  xmlns="http://www.w3.org/2000/svg"
@@ -211,6 +70,114 @@ const GithubIcon = ({ size = 24, className = "" }: GithubIconProps) => (
211
70
  </svg>
212
71
  );
213
72
 
73
+ function SectionKicker({ children }: { children: React.ReactNode }) {
74
+ return (
75
+ <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-zinc-400">{children}</p>
76
+ );
77
+ }
78
+
79
+ function SectionTitle({ children, className = "" }: { children: React.ReactNode; className?: string }) {
80
+ return (
81
+ <h2 className={`mt-1.5 text-xl font-semibold tracking-tight text-zinc-950 sm:text-2xl ${className}`}>
82
+ {children}
83
+ </h2>
84
+ );
85
+ }
86
+
87
+ function SnippetStack({
88
+ snippets,
89
+ copyLabels,
90
+ }: {
91
+ snippets: readonly string[];
92
+ copyLabels?: { idle: string; success: string };
93
+ }) {
94
+ const reduce = useReducedMotion();
95
+ if (snippets.length === 0) return null;
96
+ const preClass =
97
+ "min-w-0 flex-1 overflow-x-auto p-2.5 font-mono text-[11px] leading-relaxed text-zinc-900 whitespace-pre-wrap sm:text-xs";
98
+
99
+ return (
100
+ <div className="border border-zinc-200 bg-white">
101
+ {snippets.map((line, i) => (
102
+ <div key={`${line.slice(0, 48)}-${i}`}>
103
+ {i > 0 && <Separator className="bg-zinc-200" />}
104
+ <div className="flex min-h-11 items-stretch">
105
+ {reduce ? (
106
+ <pre className={preClass}>
107
+ <code>{line}</code>
108
+ </pre>
109
+ ) : (
110
+ <m.pre
111
+ className={preClass}
112
+ initial={{ opacity: 0, y: 8 }}
113
+ whileInView={{ opacity: 1, y: 0 }}
114
+ viewport={scrollViewport}
115
+ transition={{ duration: 0.38, ease: SITE_EASE, delay: i * 0.06 }}
116
+ >
117
+ <code>{line}</code>
118
+ </m.pre>
119
+ )}
120
+ {copyLabels ? (
121
+ <>
122
+ <Separator orientation="vertical" className="h-auto bg-zinc-200" />
123
+ <div className="flex shrink-0 items-center px-1">
124
+ <CopyButton
125
+ text={line}
126
+ idleLabel={copyLabels.idle}
127
+ successLabel={copyLabels.success}
128
+ />
129
+ </div>
130
+ </>
131
+ ) : null}
132
+ </div>
133
+ </div>
134
+ ))}
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function InstallCodeBlock({
140
+ command,
141
+ idleLabel,
142
+ successLabel,
143
+ }: {
144
+ command: string;
145
+ idleLabel: string;
146
+ successLabel: string;
147
+ }) {
148
+ const reduce = useReducedMotion();
149
+ const inner = (
150
+ <div className="flex min-h-12 items-stretch border border-zinc-200 bg-white">
151
+ <pre className="flex flex-1 items-center overflow-x-auto p-3 font-mono text-[13px] leading-snug text-zinc-900">
152
+ <code>{command}</code>
153
+ </pre>
154
+ <Separator orientation="vertical" className="bg-zinc-200" />
155
+ <div className="flex shrink-0 items-center px-3">
156
+ <CopyButton text={command} idleLabel={idleLabel} successLabel={successLabel} />
157
+ </div>
158
+ </div>
159
+ );
160
+ if (reduce) {
161
+ return inner;
162
+ }
163
+ return (
164
+ <m.div
165
+ initial={{ opacity: 0, scale: 0.985 }}
166
+ whileInView={{ opacity: 1, scale: 1 }}
167
+ viewport={scrollViewport}
168
+ transition={{ duration: 0.45, ease: SITE_EASE }}
169
+ >
170
+ {inner}
171
+ </m.div>
172
+ );
173
+ }
174
+
175
+ /** Bento rows: items-start prevents short columns (e.g. Detection) stretching to match a tall neighbor. */
176
+ const pad = "p-5 sm:p-6 lg:px-8 lg:py-6";
177
+ const bentoRow = "grid grid-cols-1 border-b border-zinc-200 lg:grid-cols-12 lg:items-start";
178
+ const colLeft = `${pad} border-b border-zinc-200 lg:border-b-0 lg:border-e lg:border-zinc-200`;
179
+ const colRight = pad;
180
+
214
181
  export default function HomeClient({
215
182
  dict,
216
183
  lang,
@@ -220,6 +187,7 @@ export default function HomeClient({
220
187
  }) {
221
188
  const [version, setVersion] = useState("3.0.0");
222
189
  const [downloads, setDownloads] = useState("2.4k");
190
+ const reduceMotion = useReducedMotion();
223
191
 
224
192
  useEffect(() => {
225
193
  let cancelled = false;
@@ -240,7 +208,9 @@ export default function HomeClient({
240
208
  const data = (await downloadsRes.json()) as DownloadStatsResponse;
241
209
  if (!cancelled && typeof data.downloads === "number") {
242
210
  const count = data.downloads;
243
- setDownloads(count >= 1000 ? `${(count / 1000).toFixed(1)}k` : count.toLocaleString());
211
+ setDownloads(
212
+ count >= 1000 ? `${(count / 1000).toFixed(1)}k` : count.toLocaleString()
213
+ );
244
214
  }
245
215
  }
246
216
  } catch (e) {
@@ -249,12 +219,12 @@ export default function HomeClient({
249
219
  };
250
220
 
251
221
  void loadStats();
252
- return () => { cancelled = true; };
222
+ return () => {
223
+ cancelled = true;
224
+ };
253
225
  }, []);
254
226
 
255
- const now = new Date();
256
- const manifestStamp = `${now.getFullYear()}_${(now.getMonth() + 1).toString().padStart(2, "0")}`;
257
- const currentYear = now.getFullYear();
227
+ const currentYear = new Date().getFullYear();
258
228
 
259
229
  return (
260
230
  <>
@@ -274,114 +244,89 @@ export default function HomeClient({
274
244
  priceCurrency: "USD",
275
245
  availability: "https://schema.org/InStock",
276
246
  },
277
- description: dict.hero.description,
247
+ description: `${dict.hero.tagline} ${dict.hero.intro}`,
278
248
  creator: {
279
249
  "@type": "Person",
280
250
  name: "Edwin Vakayil",
281
251
  url: "https://www.edwinvakayil.info/",
282
252
  },
283
- featureList: Object.values(dict.docs.capabilities).map((capability) => capability.label),
284
- keywords: "github actions, automation, ci/cd, devops, workflow generator, nextjs, docker, vercel, aws, firebase",
253
+ featureList: dict.why.items,
254
+ keywords:
255
+ "github actions, automation, ci/cd, devops, workflow generator, docker, vercel, aws, firebase",
285
256
  }),
286
257
  }}
287
258
  />
288
- <div
289
- className="page-root relative flex min-h-screen flex-col overflow-x-hidden bg-white text-zinc-900 selection:bg-black selection:text-white"
290
- style={{
291
- backgroundImage: "radial-gradient(circle at 1px 1px, rgba(0, 0, 0, 0.05) 1px, transparent 0)",
292
- backgroundSize: "24px 24px",
293
- }}
294
- >
295
- <motion.nav
296
- initial={{ opacity: 0, y: -10 }}
297
- animate={{ opacity: 1, y: 0 }}
298
- transition={{ duration: 0.8 }}
299
- className="relative z-50 mx-auto w-full max-w-[1400px] bg-white/80 backdrop-blur-md"
300
- >
301
- <div className="grid grid-cols-1 border-x border-b border-zinc-100 md:grid-cols-12">
302
- <div className="flex items-center gap-3 border-b border-zinc-100 p-5 md:col-span-3 md:border-b-0 md:border-r md:p-6">
303
- <div className="flex items-center gap-2 text-[20px] font-bold tracking-tighter text-zinc-950">
304
- <Terminal size={18} className="text-zinc-500" />
305
- cistack
306
- </div>
307
- <span className="rounded-sm border border-zinc-100 bg-zinc-50 px-1.5 py-0.5 font-mono text-[12px] font-bold text-zinc-500">
308
- V{version}
259
+ <div className="min-h-screen bg-white text-zinc-900 antialiased selection:bg-zinc-900 selection:text-white">
260
+ <SiteMotionRoot>
261
+ <MotionHeader className="sticky top-0 z-50 border-b border-zinc-200 bg-white/95 backdrop-blur-sm">
262
+ <div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6 lg:px-8">
263
+ <div className="flex flex-wrap items-center gap-3">
264
+ <Link
265
+ href={`/${lang}`}
266
+ className="flex items-center gap-2 text-lg font-semibold tracking-tight text-zinc-950"
267
+ >
268
+ <Terminal className="h-5 w-5 text-zinc-500" aria-hidden />
269
+ {dict.hero.product_name}
270
+ </Link>
271
+ <span className="border border-zinc-200 px-2 py-0.5 font-mono text-xs font-medium text-zinc-600">
272
+ {dict.navigation.version} {version}
273
+ </span>
274
+ <span className="text-xs font-medium uppercase tracking-wider text-zinc-400">
275
+ {dict.navigation.status}
309
276
  </span>
310
277
  </div>
311
-
312
- <div className="flex items-center justify-between border-b border-zinc-100 p-5 md:col-span-6 md:border-b-0 md:border-r md:p-6">
313
- <div className="flex items-center gap-6">
314
- <div className="flex flex-col gap-0.5">
315
- <span className="text-[12px] font-black uppercase leading-none tracking-[0.2em] text-zinc-500">
316
- {dict.navigation.version}
317
- </span>
318
- <div className="flex items-center gap-2">
319
- <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
320
- <span className="text-[12px] font-bold uppercase tracking-tighter text-zinc-600">
321
- {dict.navigation.status}
322
- </span>
323
- </div>
324
- </div>
325
- <div className="mx-2 hidden h-6 w-px bg-zinc-100 lg:block" />
326
- <div className="hidden items-center gap-6 text-[12px] font-bold text-zinc-500 lg:flex">
327
- <a
328
- href="https://github.com/edwinvakayil/cistack"
329
- target="_blank"
330
- rel="noopener noreferrer"
331
- className="group flex items-center gap-2 uppercase tracking-widest transition-colors hover:text-zinc-950"
332
- >
333
- <GithubIcon size={14} className="opacity-40 transition-opacity group-hover:opacity-100" />
334
- {dict.navigation.repository}
335
- </a>
336
- <a
337
- href="https://www.npmjs.com/package/cistack"
338
- target="_blank"
339
- rel="noopener noreferrer"
340
- className="group flex items-center gap-2 uppercase tracking-widest transition-colors hover:text-zinc-950"
341
- >
342
- <Package size={14} className="opacity-40 transition-opacity group-hover:opacity-100" />
343
- {dict.navigation.registry}
344
- </a>
345
- </div>
346
- </div>
347
-
348
- <div className="flex shrink-0 items-center gap-1.5 sm:gap-2">
278
+ <nav className="flex flex-wrap items-center gap-4 text-sm font-medium text-zinc-600">
279
+ <a
280
+ href="https://github.com/edwinvakayil/cistack"
281
+ target="_blank"
282
+ rel="noopener noreferrer"
283
+ className="inline-flex items-center gap-1.5 transition-colors hover:text-zinc-950"
284
+ >
285
+ <GithubIcon size={16} className="opacity-70" />
286
+ {dict.navigation.repository}
287
+ </a>
288
+ <a
289
+ href="https://www.npmjs.com/package/cistack"
290
+ target="_blank"
291
+ rel="noopener noreferrer"
292
+ className="inline-flex items-center gap-1.5 transition-colors hover:text-zinc-950"
293
+ >
294
+ <Package size={16} className="opacity-70" />
295
+ {dict.navigation.registry}
296
+ </a>
297
+ <a href="#reference" className="transition-colors hover:text-zinc-950">
298
+ {dict.navigation.reference}
299
+ </a>
300
+ <div className="flex items-center gap-2 border-s border-zinc-200 ps-4">
349
301
  <Link
350
302
  href="/en"
351
- className={`rounded-sm border px-1.5 py-0.5 text-[12px] font-bold uppercase tracking-widest transition-colors ${
303
+ className={`border px-2 py-1 text-xs font-semibold uppercase tracking-wide transition-colors ${
352
304
  lang === "en"
353
- ? "border-zinc-950 bg-zinc-950 text-white"
354
- : "border-transparent text-zinc-500 hover:border-zinc-100 hover:text-zinc-950"
305
+ ? "border-zinc-900 bg-zinc-900 text-white"
306
+ : "border-transparent text-zinc-500 hover:text-zinc-900"
355
307
  }`}
356
308
  >
357
309
  EN
358
310
  </Link>
359
311
  {lang !== "en" && (
360
- <>
361
- <div className="mx-0.5 h-3 w-px bg-zinc-100" />
362
- <Link
363
- href={`/${lang}`}
364
- className="rounded-sm border border-zinc-950 bg-zinc-950 px-1.5 py-0.5 text-[12px] font-bold uppercase tracking-widest text-white"
365
- >
366
- {lang.toUpperCase()}
367
- </Link>
368
- </>
369
- )}
370
-
371
- <details className="group/lang relative ml-1 border-l border-zinc-100 pl-2">
372
- <summary
373
- aria-label="Change language"
374
- className="flex list-none cursor-pointer items-center gap-1 py-1 text-[12px] font-bold uppercase tracking-[0.14em] text-zinc-600 transition-colors hover:text-zinc-950 [&::-webkit-details-marker]:hidden"
312
+ <Link
313
+ href={`/${lang}`}
314
+ className="border border-zinc-900 bg-zinc-900 px-2 py-1 text-xs font-semibold uppercase tracking-wide text-white"
375
315
  >
376
- <Globe size={13} className="text-current" />
316
+ {lang.toUpperCase()}
317
+ </Link>
318
+ )}
319
+ <details className="relative">
320
+ <summary className="flex cursor-pointer list-none items-center gap-1 py-1 text-xs font-semibold uppercase tracking-wide text-zinc-600 hover:text-zinc-950 [&::-webkit-details-marker]:hidden">
321
+ <Globe size={14} aria-hidden />
377
322
  Lang
378
323
  </summary>
379
- <div className="absolute right-0 top-full z-[100] mt-2 flex w-32 flex-col gap-1 rounded-sm border border-zinc-200 bg-white p-1 shadow-xl">
324
+ <div className="absolute inset-e-0 top-full z-100 mt-2 flex min-w-36 flex-col gap-0.5 border border-zinc-200 bg-white p-1 shadow-lg">
380
325
  {localeOptions.map((locale) => (
381
326
  <Link
382
327
  key={locale.code}
383
328
  href={`/${locale.code}`}
384
- className="rounded-sm p-2 text-[12px] font-bold uppercase tracking-[0.12em] text-zinc-700 transition-colors hover:bg-zinc-50 hover:text-zinc-950"
329
+ className="px-3 py-2 text-xs font-medium text-zinc-700 hover:bg-zinc-50"
385
330
  >
386
331
  {locale.label}
387
332
  </Link>
@@ -389,596 +334,330 @@ export default function HomeClient({
389
334
  </div>
390
335
  </details>
391
336
  </div>
392
- </div>
393
-
394
- <div className="flex items-center justify-between gap-6 bg-zinc-50/20 p-5 md:col-span-3 md:justify-end md:p-6">
395
- <span className="font-mono text-[12px] font-bold uppercase tracking-[0.2em] text-zinc-500">
396
- {`${dict.navigation.core_manifest} // ${manifestStamp}`}
397
- </span>
398
- <a
399
- href="#docs"
400
- className="rounded-sm border border-zinc-900 px-4 py-2 text-[12px] font-black uppercase tracking-widest text-zinc-900 transition-all hover:bg-zinc-950 hover:text-white"
401
- >
402
- {dict.navigation.docs}
403
- </a>
404
- </div>
337
+ </nav>
405
338
  </div>
406
- </motion.nav>
407
-
408
- <main className="relative z-10 mx-auto w-full max-w-[1500px] px-4 pt-6 sm:px-6 md:px-8 lg:pt-12">
409
- <div className="pointer-events-none absolute inset-x-0 top-0 -mt-40 h-[600px] bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] opacity-[0.35] [background-size:24px_24px] [mask-image:linear-gradient(to_bottom,black,transparent)]" />
410
-
411
- <div className="relative grid grid-cols-1 border-l border-t border-zinc-100 bg-white/40 lg:grid-cols-12">
412
- <div className="group/hero relative border-b border-r border-zinc-100 p-8 sm:p-12 lg:col-span-7 lg:p-16">
413
- <motion.div
414
- initial={{ opacity: 0, y: 15 }}
415
- whileInView={{ opacity: 1, y: 0 }}
416
- viewport={{ once: true }}
417
- transition={{ duration: 0.6 }}
418
- className="relative z-10 flex flex-col gap-8"
419
- >
420
- <div className="flex items-center gap-3">
421
- <Badge
422
- variant="outline"
423
- className="rounded-sm border-zinc-200 bg-white px-2 py-0.5 text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500"
424
- >
425
- VERSION_{version}
426
- </Badge>
427
- <div className="h-px w-12 bg-zinc-100" />
428
- <span className="font-mono text-[12px] font-bold uppercase tracking-widest text-zinc-500">
429
- {dict.hero.scan_identity}
430
- </span>
431
- </div>
432
-
433
- <div className="flex flex-col gap-8 md:gap-8">
434
- <div className="group/h1 flex flex-col gap-1 sm:flex-row sm:items-baseline sm:gap-4">
435
- <span className="font-mono text-[12px] font-bold text-zinc-400 transition-colors sm:text-zinc-300 md:text-[12px] group-hover/h1:text-zinc-500">
436
- {dict.hero.s1_label}
437
- </span>
438
- <h1 className="break-words text-[2.2rem] font-[1000] leading-[0.9] tracking-tighter text-black min-[400px]:text-[2.5rem] sm:leading-none md:text-[3.8rem]">
439
- {dict.hero.s1_title}
440
- </h1>
441
- </div>
442
-
443
- <div className="group/h2 flex flex-col gap-1 sm:ml-6 sm:flex-row sm:items-baseline sm:gap-4 md:ml-12">
444
- <span className="font-mono text-[12px] font-bold text-zinc-400 transition-colors sm:text-zinc-300 md:text-[12px] group-hover/h2:text-zinc-500">
445
- {dict.hero.s2_label}
446
- </span>
447
- <h2 className="break-words text-[2.2rem] font-[1000] leading-[0.9] tracking-tighter text-black min-[400px]:text-[2.5rem] sm:leading-none md:text-[3.8rem]">
448
- {dict.hero.s2_title}
449
- </h2>
450
- </div>
451
-
452
- <div className="group/h3 flex flex-col gap-1 sm:ml-12 sm:flex-row sm:items-baseline sm:gap-4 md:ml-24">
453
- <span className="font-mono text-[12px] font-bold text-zinc-400 transition-colors sm:text-zinc-300 md:text-[12px] group-hover/h3:text-zinc-500">
454
- {dict.hero.s3_label}
455
- </span>
456
- <h2 className="break-words text-[2.2rem] font-[1000] leading-[0.9] tracking-tighter text-zinc-500 min-[400px]:text-[2.5rem] sm:leading-none sm:text-zinc-500 md:text-[3.8rem]">
457
- {dict.hero.s3_title}
458
- </h2>
459
- </div>
460
- </div>
461
-
462
- <p className="max-w-[540px] text-xs font-medium leading-relaxed text-zinc-500 sm:text-sm">
463
- {dict.hero.description}
464
- </p>
465
-
466
- <div className="mt-4 flex flex-wrap items-center gap-6">
467
- <CopyButton text={dict.hero.npx_command} variant="hero" />
468
- <div className="flex flex-col gap-1 border-l-2 border-zinc-100 px-2">
469
- <span className="text-[12px] font-black uppercase tracking-widest text-zinc-500">
470
- {dict.hero.active_installs}
471
- </span>
472
- <span className="text-[14px] font-bold tracking-tight text-zinc-900">
473
- {downloads} {dict.hero.per_week}
474
- </span>
475
- </div>
476
- </div>
477
- </motion.div>
478
- </div>
479
-
480
- <div className="flex flex-col border-b border-r border-zinc-100 lg:col-span-5">
481
- <div className="group/runtime flex flex-1 items-center justify-center overflow-hidden bg-zinc-50/20 p-8 transition-colors duration-700 hover:bg-zinc-50/40 sm:p-12">
482
- <div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] opacity-[0.2] [background-size:16px_16px]" />
483
- <motion.div
484
- initial={{ opacity: 0, scale: 0.98 }}
485
- whileInView={{ opacity: 1, scale: 1 }}
486
- viewport={{ once: true }}
487
- transition={{ duration: 0.8, delay: 0.1 }}
488
- className="relative z-20 w-full overflow-hidden rounded-sm"
489
- >
490
- <TerminalCard dict={dict.terminal} version={version} />
491
- </motion.div>
492
- <div className="absolute right-8 top-6 hidden items-center gap-2 sm:flex">
493
- <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
494
- <span className="font-mono text-[12px] font-black uppercase tracking-widest text-zinc-500">
495
- {dict.hero.realtime_synth}
496
- </span>
497
- </div>
339
+ </MotionHeader>
340
+
341
+ <main className="mx-auto max-w-6xl px-4 pb-14 pt-8 sm:px-6 lg:px-8">
342
+ <div className="border border-zinc-200 bg-white">
343
+ {/* Hero + metrics */}
344
+ <Reveal className={bentoRow} y={22}>
345
+ <div className={`${colLeft} lg:col-span-8`}>
346
+ <HeroStagger>
347
+ <HeroStaggerItem>
348
+ <SectionKicker>{dict.hero.live_registry}</SectionKicker>
349
+ </HeroStaggerItem>
350
+ <HeroStaggerItem>
351
+ <SectionTitle>{dict.hero.tagline}</SectionTitle>
352
+ </HeroStaggerItem>
353
+ <HeroStaggerItem>
354
+ <Separator className="my-4 bg-zinc-200" />
355
+ </HeroStaggerItem>
356
+ <HeroStaggerItem>
357
+ <p className="max-w-2xl text-pretty text-sm leading-relaxed text-zinc-600 sm:text-base">
358
+ {dict.hero.intro}
359
+ </p>
360
+ </HeroStaggerItem>
361
+ </HeroStagger>
498
362
  </div>
499
-
500
- <div className="grid grid-cols-2 border-t border-zinc-100 bg-white">
501
- <div className="group/meta flex cursor-default flex-col gap-2 border-r border-zinc-100 p-8 transition-colors hover:bg-zinc-50/50">
502
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500 transition-colors group-hover/meta:text-zinc-700">
503
- {dict.hero.integrations}
504
- </span>
505
- <div className="flex items-center gap-2">
506
- <span className="text-[20px] font-black leading-none text-zinc-900">30+</span>
507
- <span className="text-[12px] font-bold uppercase tracking-tighter text-zinc-500">
508
- {dict.hero.stack_aware}
509
- </span>
363
+ {reduceMotion ? (
364
+ <div className={`${colRight} lg:col-span-4`}>
365
+ <SectionKicker>{dict.hero.weekly_downloads}</SectionKicker>
366
+ <p className="mt-2 text-3xl font-semibold tracking-tight text-zinc-950">{downloads}</p>
367
+ <p className="mt-0.5 text-sm text-zinc-500">{dict.hero.per_week}</p>
368
+ <Separator className="my-4 bg-zinc-200" />
369
+ <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-zinc-400">
370
+ {dict.install.quick_command}
371
+ </p>
372
+ <div className="mt-2">
373
+ <InstallCodeBlock
374
+ command={dict.hero.npx_command}
375
+ idleLabel={dict.copy_button.idle}
376
+ successLabel={dict.copy_button.success}
377
+ />
510
378
  </div>
511
379
  </div>
512
- <div className="group/meta flex cursor-default flex-col gap-2 bg-zinc-50/30 p-8 transition-colors hover:bg-zinc-50/60">
513
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500 transition-colors group-hover/meta:text-zinc-700">
514
- {dict.hero.success_rate}
515
- </span>
516
- <div className="flex items-center gap-2">
517
- <span className="text-[20px] font-black leading-none tracking-tighter text-emerald-600">99.9%</span>
518
- <Check size={14} className="text-emerald-500/50" />
380
+ ) : (
381
+ <m.div
382
+ className={`${colRight} lg:col-span-4`}
383
+ initial={{ opacity: 0, y: 26, x: 12 }}
384
+ animate={{ opacity: 1, y: 0, x: 0 }}
385
+ transition={{ duration: 0.58, ease: SITE_EASE, delay: 0.18 }}
386
+ >
387
+ <SectionKicker>{dict.hero.weekly_downloads}</SectionKicker>
388
+ <p className="mt-2 text-3xl font-semibold tracking-tight text-zinc-950">{downloads}</p>
389
+ <p className="mt-0.5 text-sm text-zinc-500">{dict.hero.per_week}</p>
390
+ <Separator className="my-4 bg-zinc-200" />
391
+ <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-zinc-400">
392
+ {dict.install.quick_command}
393
+ </p>
394
+ <div className="mt-2">
395
+ <InstallCodeBlock
396
+ command={dict.hero.npx_command}
397
+ idleLabel={dict.copy_button.idle}
398
+ successLabel={dict.copy_button.success}
399
+ />
519
400
  </div>
520
- </div>
521
- </div>
522
- </div>
523
- </div>
524
- </main>
525
-
526
- <section id="docs" className="relative z-10 mx-auto w-full max-w-[1400px] px-4 py-24 pb-40 sm:px-6 md:px-8">
527
- <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] opacity-[0.4] [background-size:32px_32px]" />
528
-
529
- <div className="mb-16 flex max-w-[900px] flex-col gap-4">
530
- <div className="flex items-center gap-3">
531
- <Badge
532
- variant="outline"
533
- className="rounded-sm border-zinc-200 bg-white px-3 py-1 text-[12px] font-black uppercase tracking-[0.2em] text-zinc-700"
534
- >
535
- {dict.docs.badge}
536
- {version}
537
- </Badge>
538
- <div className="h-px flex-1 bg-zinc-200/60" />
539
- </div>
540
- <h2 className="text-[36px] font-bold leading-[1.1] tracking-tight text-zinc-900 sm:text-[44px]">
541
- {dict.docs.title_part1}
542
- <br />
543
- {dict.docs.title_part2}
544
- </h2>
545
- <p className="max-w-[760px] text-xs font-medium leading-relaxed text-zinc-500 sm:text-sm">
546
- {dict.docs.description}
547
- </p>
548
- </div>
549
-
550
- <motion.div
551
- initial={{ opacity: 0, y: 20 }}
552
- whileInView={{ opacity: 1, y: 0 }}
553
- viewport={{ once: true }}
554
- transition={{ duration: 0.8 }}
555
- className="grid grid-cols-1 border-l border-t border-zinc-100 lg:grid-cols-12"
556
- >
557
- <div className="group/cell relative border-b border-r border-zinc-100 bg-zinc-50/20 p-8 transition-colors duration-500 hover:bg-zinc-50/40 lg:col-span-4 lg:p-12">
558
- <div className="absolute right-4 top-4 font-mono text-[12px] font-bold text-zinc-500 transition-colors group-hover/cell:text-zinc-700">
559
- {dict.docs.section1_id}
401
+ </m.div>
402
+ )}
403
+ </Reveal>
404
+
405
+ {/* Install + preview */}
406
+ <Reveal className={bentoRow} delay={0.04} y={20}>
407
+ <div className={`${colLeft} lg:col-span-5`}>
408
+ <SectionTitle className="mb-3">{dict.install.title}</SectionTitle>
409
+ <InstallToggle dict={dict} />
410
+ <Separator className="my-4 bg-zinc-200" />
411
+ <p className="text-sm leading-relaxed text-zinc-600">{dict.install.node_note}</p>
560
412
  </div>
561
- <div className="flex flex-col gap-10">
562
- <div className="flex flex-col gap-2">
563
- <span className="text-[12px] font-black uppercase tracking-[0.18em] text-zinc-500">
564
- {dict.docs.section1_num}
565
- </span>
566
- <h3 className="text-[18px] font-bold text-zinc-900">{dict.docs.section1_title}</h3>
413
+ <div className={`${colRight} lg:col-span-7`}>
414
+ <SectionTitle className="mb-1">{dict.preview.title}</SectionTitle>
415
+ <p className="text-sm text-zinc-500">{dict.preview.caption}</p>
416
+ <Separator className="my-4 bg-zinc-200" />
417
+ <div className="min-h-[260px] sm:min-h-[300px] lg:min-h-[320px]">
418
+ <TerminalCard
419
+ dict={dict.terminal}
420
+ version={version}
421
+ copyLabels={dict.copy_button}
422
+ />
567
423
  </div>
568
- <div className="flex flex-col divide-y divide-zinc-200/40">
569
- {Object.values(dict.docs.capabilities).map((item) => (
570
- <div key={item.label} className="flex items-start gap-3 py-3.5 first:pt-0 last:pb-0">
571
- <span
572
- role="img"
573
- aria-label="Available feature"
574
- className="mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-[4px] border border-zinc-200 bg-zinc-900 text-white"
575
- >
576
- <Check size={11} strokeWidth={3} />
577
- </span>
578
- <div className="flex flex-col">
579
- <span className="text-[14px] font-semibold leading-tight text-zinc-800">{item.label}</span>
580
- <span className="mt-0.5 text-[12px] leading-snug text-zinc-500">{item.sub}</span>
581
- </div>
582
- </div>
424
+ </div>
425
+ </Reveal>
426
+
427
+ {/* Why */}
428
+ <Reveal className={bentoRow} delay={0.06} y={20}>
429
+ <div id="reference" className={`${pad} scroll-mt-24 lg:col-span-12`}>
430
+ <SectionKicker>{dict.navigation.reference}</SectionKicker>
431
+ <SectionTitle>{dict.why.title}</SectionTitle>
432
+ <Separator className="my-4 bg-zinc-200" />
433
+ <StaggerList className="grid gap-2.5 text-sm leading-snug text-zinc-700 sm:grid-cols-2 sm:gap-x-8 sm:gap-y-2">
434
+ {dict.why.items.map((item) => (
435
+ <StaggerItem key={item} className="flex gap-2.5">
436
+ <span className="mt-1.5 h-1 w-1 shrink-0 bg-zinc-400" aria-hidden />
437
+ <span>{item}</span>
438
+ </StaggerItem>
583
439
  ))}
584
- </div>
440
+ </StaggerList>
585
441
  </div>
586
- </div>
587
-
588
- <div className="flex flex-col lg:col-span-8">
589
- <div className="border-b border-r border-zinc-100 p-8 lg:p-12">
590
- <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:gap-16">
591
- <div className="flex flex-col gap-6">
592
- <div className="flex flex-col gap-2">
593
- <span className="text-[12px] font-black uppercase tracking-[0.18em] text-zinc-500">
594
- {dict.docs.section2_num}
595
- </span>
596
- <h3 className="text-[18px] font-bold text-zinc-900">{dict.docs.section2_title}</h3>
597
- </div>
598
- <InstallToggle dict={dict} />
599
-
600
- <div className="mt-2 flex flex-col gap-3">
601
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
602
- {dict.docs.command_registry}
603
- </span>
604
- <div className="flex flex-col gap-2">
605
- {[
606
- { command: "audit", description: dict.docs.commands.audit },
607
- { command: "upgrade", description: dict.docs.commands.upgrade },
608
- { command: "init", description: dict.docs.commands.init },
609
- ].map((cmd) => (
610
- <div
611
- key={cmd.command}
612
- className="group flex items-center justify-between rounded-sm border border-zinc-100 bg-zinc-50/50 px-3 py-2 transition-colors hover:border-zinc-200"
613
- >
614
- <code className="text-[12px] font-extrabold text-zinc-800">
615
- npx cistack {cmd.command}
616
- </code>
617
- <span className="text-[12px] font-medium text-zinc-500">{cmd.description}</span>
618
- </div>
442
+ </Reveal>
443
+
444
+ {/* CLI + Generated */}
445
+ <Reveal className={bentoRow} delay={0.08} y={18}>
446
+ <div className={`${colLeft} lg:col-span-6`}>
447
+ <SectionTitle className="mb-3">{dict.cli.section_title}</SectionTitle>
448
+ <Accordion multiple className="w-full border-t border-zinc-200">
449
+ {dict.cli.items.map((item, i) => (
450
+ <AccordionItem key={item.title} value={`cli-${i}`} className="border-zinc-200">
451
+ <AccordionTrigger className="py-3 text-left text-sm font-semibold text-zinc-900 hover:no-underline">
452
+ {item.title}
453
+ </AccordionTrigger>
454
+ <AccordionContent className="space-y-3 pb-4 text-zinc-600">
455
+ {item.paragraphs.map((p) => (
456
+ <p key={p} className="text-sm leading-relaxed">
457
+ {p}
458
+ </p>
619
459
  ))}
620
- </div>
621
- </div>
622
- <p className="text-[13px] italic text-zinc-400">{dict.docs.recommended_npx}</p>
623
- </div>
624
- <div className="mt-4 flex flex-col gap-4">
625
- <div className="flex items-center gap-3">
626
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
627
- {dict.docs.parameters_manifest}
628
- </span>
629
- <div className="h-px flex-1 bg-zinc-200 opacity-60" />
630
- </div>
631
-
632
- <div className="flex flex-col divide-y divide-zinc-100 border-y border-zinc-200 bg-white/40">
633
- {[
634
- { flag: "--explain", type: "boolean", description: dict.docs.flags.explain },
635
- { flag: "--path", type: "string", description: dict.docs.flags.path },
636
- { flag: "--output", type: "string", description: dict.docs.flags.output },
637
- { flag: "--dry-run", type: "boolean", description: dict.docs.flags.dry_run },
638
- { flag: "--no-prompt", type: "boolean", description: dict.docs.flags.no_prompt },
639
- { flag: "--verbose", type: "boolean", description: dict.docs.flags.verbose },
640
- { flag: "--force", type: "boolean", description: dict.docs.flags.force },
641
- ].map((flag) => (
642
- <div
643
- key={flag.flag}
644
- className="group flex flex-col px-2 py-3 transition-colors hover:bg-zinc-50/80 md:flex-row md:items-center"
645
- >
646
- <div className="flex shrink-0 items-center gap-3 md:w-[150px]">
647
- <div className="h-[12px] w-[3px] bg-zinc-200 transition-colors group-hover:bg-emerald-500" />
648
- <code className="font-mono text-[12px] font-bold tracking-tight text-zinc-800">
649
- {flag.flag}
650
- </code>
651
- </div>
652
- <div className="mt-2 flex flex-1 flex-row items-center gap-4 pl-[15px] md:mt-0 md:pl-0">
653
- <span className="rounded-sm bg-zinc-100 px-1.5 py-0.5 font-mono text-[12px] uppercase tracking-widest text-zinc-600">
654
- {flag.type}
655
- </span>
656
- <span className="text-[12px] font-medium text-zinc-600">{flag.description}</span>
657
- </div>
658
- </div>
659
- ))}
660
- </div>
661
- </div>
662
- </div>
460
+ <SnippetStack snippets={item.snippets} copyLabels={dict.copy_button} />
461
+ </AccordionContent>
462
+ </AccordionItem>
463
+ ))}
464
+ </Accordion>
663
465
  </div>
664
-
665
- <div className="grid flex-1 grid-cols-1 border-b border-r border-zinc-100 md:grid-cols-2">
666
- <div className="border-b border-zinc-100 p-8 md:border-b-0 md:border-r lg:p-12">
667
- <div className="flex flex-col gap-8">
668
- <div className="flex flex-col gap-2">
669
- <h4 className="text-[14px] font-bold uppercase tracking-widest text-zinc-900">
670
- {dict.docs.detection_logic_title}
671
- </h4>
672
- <p className="text-[13px] font-medium leading-relaxed text-zinc-400">
673
- {dict.docs.detection_logic_desc}
674
- </p>
675
- </div>
676
-
677
- <div className="max-h-[250px] w-full overflow-y-auto overflow-x-hidden pr-3 sm:max-h-[265px] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-zinc-200 hover:[&::-webkit-scrollbar-thumb]:bg-zinc-300 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-1">
678
- <div className="w-full">
679
- {detectionPanels.map((panel) => (
680
- <details key={panel.id} className="group border-b border-zinc-100 last:border-0">
681
- <summary className="list-none cursor-pointer py-4 text-zinc-500 transition-colors hover:text-zinc-900 sm:py-5 [&::-webkit-details-marker]:hidden">
682
- <div className="flex min-w-0 flex-1 flex-col justify-center gap-1.5 overflow-hidden text-start sm:gap-2">
683
- <div className="flex shrink-0 items-center gap-3 sm:gap-4">
684
- <span className="w-[24px] shrink-0 font-mono text-[12px] font-bold text-zinc-500 transition-colors group-hover:text-zinc-700">
685
- {panel.idx}
686
- </span>
687
- <div className="flex items-center gap-2 sm:gap-3">
688
- <panel.Icon
689
- size={14}
690
- className="shrink-0 text-zinc-600 transition-colors group-hover:text-zinc-900"
691
- />
692
- <span className="text-[15px] font-semibold whitespace-nowrap sm:text-[16px]">
693
- {panel.name}
694
- </span>
695
- </div>
696
- <span className="ml-auto text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
697
- OPEN
698
- </span>
699
- </div>
700
- <div className="flex w-full justify-start overflow-hidden ps-[36px] sm:ps-[44px]">
701
- <Badge
702
- variant="outline"
703
- className="max-w-full truncate rounded-sm border-zinc-200 bg-zinc-50/50 text-[12px] font-mono font-normal tracking-tight opacity-80"
704
- >
705
- {panel.signal}
706
- </Badge>
707
- </div>
708
- </div>
709
- </summary>
710
- <div className="border-s-2 border-zinc-200 pb-6 ps-6">
711
- <p className="max-w-[440px] text-[15px] leading-relaxed text-zinc-500">{panel.description}</p>
712
- <div className="mt-4 flex items-center gap-2">
713
- <span className="text-[12px] font-bold uppercase tracking-widest text-zinc-500">
714
- {dict.docs.signal_source}
715
- </span>
716
- <code className="rounded-sm border border-zinc-100 bg-white px-2 py-0.5 font-mono text-[12px] text-zinc-600">
717
- {panel.signal}
718
- </code>
719
- </div>
720
- </div>
721
- </details>
466
+ <div className={`${colRight} lg:col-span-6`}>
467
+ <SectionTitle className="mb-3">{dict.generated.section_title}</SectionTitle>
468
+ <Accordion multiple className="w-full border-t border-zinc-200">
469
+ {dict.generated.items.map((item, i) => (
470
+ <AccordionItem key={item.title} value={`gen-${i}`} className="border-zinc-200">
471
+ <AccordionTrigger className="py-3 text-left text-sm font-semibold text-zinc-900 hover:no-underline">
472
+ {item.title}
473
+ </AccordionTrigger>
474
+ <AccordionContent className="space-y-3 pb-4 text-zinc-600">
475
+ {item.paragraphs.map((p) => (
476
+ <p key={p} className="text-sm leading-relaxed">
477
+ {p}
478
+ </p>
722
479
  ))}
480
+ <SnippetStack snippets={item.snippets} copyLabels={dict.copy_button} />
481
+ </AccordionContent>
482
+ </AccordionItem>
483
+ ))}
484
+ </Accordion>
485
+ </div>
486
+ </Reveal>
487
+
488
+ {/* Detection + Configuration */}
489
+ <Reveal className={bentoRow} delay={0.1} y={18}>
490
+ <div className={`${colLeft} lg:col-span-6`}>
491
+ <SectionTitle className="mb-3">{dict.detection.section_title}</SectionTitle>
492
+ <Accordion multiple defaultValue={["hosting"]} className="w-full border-t border-zinc-200">
493
+ <AccordionItem value="hosting" className="border-zinc-200">
494
+ <AccordionTrigger className="py-3 text-left text-sm font-semibold text-zinc-900 hover:no-underline">
495
+ {dict.detection.hosting_title}
496
+ </AccordionTrigger>
497
+ <AccordionContent className="space-y-4 pb-4 text-zinc-600">
498
+ <MotionTagList tags={dict.detection.hosting_tags} />
499
+ <Separator className="bg-zinc-200" />
500
+ <div>
501
+ <h3 className="text-sm font-semibold text-zinc-900">
502
+ {dict.configuration.keys_title}
503
+ </h3>
504
+ <StaggerList className="mt-2 grid gap-1.5 sm:grid-cols-2">
505
+ {dict.configuration.keys.map((key) => (
506
+ <StaggerItem key={key} className="font-mono text-xs text-zinc-700">
507
+ {key}
508
+ </StaggerItem>
509
+ ))}
510
+ </StaggerList>
723
511
  </div>
724
- </div>
725
- </div>
726
- </div>
727
- <div className="flex flex-col gap-10 bg-zinc-50/20 p-8 lg:p-12">
728
- <div className="flex flex-col gap-6">
729
- <h4 className="text-[14px] font-bold uppercase tracking-wider text-zinc-900">
730
- {dict.docs.framework_coverage}
731
- </h4>
732
- <div className="flex flex-wrap gap-2">
733
- {frameworkCoverage.map((framework) => (
734
- <Badge
735
- key={framework}
736
- variant="outline"
737
- className="rounded-sm border-zinc-100 bg-white px-2 py-0.5 text-[12px] font-medium text-zinc-600"
738
- >
739
- {framework}
740
- </Badge>
741
- ))}
742
- </div>
743
- </div>
744
- <div className="flex flex-col gap-6">
745
- <h4 className="text-[14px] font-bold uppercase tracking-wider text-zinc-900">
746
- {dict.docs.testing_tools}
747
- </h4>
748
- <div className="flex flex-wrap gap-2">
749
- {testingTools.map((tool) => (
750
- <Badge
751
- key={tool}
752
- variant="secondary"
753
- className="rounded-sm border-transparent bg-zinc-100 px-2 py-0.5 text-[12px] font-bold text-zinc-600"
754
- >
755
- {tool}
756
- </Badge>
757
- ))}
512
+ <Separator className="bg-zinc-200" />
513
+ <div>
514
+ <h3 className="text-sm font-semibold text-zinc-900">
515
+ {dict.configuration.branches_title}
516
+ </h3>
517
+ <StaggerList className="mt-2 space-y-1.5 text-sm leading-snug text-zinc-600">
518
+ {dict.configuration.branches.map((line) => (
519
+ <StaggerItem key={line} className="flex gap-2">
520
+ <span className="text-zinc-400" aria-hidden>
521
+
522
+ </span>
523
+ {line}
524
+ </StaggerItem>
525
+ ))}
526
+ </StaggerList>
527
+ </div>
528
+ </AccordionContent>
529
+ </AccordionItem>
530
+ <AccordionItem value="frameworks" className="border-zinc-200">
531
+ <AccordionTrigger className="py-3 text-left text-sm font-semibold text-zinc-900 hover:no-underline">
532
+ {dict.detection.frameworks_title}
533
+ </AccordionTrigger>
534
+ <AccordionContent className="pb-4">
535
+ <MotionTagList tags={dict.detection.frameworks_tags} />
536
+ </AccordionContent>
537
+ </AccordionItem>
538
+ <AccordionItem value="testing" className="border-zinc-200">
539
+ <AccordionTrigger className="py-3 text-left text-sm font-semibold text-zinc-900 hover:no-underline">
540
+ {dict.detection.testing_title}
541
+ </AccordionTrigger>
542
+ <AccordionContent className="pb-4">
543
+ <MotionTagList tags={dict.detection.testing_tags} />
544
+ </AccordionContent>
545
+ </AccordionItem>
546
+ </Accordion>
547
+ </div>
548
+ <div className={`${colRight} lg:col-span-6`}>
549
+ <SectionTitle className="mb-3">{dict.configuration.section_title}</SectionTitle>
550
+ <p className="text-sm leading-relaxed text-zinc-600">{dict.configuration.intro}</p>
551
+ <Separator className="my-4 bg-zinc-200" />
552
+ <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-zinc-400">
553
+ {dict.configuration.example_caption}
554
+ </p>
555
+ <div className="mt-1.5">
556
+ <div className="flex min-h-12 items-stretch border border-zinc-200 bg-white">
557
+ {reduceMotion ? (
558
+ <pre className="min-w-0 flex-1 overflow-x-auto p-3 font-mono text-[11px] leading-snug text-zinc-900 whitespace-pre-wrap sm:text-xs">
559
+ <code>{dict.configuration.config_snippet}</code>
560
+ </pre>
561
+ ) : (
562
+ <m.pre
563
+ className="min-w-0 flex-1 overflow-x-auto p-3 font-mono text-[11px] leading-snug text-zinc-900 whitespace-pre-wrap sm:text-xs"
564
+ initial={{ opacity: 0, y: 14, scale: 0.99 }}
565
+ whileInView={{ opacity: 1, y: 0, scale: 1 }}
566
+ viewport={scrollViewport}
567
+ transition={{ duration: 0.55, ease: SITE_EASE }}
568
+ >
569
+ <code>{dict.configuration.config_snippet}</code>
570
+ </m.pre>
571
+ )}
572
+ <Separator orientation="vertical" className="h-auto bg-zinc-200" />
573
+ <div className="flex shrink-0 items-center px-1">
574
+ <CopyButton
575
+ text={dict.configuration.config_snippet}
576
+ idleLabel={dict.copy_button.idle}
577
+ successLabel={dict.copy_button.success}
578
+ />
758
579
  </div>
759
580
  </div>
760
581
  </div>
761
582
  </div>
762
- </div>
763
- </motion.div>
764
-
765
- {/* Workflow Outputs */}
766
- <motion.div
767
- initial={{ opacity: 0, y: 20 }}
768
- whileInView={{ opacity: 1, y: 0 }}
769
- viewport={{ once: true }}
770
- transition={{ duration: 0.8, delay: 0.1 }}
771
- className="grid grid-cols-1 border-b border-e border-s border-zinc-100 lg:grid-cols-12"
772
- >
773
- <div className="group/artifacts border-e border-zinc-100 p-8 transition-colors duration-500 hover:bg-zinc-50/20 lg:col-span-8 lg:p-12">
774
- <div className="flex flex-col gap-10">
775
- <div className="flex flex-col gap-2">
776
- <div className="flex items-center justify-between">
777
- <span className="text-[12px] font-black uppercase tracking-[0.18em] text-zinc-500">
778
- {dict.docs.section4_num}
779
- </span>
780
- <Terminal size={14} className="text-zinc-400 transition-colors group-hover/artifacts:text-zinc-500" />
781
- </div>
782
- <h3 className="text-[18px] font-bold text-zinc-900">{dict.docs.section3_title}</h3>
783
- </div>
784
- <div className="grid grid-cols-1 gap-x-16 gap-y-12 md:grid-cols-2">
785
- {[
786
- { f: "ci.yml", l: dict.docs.workflows.ci.label, d: dict.docs.workflows.ci.desc },
787
- { f: "deploy.yml", l: dict.docs.workflows.deploy.label, d: dict.docs.workflows.deploy.desc },
788
- { f: "docker.yml", l: dict.docs.workflows.docker.label, d: dict.docs.workflows.docker.desc },
789
- { f: "security.yml", l: dict.docs.workflows.security.label, d: dict.docs.workflows.security.desc },
790
- ].map((wf) => (
791
- <div key={wf.f} className="group/item flex flex-col gap-3">
792
- <div className="flex flex-col gap-1">
793
- <div className="flex items-center gap-3">
794
- <code className="rounded-sm border border-zinc-100 px-2.5 py-1 text-[14px] font-black leading-none text-zinc-800 transition-colors group-hover/item:border-zinc-200">
795
- {wf.f}
796
- </code>
797
- <div className="h-[1px] flex-1 bg-zinc-100 transition-colors group-hover/item:bg-zinc-200" />
583
+ </Reveal>
584
+
585
+ {/* Secrets + local checks (left) | Quality (right) */}
586
+ <Reveal className={`${bentoRow} border-b-0`} delay={0.12} y={16}>
587
+ <div className={`${colLeft} lg:col-span-6`}>
588
+ <SectionTitle className="mb-3">{dict.secrets.section_title}</SectionTitle>
589
+ <Separator className="mb-4 bg-zinc-200" />
590
+ <p className="text-sm leading-relaxed text-zinc-600">{dict.secrets.body}</p>
591
+ <Separator className="my-4 bg-zinc-200" />
592
+ <h3 className="text-sm font-semibold text-zinc-900">{dict.quality.commands_title}</h3>
593
+ <div className="mt-2 border border-zinc-200 bg-white">
594
+ {dict.quality.commands.map((cmd, i) => (
595
+ <div key={cmd}>
596
+ {i > 0 && <Separator className="bg-zinc-200" />}
597
+ <div className="flex min-h-11 items-stretch">
598
+ <pre className="min-w-0 flex-1 overflow-x-auto p-2.5 font-mono text-[11px] text-zinc-800 sm:text-xs">
599
+ <code>{cmd}</code>
600
+ </pre>
601
+ <Separator orientation="vertical" className="h-auto bg-zinc-200" />
602
+ <div className="flex shrink-0 items-center px-1">
603
+ <CopyButton
604
+ text={cmd}
605
+ idleLabel={dict.copy_button.idle}
606
+ successLabel={dict.copy_button.success}
607
+ />
798
608
  </div>
799
- <span className="text-[12px] font-bold uppercase tracking-tight text-zinc-500">{wf.l}</span>
800
609
  </div>
801
- <p className="text-[14px] text-zinc-500 transition-colors leading-relaxed group-hover/item:text-zinc-600">
802
- {wf.d}
803
- </p>
804
610
  </div>
805
611
  ))}
806
612
  </div>
613
+ <p className="mt-3 text-sm leading-relaxed text-zinc-500">{dict.quality.repo_note}</p>
807
614
  </div>
808
- </div>
809
-
810
- <div className="flex flex-col lg:col-span-4">
811
- <div className="group relative flex-1 overflow-hidden bg-zinc-950 p-8 text-white lg:p-12">
812
- <div className="absolute end-0 top-0 -me-32 -mt-32 h-64 w-64 rounded-full bg-emerald-500/10 blur-[100px]" />
813
- <div className="relative z-10 flex flex-col gap-8">
814
- <div className="flex flex-col gap-2">
815
- <span className="text-[12px] font-black uppercase tracking-[0.18em] text-zinc-500">
816
- {dict.docs.security_requirement}
817
- </span>
818
- <h4 className="text-[18px] font-bold">{dict.docs.encrypted_secrets}</h4>
819
- </div>
820
- <div className="flex flex-col gap-1">
821
- {["VERCEL_TOKEN", "AWS_ACCESS_KEY", "FIREBASE_TOKEN", "GHCR_TOKEN"].map((token) => (
822
- <div
823
- key={token}
824
- className="group/token flex items-center justify-between border-b border-white/5 py-3 transition-colors last:border-0"
825
- >
826
- <code className="text-[13px] font-mono text-zinc-400 transition-colors group-hover/token:text-white">
827
- {token}
828
- </code>
829
- <Shield size={12} className="text-zinc-700 transition-colors group-hover/token:text-emerald-500" />
830
- </div>
831
- ))}
832
- </div>
833
- <div className="flex flex-col gap-3">
834
- <p className="border-l-2 border-emerald-500/30 py-2 pl-4 text-[13px] italic leading-relaxed text-zinc-500">
835
- {dict.docs.add_secrets_at} <br />
836
- <span className="font-bold not-italic tracking-tight text-zinc-300">
837
- Settings → Secrets and variables → Actions
615
+ <div className={`${colRight} lg:col-span-6`}>
616
+ <SectionTitle className="mb-3">{dict.quality.section_title}</SectionTitle>
617
+ <p className="text-sm leading-snug text-zinc-600">{dict.quality.intro}</p>
618
+ <Separator className="my-4 bg-zinc-200" />
619
+ <StaggerList className="space-y-1.5 text-sm leading-snug text-zinc-700">
620
+ {dict.quality.items.map((item) => (
621
+ <StaggerItem key={item} className="flex gap-2">
622
+ <span className="text-zinc-400" aria-hidden>
623
+ ·
838
624
  </span>
839
- </p>
840
- </div>
841
- </div>
842
- </div>
843
- </div>
844
- </motion.div>
845
-
846
- <motion.div
847
- initial={{ opacity: 0, scale: 0.995 }}
848
- whileInView={{ opacity: 1, scale: 1 }}
849
- viewport={{ once: true }}
850
- transition={{ duration: 0.8, delay: 0.2 }}
851
- className="grid grid-cols-1 border-b border-l border-r border-zinc-100 bg-zinc-50/10 lg:grid-cols-12"
852
- >
853
- <div className="p-8 lg:col-span-12 lg:p-12">
854
- <div className="flex flex-col gap-8">
855
- <div className="flex flex-col gap-2">
856
- <span className="text-[12px] font-black uppercase tracking-[0.18em] text-zinc-500">
857
- {dict.docs.section4_num}
858
- </span>
859
- <h3 className="text-[18px] font-bold text-zinc-900">{dict.docs.section4_title}</h3>
860
- </div>
861
- <div className="grid grid-cols-1 gap-8 md:grid-cols-3">
862
- {exampleStacks.map((example) => (
863
- <div
864
- key={example.name}
865
- className="flex cursor-default flex-col gap-3 rounded-sm border border-zinc-200/60 bg-white p-6 transition-all hover:shadow-xl hover:shadow-zinc-200/40"
866
- >
867
- <span className="text-[14px] font-black text-zinc-900">{example.name}</span>
868
- <p className="text-[13px] font-medium leading-relaxed text-zinc-500">{example.description}</p>
869
- <div className="mt-2 flex items-center gap-1.5">
870
- <div className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
871
- <span className="font-mono text-[12px] font-bold uppercase tracking-widest text-zinc-500">
872
- {dict.docs.validated_output}
873
- </span>
874
- </div>
875
- </div>
625
+ {item}
626
+ </StaggerItem>
876
627
  ))}
877
- </div>
628
+ </StaggerList>
878
629
  </div>
879
- </div>
880
- </motion.div>
881
- </section>
882
-
883
- <motion.footer
884
- initial={{ opacity: 0 }}
885
- whileInView={{ opacity: 1 }}
886
- viewport={{ once: true }}
887
- transition={{ duration: 1.2 }}
888
- className="relative z-20 border-t border-zinc-100 bg-white"
889
- >
890
- <div className="mx-auto max-w-[1400px] border-l border-r border-zinc-100">
891
- <div className="grid grid-cols-1 md:grid-cols-12">
892
- <div className="flex flex-col gap-6 border-b border-zinc-100 p-8 md:col-span-4 md:border-b-0 md:border-r">
893
- <div className="flex flex-col gap-1">
894
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
895
- {dict.footer.project_origin}
896
- </span>
897
- <div className="flex items-center gap-2">
898
- <span className="text-[18px] font-bold tracking-tighter text-zinc-900">cistack</span>
899
- <span className="rounded-sm border border-zinc-100 bg-zinc-50 px-1.5 py-0.5 font-mono text-[12px] font-bold text-zinc-500">
900
- {`V_${version} // PRODUCTION`}
901
- </span>
902
- </div>
903
- </div>
904
- <div className="flex flex-col gap-1">
905
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
906
- {dict.footer.architected_by}
907
- </span>
908
- <a
909
- href="https://www.edwinvakayil.info/"
910
- target="_blank"
911
- rel="noopener noreferrer"
912
- className="group flex items-center gap-2 text-[14px] font-bold text-zinc-600 transition-colors hover:text-zinc-950"
913
- >
914
- Edwin Vakayil
915
- <ArrowUpRight size={12} className="text-zinc-400 transition-colors group-hover:text-zinc-950" />
916
- </a>
630
+ </Reveal>
631
+
632
+ {/* Footer */}
633
+ <Reveal className="border-t border-zinc-200" y={14} delay={0.02}>
634
+ <div className="grid gap-6 p-5 sm:p-6 lg:grid-cols-2 lg:gap-0 lg:px-8 lg:py-6">
635
+ <div className="lg:pe-8">
636
+ <p className="text-sm font-semibold text-zinc-900">{dict.footer.license}</p>
637
+ <p className="mt-1.5 max-w-md text-sm leading-relaxed text-zinc-500">{dict.footer.tagline}</p>
917
638
  </div>
918
- </div>
919
-
920
- <div className="flex flex-col justify-between gap-8 border-b border-zinc-100 p-8 md:col-span-5 md:border-b-0 md:border-r">
921
- <div className="flex flex-wrap gap-x-8 gap-y-4">
922
- <div className="flex flex-col gap-1">
923
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
924
- {dict.footer.status}
925
- </span>
926
- <div className="flex items-center gap-2">
927
- <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
928
- <span className="text-[12px] font-bold text-zinc-700">{dict.footer.operational}</span>
929
- </div>
930
- </div>
931
- <div className="flex flex-col gap-1">
932
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
933
- {dict.footer.license}
934
- </span>
935
- <span className="text-[12px] font-bold text-zinc-700 uppercase">{dict.footer.open_source}</span>
936
- </div>
639
+ <div className="lg:border-s lg:border-zinc-200 lg:ps-8">
640
+ <p className="text-sm text-zinc-600">
641
+ <span>{dict.footer.architect_credit} </span>
642
+ <a
643
+ href="https://www.edwinvakayil.info/"
644
+ target="_blank"
645
+ rel="noopener noreferrer"
646
+ className="font-medium text-zinc-950 underline-offset-4 hover:underline"
647
+ >
648
+ {dict.footer.architect_name}
649
+ </a>
650
+ </p>
937
651
  </div>
938
- <p className="text-[12px] font-medium leading-relaxed text-zinc-500">{dict.footer.footer_desc}</p>
939
652
  </div>
940
-
941
- <div className="flex flex-col items-start justify-center gap-3 bg-zinc-50/30 p-8 md:col-span-3 md:items-end">
942
- <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-500">
943
- {dict.footer.global_install}
944
- </span>
945
- <code className="cursor-default rounded-sm border border-zinc-100 bg-white px-3 py-1.5 font-mono text-[13px] font-bold text-zinc-800 transition-colors">
946
- npm install -g cistack
947
- </code>
948
- </div>
949
- </div>
950
-
951
- <div className="flex flex-col items-center justify-between gap-4 border-t border-zinc-100 p-4 px-8 sm:flex-row">
952
- <span className="font-mono text-[12px] font-bold uppercase tracking-widest text-zinc-500">
953
- {`© ${currentYear} ${dict.footer.copyright}`}
954
- </span>
955
- <div className="flex items-center gap-6">
956
- <a
957
- href="https://github.com/edwinvakayil/cistack"
958
- target="_blank"
959
- rel="noopener noreferrer"
960
- className="text-[12px] font-black uppercase tracking-widest text-zinc-500 transition-colors hover:text-zinc-950"
961
- >
962
- Github
963
- </a>
964
- <a
965
- href="https://www.npmjs.com/package/cistack"
966
- target="_blank"
967
- rel="noopener noreferrer"
968
- className="text-[12px] font-black uppercase tracking-widest text-zinc-500 transition-colors hover:text-zinc-950"
969
- >
970
- Npm
971
- </a>
972
- <a
973
- href="#docs"
974
- className="text-[12px] font-black uppercase tracking-widest text-zinc-500 transition-colors hover:text-zinc-950"
975
- >
976
- {dict.navigation.docs}
977
- </a>
978
- </div>
979
- </div>
653
+ <Separator className="bg-zinc-200" />
654
+ <p className="px-5 py-3 text-center text-xs text-zinc-400 sm:px-8">
655
+ © {currentYear} {dict.footer.copyright_suffix}
656
+ </p>
657
+ </Reveal>
980
658
  </div>
981
- </motion.footer>
659
+ </main>
660
+ </SiteMotionRoot>
982
661
  </div>
983
662
  </>
984
663
  );