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,664 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useReducedMotion } from "framer-motion";
5
+ import { Globe, Package, Terminal } from "lucide-react";
6
+ import { useEffect, useState } from "react";
7
+
8
+ import CopyButton from "@/components/CopyButton";
9
+ import InstallToggle from "@/components/InstallToggle";
10
+ import TerminalCard from "@/components/TerminalCard";
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";
31
+ import type { Dictionary } from "@/lib/dictionary-types";
32
+
33
+ interface GithubIconProps {
34
+ size?: number;
35
+ className?: string;
36
+ }
37
+
38
+ interface RegistryPackageResponse {
39
+ version?: string;
40
+ }
41
+
42
+ interface DownloadStatsResponse {
43
+ downloads?: number;
44
+ }
45
+
46
+ const localeOptions = [
47
+ { code: "en", label: "English" },
48
+ { code: "fr", label: "Français" },
49
+ { code: "es", label: "Español" },
50
+ { code: "pt", label: "Português" },
51
+ { code: "br", label: "BR (Brasil)" },
52
+ { code: "de", label: "Deutsch" },
53
+ { code: "cn", label: "简体中文" },
54
+ ] as const;
55
+
56
+ const GithubIcon = ({ size = 24, className = "" }: GithubIconProps) => (
57
+ <svg
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ width={size}
60
+ height={size}
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ strokeWidth="2"
65
+ strokeLinecap="round"
66
+ strokeLinejoin="round"
67
+ className={className}
68
+ >
69
+ <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.02c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A4.8 4.8 0 0 0 8 18v4" />
70
+ </svg>
71
+ );
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
+
181
+ export default function HomeClient({
182
+ dict,
183
+ lang,
184
+ }: {
185
+ dict: Dictionary;
186
+ lang: string;
187
+ }) {
188
+ const [version, setVersion] = useState("3.0.0");
189
+ const [downloads, setDownloads] = useState("2.4k");
190
+ const reduceMotion = useReducedMotion();
191
+
192
+ useEffect(() => {
193
+ let cancelled = false;
194
+
195
+ const loadStats = async () => {
196
+ try {
197
+ const [registryRes, downloadsRes] = await Promise.all([
198
+ fetch("https://registry.npmjs.org/cistack/latest"),
199
+ fetch("https://api.npmjs.org/downloads/point/last-week/cistack"),
200
+ ]);
201
+
202
+ if (registryRes.ok) {
203
+ const data = (await registryRes.json()) as RegistryPackageResponse;
204
+ if (!cancelled && data.version) setVersion(data.version);
205
+ }
206
+
207
+ if (downloadsRes.ok) {
208
+ const data = (await downloadsRes.json()) as DownloadStatsResponse;
209
+ if (!cancelled && typeof data.downloads === "number") {
210
+ const count = data.downloads;
211
+ setDownloads(
212
+ count >= 1000 ? `${(count / 1000).toFixed(1)}k` : count.toLocaleString()
213
+ );
214
+ }
215
+ }
216
+ } catch (e) {
217
+ console.error("Stats fetch error", e);
218
+ }
219
+ };
220
+
221
+ void loadStats();
222
+ return () => {
223
+ cancelled = true;
224
+ };
225
+ }, []);
226
+
227
+ const currentYear = new Date().getFullYear();
228
+
229
+ return (
230
+ <>
231
+ <script
232
+ type="application/ld+json"
233
+ dangerouslySetInnerHTML={{
234
+ __html: JSON.stringify({
235
+ "@context": "https://schema.org",
236
+ "@type": "SoftwareApplication",
237
+ name: "cistack",
238
+ operatingSystem: "Any",
239
+ applicationCategory: "DeveloperApplication",
240
+ softwareVersion: version,
241
+ offers: {
242
+ "@type": "Offer",
243
+ price: "0",
244
+ priceCurrency: "USD",
245
+ availability: "https://schema.org/InStock",
246
+ },
247
+ description: `${dict.hero.tagline} ${dict.hero.intro}`,
248
+ creator: {
249
+ "@type": "Person",
250
+ name: "Edwin Vakayil",
251
+ url: "https://www.edwinvakayil.info/",
252
+ },
253
+ featureList: dict.why.items,
254
+ keywords:
255
+ "github actions, automation, ci/cd, devops, workflow generator, docker, vercel, aws, firebase",
256
+ }),
257
+ }}
258
+ />
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}
276
+ </span>
277
+ </div>
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">
301
+ <Link
302
+ href="/en"
303
+ className={`border px-2 py-1 text-xs font-semibold uppercase tracking-wide transition-colors ${
304
+ lang === "en"
305
+ ? "border-zinc-900 bg-zinc-900 text-white"
306
+ : "border-transparent text-zinc-500 hover:text-zinc-900"
307
+ }`}
308
+ >
309
+ EN
310
+ </Link>
311
+ {lang !== "en" && (
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"
315
+ >
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 />
322
+ Lang
323
+ </summary>
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">
325
+ {localeOptions.map((locale) => (
326
+ <Link
327
+ key={locale.code}
328
+ href={`/${locale.code}`}
329
+ className="px-3 py-2 text-xs font-medium text-zinc-700 hover:bg-zinc-50"
330
+ >
331
+ {locale.label}
332
+ </Link>
333
+ ))}
334
+ </div>
335
+ </details>
336
+ </div>
337
+ </nav>
338
+ </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>
362
+ </div>
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
+ />
378
+ </div>
379
+ </div>
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
+ />
400
+ </div>
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>
412
+ </div>
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
+ />
423
+ </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>
439
+ ))}
440
+ </StaggerList>
441
+ </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>
459
+ ))}
460
+ <SnippetStack snippets={item.snippets} copyLabels={dict.copy_button} />
461
+ </AccordionContent>
462
+ </AccordionItem>
463
+ ))}
464
+ </Accordion>
465
+ </div>
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>
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>
511
+ </div>
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
+ />
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </div>
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
+ />
608
+ </div>
609
+ </div>
610
+ </div>
611
+ ))}
612
+ </div>
613
+ <p className="mt-3 text-sm leading-relaxed text-zinc-500">{dict.quality.repo_note}</p>
614
+ </div>
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
+ ·
624
+ </span>
625
+ {item}
626
+ </StaggerItem>
627
+ ))}
628
+ </StaggerList>
629
+ </div>
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>
638
+ </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>
651
+ </div>
652
+ </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>
658
+ </div>
659
+ </main>
660
+ </SiteMotionRoot>
661
+ </div>
662
+ </>
663
+ );
664
+ }