cistack 5.5.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.github/dependabot.yml +42 -0
  2. package/.github/workflows/ci.yml +2 -1
  3. package/.github/workflows/pipeline.yml +250 -0
  4. package/README.md +4 -0
  5. package/package.json +7 -2
  6. package/product-site/.github/dependabot.yml +27 -0
  7. package/product-site/.github/workflows/pipeline.yml +202 -0
  8. package/product-site/.lighthouserc.json +22 -0
  9. package/product-site/README.md +1 -0
  10. package/product-site/app/[lang]/layout.tsx +95 -0
  11. package/product-site/app/[lang]/page.tsx +19 -0
  12. package/product-site/app/favicon.ico +0 -0
  13. package/product-site/app/globals.css +228 -0
  14. package/product-site/app/manifest.ts +20 -0
  15. package/product-site/app/robots.ts +12 -0
  16. package/product-site/app/sitemap.ts +12 -0
  17. package/product-site/components/CanvasText.tsx +219 -0
  18. package/product-site/components/CopyButton.tsx +85 -0
  19. package/product-site/components/HomeClient.tsx +985 -0
  20. package/product-site/components/InstallToggle.tsx +87 -0
  21. package/product-site/components/MotionRevealClient.tsx +53 -0
  22. package/product-site/components/TerminalCard.tsx +61 -0
  23. package/product-site/components/TerminalCardMotion.tsx +317 -0
  24. package/product-site/components/ui/accordion.tsx +74 -0
  25. package/product-site/components/ui/badge.tsx +52 -0
  26. package/product-site/components/ui/button.tsx +60 -0
  27. package/product-site/components/ui/card.tsx +103 -0
  28. package/product-site/components/ui/checkbox.tsx +29 -0
  29. package/product-site/components/ui/separator.tsx +25 -0
  30. package/product-site/components/ui/table.tsx +116 -0
  31. package/product-site/components/ui/tabs.tsx +82 -0
  32. package/product-site/components.json +25 -0
  33. package/product-site/dictionaries/br.json +137 -0
  34. package/product-site/dictionaries/cn.json +137 -0
  35. package/product-site/dictionaries/de.json +137 -0
  36. package/product-site/dictionaries/en.json +137 -0
  37. package/product-site/dictionaries/es.json +182 -0
  38. package/product-site/dictionaries/fr.json +182 -0
  39. package/product-site/dictionaries/pt.json +137 -0
  40. package/product-site/eslint.config.mjs +18 -0
  41. package/product-site/lib/dictionaries.ts +18 -0
  42. package/product-site/lib/dictionary-types.ts +3 -0
  43. package/product-site/lib/utils.ts +6 -0
  44. package/product-site/middleware.ts +39 -0
  45. package/product-site/next.config.mjs +14 -0
  46. package/product-site/package-lock.json +14201 -0
  47. package/product-site/package.json +41 -0
  48. package/product-site/postcss.config.mjs +7 -0
  49. package/product-site/public/file.svg +1 -0
  50. package/product-site/public/globe.svg +1 -0
  51. package/product-site/public/next.svg +1 -0
  52. package/product-site/public/og-image.png +0 -0
  53. package/product-site/public/vercel.svg +1 -0
  54. package/product-site/public/window.svg +1 -0
  55. package/product-site/scripts/sync-i18n.mjs +58 -0
  56. package/product-site/tsconfig.json +34 -0
  57. package/product-site/types/negotiator.d.ts +14 -0
  58. package/product-site/vercel.json +5 -0
  59. package/src/analyzers/codebase.js +53 -0
  60. package/src/detectors/framework.js +6 -6
  61. package/src/detectors/testing.js +37 -5
  62. package/src/generators/workflow.js +3 -63
  63. package/tests/run.js +38 -63
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import type { Dictionary } from "@/lib/dictionary-types";
6
+
7
+ type InstallMode = "npx" | "npm";
8
+
9
+ const installModes: Record<
10
+ InstallMode,
11
+ {
12
+ command: string;
13
+ badgeClassName: string;
14
+ badgeLabel: keyof Dictionary["install_toggle"];
15
+ description: keyof Dictionary["install_toggle"];
16
+ }
17
+ > = {
18
+ npx: {
19
+ command: "$ npx cistack",
20
+ badgeClassName: "border-emerald-500/20 bg-emerald-500/10 text-emerald-500",
21
+ badgeLabel: "recommended_badge",
22
+ description: "npx_desc",
23
+ },
24
+ npm: {
25
+ command: "$ npm install -g cistack",
26
+ badgeClassName: "border-zinc-700 bg-zinc-500/10 text-zinc-400",
27
+ badgeLabel: "global_badge",
28
+ description: "npm_desc",
29
+ },
30
+ };
31
+
32
+ export default function InstallToggle({ dict }: { dict: Dictionary }) {
33
+ const [mode, setMode] = useState<InstallMode>("npx");
34
+ const selectedMode = installModes[mode];
35
+
36
+ return (
37
+ <div className="flex flex-col gap-4">
38
+ <div className="flex items-center">
39
+ <button
40
+ type="button"
41
+ onClick={() => setMode("npx")}
42
+ className={`py-1 pr-4 text-[12px] font-semibold transition-colors ${
43
+ mode === "npx" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
44
+ }`}
45
+ >
46
+ npx
47
+ {mode === "npx" && (
48
+ <span className="ml-1.5 text-[11px] font-normal text-zinc-400">
49
+ {" "}
50
+ - {dict.install_toggle.recommended}
51
+ </span>
52
+ )}
53
+ </button>
54
+ <div className="h-3.5 w-px bg-zinc-200" />
55
+ <button
56
+ type="button"
57
+ onClick={() => setMode("npm")}
58
+ className={`py-1 pl-4 text-[12px] font-semibold transition-colors ${
59
+ mode === "npm" ? "text-zinc-900" : "text-zinc-400 hover:text-zinc-600"
60
+ }`}
61
+ >
62
+ {dict.install_toggle.npm_global}
63
+ </button>
64
+ </div>
65
+
66
+ <div className="relative overflow-hidden rounded-sm bg-zinc-950 px-6 pt-5 pb-6 font-mono text-sm text-zinc-300">
67
+ <div className="mb-5 flex items-center justify-between">
68
+ <span
69
+ className={`px-2 py-0.5 text-[12px] font-black uppercase tracking-widest ${selectedMode.badgeClassName}`}
70
+ >
71
+ {dict.install_toggle[selectedMode.badgeLabel]}
72
+ </span>
73
+ <span className="text-[12px] font-black uppercase tracking-[0.2em] text-zinc-700">
74
+ {mode}
75
+ </span>
76
+ </div>
77
+ <code className="text-[15px] tracking-tight text-emerald-400">
78
+ {selectedMode.command}
79
+ </code>
80
+ <div className="my-4 h-px w-full bg-zinc-800" />
81
+ <p className="font-sans text-[12px] leading-relaxed text-zinc-500">
82
+ {dict.install_toggle[selectedMode.description]}
83
+ </p>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { LazyMotion, domAnimation, m } from "framer-motion";
5
+
6
+ type MotionTag = "div" | "nav" | "footer";
7
+
8
+ interface MotionRevealClientProps {
9
+ as?: MotionTag;
10
+ children: ReactNode;
11
+ className?: string;
12
+ delay?: number;
13
+ duration?: number;
14
+ immediate?: boolean;
15
+ initialScale?: number;
16
+ initialY?: number;
17
+ }
18
+
19
+ const motionTags = {
20
+ div: m.div,
21
+ nav: m.nav,
22
+ footer: m.footer,
23
+ } as const;
24
+
25
+ export default function MotionRevealClient({
26
+ as = "div",
27
+ children,
28
+ className,
29
+ delay = 0,
30
+ duration = 0.8,
31
+ immediate = false,
32
+ initialScale = 1,
33
+ initialY = 20,
34
+ }: MotionRevealClientProps) {
35
+ const MotionTag = motionTags[as];
36
+ const transition = { duration, delay };
37
+ const target = { opacity: 1, y: 0, scale: 1 };
38
+
39
+ return (
40
+ <LazyMotion features={domAnimation}>
41
+ <MotionTag
42
+ className={className}
43
+ initial={{ opacity: 0, y: initialY, scale: initialScale }}
44
+ {...(immediate
45
+ ? { animate: target }
46
+ : { whileInView: target, viewport: { once: true, amount: 0.2 } })}
47
+ transition={transition}
48
+ >
49
+ {children}
50
+ </MotionTag>
51
+ </LazyMotion>
52
+ );
53
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { useMemo } from "react";
5
+ import type { Dictionary } from "@/lib/dictionary-types";
6
+
7
+ function TerminalCardFallback({ version, dict }: { version: string; dict: Dictionary["terminal"] }) {
8
+ return (
9
+ <div className="flex h-[300px] w-full flex-col rounded-sm border border-zinc-200 bg-white sm:h-[350px] lg:h-[380px]">
10
+ <div className="flex shrink-0 items-center justify-between border-b border-zinc-200 bg-white px-4 py-3">
11
+ <div className="flex items-center gap-4">
12
+ <div className="flex items-center gap-2">
13
+ <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-600" />
14
+ <span className="text-[12px] font-bold tracking-[0.14em] text-zinc-600 uppercase">
15
+ {dict?.processing || "Processing Output..."}
16
+ </span>
17
+ </div>
18
+ </div>
19
+ <div className="rounded-sm border border-zinc-200 bg-zinc-50 px-2.5 py-1 font-mono text-[13px] font-bold tracking-tight text-zinc-800">
20
+ npx cistack
21
+ </div>
22
+ </div>
23
+
24
+ <div
25
+ className="flex-1 bg-white p-6 pt-4 font-mono text-[12px] leading-relaxed tracking-tight text-zinc-700 sm:text-[13px]"
26
+ style={{ fontFamily: "var(--font-mono)" }}
27
+ >
28
+ <div className="mb-2 flex items-center gap-2">
29
+ <span className="font-bold text-zinc-600">$</span>
30
+ <span className="font-bold text-zinc-900">npx cistack</span>
31
+ <span className="inline-block h-4 w-1.5 animate-pulse bg-emerald-600" />
32
+ </div>
33
+ <div className="space-y-1">
34
+ <div className="font-bold text-zinc-900">cistack v{version}</div>
35
+ <div>Project scanned</div>
36
+ <div>Stack detected</div>
37
+ <div>Detected Stack: Next.js, React, TypeScript</div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export default function TerminalCard({
45
+ dict,
46
+ version = "3.0.0",
47
+ }: {
48
+ dict: Dictionary["terminal"];
49
+ version?: string;
50
+ }) {
51
+ const TerminalCardMotion = useMemo(
52
+ () =>
53
+ dynamic(() => import("@/components/TerminalCardMotion"), {
54
+ ssr: false,
55
+ loading: () => <TerminalCardFallback version={version} dict={dict} />,
56
+ }),
57
+ [version, dict]
58
+ );
59
+
60
+ return <TerminalCardMotion dict={dict} version={version} />;
61
+ }
@@ -0,0 +1,317 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { LazyMotion, domAnimation, m } from "framer-motion";
5
+ import { RefreshCcw } from "lucide-react";
6
+
7
+ import CopyButton from "@/components/CopyButton";
8
+ import type { Dictionary } from "@/lib/dictionary-types";
9
+
10
+ const COMMAND = "npx cistack";
11
+
12
+ interface OutputLine {
13
+ text: string;
14
+ type:
15
+ | "success"
16
+ | "info"
17
+ | "heading"
18
+ | "detail"
19
+ | "merged"
20
+ | "bullet"
21
+ | "written"
22
+ | "done"
23
+ | "path"
24
+ | "blank";
25
+ delay: number;
26
+ }
27
+
28
+ const lineColor: Record<OutputLine["type"], string> = {
29
+ success: "text-emerald-500",
30
+ info: "text-zinc-700",
31
+ heading: "font-bold text-zinc-900",
32
+ detail: "text-zinc-700",
33
+ merged: "text-amber-500",
34
+ bullet: "font-medium text-zinc-600",
35
+ written: "text-emerald-600",
36
+ done: "font-bold text-zinc-950",
37
+ path: "text-zinc-600",
38
+ blank: "",
39
+ };
40
+
41
+ export default function TerminalCardMotion({
42
+ dict,
43
+ version,
44
+ }: {
45
+ dict: Dictionary["terminal"];
46
+ version: string;
47
+ }) {
48
+ const [typedCommand, setTypedCommand] = useState("");
49
+ const [visibleLines, setVisibleLines] = useState(0);
50
+ const [phase, setPhase] = useState<"typing" | "output" | "done">("typing");
51
+ const [animationKey, setAnimationKey] = useState(0);
52
+
53
+ const outputLines = useMemo<OutputLine[]>(() => {
54
+ return [
55
+ { text: ` cistack v${version}`, type: "heading", delay: 100 },
56
+ { text: ` ${"─".repeat(24)}`, type: "detail", delay: 200 },
57
+ { text: "", type: "blank", delay: 250 },
58
+ { text: dict.project_scanned || "Project scanned", type: "success", delay: 500 },
59
+ { text: dict.stack_detected || "Stack detected", type: "success", delay: 800 },
60
+ { text: "", type: "blank", delay: 850 },
61
+ {
62
+ text: ` ${dict.detected_stack || "Detected Stack"}`,
63
+ type: "heading",
64
+ delay: 1000,
65
+ },
66
+ { text: ` ${"─".repeat(48)}`, type: "detail", delay: 1100 },
67
+ {
68
+ text: ` ${dict.languages || "Languages:"} TypeScript`,
69
+ type: "info",
70
+ delay: 1300,
71
+ },
72
+ {
73
+ text: ` ${dict.frameworks || "Frameworks:"} Next.js, React`,
74
+ type: "info",
75
+ delay: 1450,
76
+ },
77
+ { text: ` ${dict.hosting || "Hosting:"} Vercel`, type: "info", delay: 1600 },
78
+ { text: ` ${dict.testing || "Testing:"} none`, type: "info", delay: 1750 },
79
+ {
80
+ text: ` ${dict.release_tool || "Release tool:"} none`,
81
+ type: "info",
82
+ delay: 1900,
83
+ },
84
+ { text: "", type: "blank", delay: 1950 },
85
+ {
86
+ text:
87
+ dict.look_correct ||
88
+ "Does this look correct? Generate pipeline with these settings? Yes",
89
+ type: "detail",
90
+ delay: 2200,
91
+ },
92
+ {
93
+ text: dict.generated_workflows || "Generated 3 CI workflow(s)",
94
+ type: "success",
95
+ delay: 2600,
96
+ },
97
+ {
98
+ text: ` ${dict.smart_merged || "Smart-merged: ci.yml"}`,
99
+ type: "merged",
100
+ delay: 2800,
101
+ },
102
+ {
103
+ text: ` • ${dict.updated_on || 'updated top-level "on"'}`,
104
+ type: "bullet",
105
+ delay: 2900,
106
+ },
107
+ {
108
+ text: ` • ${dict.updated_concurrency || 'updated top-level "concurrency"'}`,
109
+ type: "bullet",
110
+ delay: 2950,
111
+ },
112
+ {
113
+ text: ` • ${dict.added_lint || 'added job "lint"'}`,
114
+ type: "bullet",
115
+ delay: 3000,
116
+ },
117
+ {
118
+ text: ` • ${dict.updated_build || 'job "build" → updated "name"'}`,
119
+ type: "bullet",
120
+ delay: 3050,
121
+ },
122
+ {
123
+ text: ` • ${dict.updated_needs || 'job "build" → updated "needs"'}`,
124
+ type: "bullet",
125
+ delay: 3100,
126
+ },
127
+ {
128
+ text: ` • ${
129
+ dict.added_checkout || 'job "build" → added step "Checkout code"'
130
+ }`,
131
+ type: "bullet",
132
+ delay: 3150,
133
+ },
134
+ {
135
+ text: ` • ${dict.added_node || 'job "build" → added step "Set up Node.js"'}`,
136
+ type: "bullet",
137
+ delay: 3200,
138
+ },
139
+ {
140
+ text: ` • ${dict.updated_step_build || 'job "build" → updated step "Build"'}`,
141
+ type: "bullet",
142
+ delay: 3250,
143
+ },
144
+ {
145
+ text: ` • ${
146
+ dict.added_upload || 'job "build" → added step "Upload build artifact"'
147
+ }`,
148
+ type: "bullet",
149
+ delay: 3300,
150
+ },
151
+ {
152
+ text: ` ✔ ${dict.written_deploy || "Written: deploy.yml"}`,
153
+ type: "written",
154
+ delay: 3500,
155
+ },
156
+ {
157
+ text: ` ✔ ${dict.written_security || "Written: security.yml"}`,
158
+ type: "written",
159
+ delay: 3650,
160
+ },
161
+ {
162
+ text: ` ✔ ${
163
+ dict.written_dependabot || "Written: .github/dependabot.yml"
164
+ }`,
165
+ type: "written",
166
+ delay: 3800,
167
+ },
168
+ { text: "", type: "blank", delay: 3850 },
169
+ {
170
+ text: ` ${dict.done_msg || "Done! Your GitHub Actions pipeline is ready."}`,
171
+ type: "done",
172
+ delay: 4100,
173
+ },
174
+ {
175
+ text: ` ${dict.workflows_path || "Workflows → cistack/.github/workflows"}`,
176
+ type: "path",
177
+ delay: 4300,
178
+ },
179
+ {
180
+ text: ` ${
181
+ dict.dependabot_path || "Dependabot → cistack/.github/dependabot.yml"
182
+ }`,
183
+ type: "path",
184
+ delay: 4450,
185
+ },
186
+ ];
187
+ }, [dict, version]);
188
+
189
+ useEffect(() => {
190
+ if (phase !== "typing") {
191
+ return;
192
+ }
193
+
194
+ if (typedCommand.length < COMMAND.length) {
195
+ const timeout = window.setTimeout(() => {
196
+ setTypedCommand(COMMAND.slice(0, typedCommand.length + 1));
197
+ }, 60 + Math.random() * 60);
198
+
199
+ return () => window.clearTimeout(timeout);
200
+ }
201
+
202
+ const timeout = window.setTimeout(() => setPhase("output"), 400);
203
+ return () => window.clearTimeout(timeout);
204
+ }, [typedCommand, phase, animationKey]);
205
+
206
+ useEffect(() => {
207
+ if (phase !== "output") {
208
+ return;
209
+ }
210
+
211
+ if (visibleLines >= outputLines.length) {
212
+ const timeout = window.setTimeout(() => setPhase("done"), 0);
213
+ return () => window.clearTimeout(timeout);
214
+ }
215
+
216
+ const currentDelay =
217
+ outputLines[visibleLines].delay -
218
+ (visibleLines > 0 ? outputLines[visibleLines - 1].delay : 0);
219
+ const timeout = window.setTimeout(() => {
220
+ setVisibleLines((current) => current + 1);
221
+ }, Math.max(currentDelay, 30));
222
+
223
+ return () => window.clearTimeout(timeout);
224
+ }, [visibleLines, phase, outputLines]);
225
+
226
+ const handleReplay = () => {
227
+ setTypedCommand("");
228
+ setVisibleLines(0);
229
+ setPhase("typing");
230
+ setAnimationKey((current) => current + 1);
231
+ };
232
+
233
+ return (
234
+ <LazyMotion features={domAnimation}>
235
+ <div
236
+ key={animationKey}
237
+ className="flex h-[300px] w-full flex-col rounded-sm border border-zinc-100 bg-white sm:h-[350px] lg:h-[380px]"
238
+ >
239
+ <div className="flex shrink-0 items-center justify-between border-b border-zinc-100 bg-white px-4 py-3">
240
+ <div className="flex items-center gap-4">
241
+ <div className="flex items-center gap-2">
242
+ <div className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500" />
243
+ <span className="font-mono text-[12px] font-black tracking-[0.18em] text-zinc-600 uppercase">
244
+ TERMINAL
245
+ </span>
246
+ </div>
247
+ </div>
248
+
249
+ <div className="flex items-center gap-2">
250
+ <div className="flex items-center gap-3 rounded-sm border border-zinc-200 bg-zinc-50 px-2.5 py-1">
251
+ <span className="font-mono text-[13px] font-bold tracking-tight text-zinc-800">
252
+ {COMMAND}
253
+ </span>
254
+ <div className="h-3 w-px bg-zinc-300" />
255
+ <CopyButton text={COMMAND} variant="terminal" />
256
+ </div>
257
+ <m.button
258
+ type="button"
259
+ whileTap={{ scale: 0.9 }}
260
+ onClick={handleReplay}
261
+ className="rounded-sm border border-transparent p-1.5 text-zinc-500 transition-colors hover:border-zinc-200 hover:text-zinc-900"
262
+ aria-label="Replay terminal animation"
263
+ >
264
+ <RefreshCcw size={14} />
265
+ </m.button>
266
+ </div>
267
+ </div>
268
+
269
+ <div
270
+ className="custom-scrollbar flex-1 overflow-y-auto bg-white p-6 pt-4 font-mono text-[12px] leading-relaxed tracking-tight selection:bg-zinc-900 selection:text-white sm:text-[13px]"
271
+ style={{ fontFamily: "var(--font-mono)" }}
272
+ >
273
+ <div className="flex flex-col gap-1.5">
274
+ <div className="mb-2 flex items-center gap-2">
275
+ <span className="font-bold text-zinc-600">$</span>
276
+ <span className="font-bold text-zinc-900">{typedCommand}</span>
277
+ {phase === "typing" && (
278
+ <m.span
279
+ animate={{ opacity: [1, 0] }}
280
+ transition={{ duration: 0.8, repeat: Number.POSITIVE_INFINITY }}
281
+ className="inline-block h-4 w-1.5 bg-emerald-500"
282
+ />
283
+ )}
284
+ </div>
285
+
286
+ {phase !== "typing" && (
287
+ <div className="space-y-0.5">
288
+ {outputLines.slice(0, visibleLines).map((line, index) => (
289
+ <m.div
290
+ key={`${animationKey}-${index}`}
291
+ initial={{ opacity: 0, x: -4 }}
292
+ animate={{ opacity: 1, x: 0 }}
293
+ transition={{ duration: 0.2 }}
294
+ className={`whitespace-pre-wrap break-words ${lineColor[line.type]}`}
295
+ style={{
296
+ minHeight: line.type === "blank" ? "0.75rem" : undefined,
297
+ }}
298
+ >
299
+ {line.text}
300
+ </m.div>
301
+ ))}
302
+ {phase === "output" && (
303
+ <div className="mt-2 flex items-center gap-2">
304
+ <div className="h-3 w-1 animate-pulse bg-zinc-300" />
305
+ <span className="text-[12px] font-bold tracking-[0.14em] text-zinc-600 uppercase">
306
+ {dict.processing || "Processing Output..."}
307
+ </span>
308
+ </div>
309
+ )}
310
+ </div>
311
+ )}
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </LazyMotion>
316
+ );
317
+ }
@@ -0,0 +1,74 @@
1
+ "use client"
2
+
3
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
7
+
8
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
9
+ return (
10
+ <AccordionPrimitive.Root
11
+ data-slot="accordion"
12
+ className={cn("flex w-full flex-col", className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("not-last:border-b", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: AccordionPrimitive.Trigger.Props) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
45
+ <ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
46
+ </AccordionPrimitive.Trigger>
47
+ </AccordionPrimitive.Header>
48
+ )
49
+ }
50
+
51
+ function AccordionContent({
52
+ className,
53
+ children,
54
+ ...props
55
+ }: AccordionPrimitive.Panel.Props) {
56
+ return (
57
+ <AccordionPrimitive.Panel
58
+ data-slot="accordion-content"
59
+ className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
60
+ {...props}
61
+ >
62
+ <div
63
+ className={cn(
64
+ "h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
65
+ className
66
+ )}
67
+ >
68
+ {children}
69
+ </div>
70
+ </AccordionPrimitive.Panel>
71
+ )
72
+ }
73
+
74
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,52 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props"
2
+ import { useRender } from "@base-ui/react/use-render"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ render,
34
+ ...props
35
+ }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
+ return useRender({
37
+ defaultTagName: "span",
38
+ props: mergeProps<"span">(
39
+ {
40
+ className: cn(badgeVariants({ variant }), className),
41
+ },
42
+ props
43
+ ),
44
+ render,
45
+ state: {
46
+ slot: "badge",
47
+ variant,
48
+ },
49
+ })
50
+ }
51
+
52
+ export { Badge, badgeVariants }