create-spine 1.0.2 → 1.0.4

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 (42) hide show
  1. package/final-test/AGENTS.md +5 -0
  2. package/final-test/CLAUDE.md +1 -0
  3. package/final-test/README.md +37 -0
  4. package/final-test/app/Refund/page.tsx +340 -0
  5. package/final-test/app/api/auth/[...nextauth]/route.ts +6 -0
  6. package/final-test/app/api/generate/route.ts +57 -0
  7. package/final-test/app/dashboard/page.tsx +713 -0
  8. package/final-test/app/faq/page.tsx +343 -0
  9. package/final-test/app/globals.css +130 -0
  10. package/final-test/app/layout.tsx +33 -0
  11. package/final-test/app/page.tsx +650 -0
  12. package/final-test/app/pricing/page.tsx +23 -0
  13. package/final-test/app/privacy/page.tsx +328 -0
  14. package/final-test/app/terms/page.tsx +12 -0
  15. package/final-test/components/ui/badge.tsx +52 -0
  16. package/final-test/components/ui/button.tsx +58 -0
  17. package/final-test/components/ui/card.tsx +103 -0
  18. package/final-test/components/ui/separator.tsx +25 -0
  19. package/final-test/components/ui/textarea.tsx +18 -0
  20. package/final-test/components/ui/ui/footer/page.tsx +103 -0
  21. package/final-test/components/ui/ui/navbar/navbar.tsx +0 -0
  22. package/final-test/components.json +25 -0
  23. package/final-test/desktop.ini +6 -0
  24. package/final-test/eslint.config.mjs +18 -0
  25. package/final-test/lib/auth.ts +19 -0
  26. package/final-test/lib/prisma.ts +7 -0
  27. package/final-test/lib/utils.ts +6 -0
  28. package/final-test/next.config.ts +7 -0
  29. package/final-test/package-lock.json +9794 -0
  30. package/final-test/package.json +35 -0
  31. package/final-test/postcss.config.mjs +7 -0
  32. package/final-test/public/X.svg +1 -0
  33. package/final-test/public/github.svg +2 -0
  34. package/final-test/public/google.svg +2 -0
  35. package/final-test/src/components/ui/badge.tsx +58 -0
  36. package/final-test/src/components/ui/button.tsx +90 -0
  37. package/final-test/src/components/ui/card.tsx +103 -0
  38. package/final-test/src/components/ui/textarea.tsx +29 -0
  39. package/final-test/src/lib/utils.ts +6 -0
  40. package/final-test/tsconfig.json +34 -0
  41. package/index.js +3 -6
  42. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import type { Metadata } from "next"
2
+ import PricingClient from "./pricing-client"
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Pricing — Spine",
6
+ description:
7
+ "Free forever with 5 AI generations a day, or go Pro for ₹499/month with unlimited generations, custom presets, and team sharing.",
8
+ openGraph: {
9
+ title: "Pricing — Spine",
10
+ description: "Simple, transparent pricing. Free to try, no card required.",
11
+ type: "website",
12
+ url: "https://spine.io/pricing",
13
+ },
14
+ twitter: {
15
+ card: "summary_large_image",
16
+ title: "Pricing — Spine",
17
+ description: "Simple, transparent pricing. Free to try, no card required.",
18
+ },
19
+ }
20
+
21
+ export default function Page() {
22
+ return <PricingClient />
23
+ }
@@ -0,0 +1,328 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Spine — Privacy Policy
5
+ *
6
+ * IMPORTANT — READ BEFORE PUBLISHING:
7
+ * This is a structured starting template, not legal advice. It follows a
8
+ * standard SaaS privacy-policy outline and is grounded in what's publicly
9
+ * true about how Spine works (anonymous free tier, AI-generated scaffolds,
10
+ * optional accounts, Pro billing). Anything in [brackets] is a placeholder
11
+ * that needs a real, verified answer from your team — most importantly:
12
+ * - Section 3: confirmed from page.tsx that generation calls Google's Gemini
13
+ * API — still need YOUR Gemini plan/terms re: training use, confirmed
14
+ * - Section 5: remaining sub-processors (hosting, payments, analytics)
15
+ * - Section 6: real data-retention windows
16
+ * - Section 14: your registered legal entity name + address
17
+ * Have this reviewed by a lawyer before it goes live, especially if you
18
+ * have EU/UK or California users (GDPR / CCPA).
19
+ */
20
+
21
+ import { ChevronDown, Info } from "lucide-react"
22
+ import { Button } from "@/components/ui/button"
23
+
24
+ const GithubIcon = ({ className }) => (
25
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
26
+ <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
27
+ </svg>
28
+ )
29
+
30
+ /* ════════════════════════════════════════════════════════════════
31
+ CONTENT — edit freely, this is plain data
32
+ ════════════════════════════════════════════════════════════════ */
33
+ const LAST_UPDATED = "August 8, 2026"
34
+
35
+ const SECTIONS = [
36
+ {
37
+ id: "overview",
38
+ title: "1. Overview",
39
+ blocks: [
40
+ { type: "p", text: `This Privacy Policy explains how Spine collects, uses, and shares information when you use our website, our command-line tool, and related services (together, the Service). It covers information you give us and information we collect automatically — it does not cover the source code Spine generates for you once it's downloaded to your machine or pushed to your own repository, which is yours to do with as you like.` },
41
+ { type: "p", text: `By using the Service, you agree to the collection and use of information as described here. If you don't agree with this policy, please don't use the Service.` },
42
+ ],
43
+ },
44
+ {
45
+ id: "information-we-collect",
46
+ title: "2. Information We Collect",
47
+ blocks: [
48
+ { type: "h3", text: "Information you provide" },
49
+ {
50
+ type: "list",
51
+ items: [
52
+ `The prompts you write describing the backend you want generated.`,
53
+ `Account details, if you create one — email address and, optionally, a display name.`,
54
+ `Payment details, if you subscribe to Pro. These are handled directly by our payment processor [name your processor, e.g. Stripe]; we don't store full card numbers ourselves.`,
55
+ `Anything you send us directly, like support requests or feedback on GitHub.`,
56
+ ],
57
+ },
58
+ { type: "h3", text: "Information collected automatically" },
59
+ {
60
+ type: "list",
61
+ items: [
62
+ `Usage data — which stack you generated, how many generations you've run, timestamps.`,
63
+ `Standard log data — IP address, browser and device type, pages visited, referring URLs.`,
64
+ `Cookies and similar technologies (see Section 8).`,
65
+ ],
66
+ },
67
+ { type: "h3", text: "Information from third parties" },
68
+ { type: "p", text: `If you sign in using a third-party provider [e.g. GitHub OAuth, if applicable], we receive the basic profile information that provider shares with us, such as your name, email, and avatar.` },
69
+ ],
70
+ },
71
+ {
72
+ id: "prompts-and-ai",
73
+ title: "3. How Your Prompts and Generated Code Are Handled",
74
+ blocks: [
75
+ { type: "p", text: `When you generate a scaffold, your description is sent to Google's Gemini API, which we use to process your prompt and produce the files, schema, and routes you download. Free, anonymous generations are not linked to an account unless you're signed in.` },
76
+ { type: "p", text: `If you save a template while signed in, that prompt and its output are stored to your account so you can reuse it, and remain there until you delete it or close your account.` },
77
+ { type: "note", text: `Placeholder — confirm before publishing: your Gemini API tier and terms with Google determine whether prompts can be used to improve Google's models — this varies by plan and account configuration, so confirm your actual setup rather than assuming. Separately confirm whether Spine itself logs or reuses prompts/output beyond generating the response.` },
78
+ ],
79
+ },
80
+ {
81
+ id: "how-we-use-information",
82
+ title: "4. How We Use Information",
83
+ blocks: [
84
+ {
85
+ type: "list",
86
+ items: [
87
+ `To operate the Service — running generations, producing downloads, maintaining your account.`,
88
+ `To process Pro subscription payments and manage billing.`,
89
+ `To communicate with you — service notices, security alerts, and, only with your consent, product updates.`,
90
+ `To improve and secure the product — debugging, understanding usage patterns, and preventing abuse of the daily generation limit.`,
91
+ `To comply with legal obligations and enforce our terms.`,
92
+ ],
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ id: "how-we-share-information",
98
+ title: "5. How We Share Information",
99
+ blocks: [
100
+ { type: "p", text: `We don't sell your personal information. We share it only in these circumstances:` },
101
+ {
102
+ type: "list",
103
+ items: [
104
+ `With service providers who help us run Spine — including Google (Gemini API) to process generation prompts, and [list your other actual sub-processors: hosting/infrastructure, payment processor, email delivery, analytics].`,
105
+ `When required by law, or to respond to valid legal requests.`,
106
+ `To protect the rights, safety, or property of Spine, our users, or the public.`,
107
+ `In connection with a merger, acquisition, or sale of assets, with notice to you where required.`,
108
+ `With your consent, or at your direction.`,
109
+ ],
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ id: "data-retention",
115
+ title: "6. Data Retention",
116
+ blocks: [
117
+ { type: "p", text: `We keep account information for as long as your account is active, plus [XX days/months] afterward in case you want to reactivate it. Saved templates persist until you delete them or close your account. Anonymous usage and log data is retained for [XX days] for security and abuse-prevention purposes, then deleted or aggregated.` },
118
+ { type: "p", text: `You can ask us to delete your data earlier — see Your Rights and Choices below.` },
119
+ ],
120
+ },
121
+ {
122
+ id: "your-rights",
123
+ title: "7. Your Rights and Choices",
124
+ blocks: [
125
+ { type: "h3", text: "General" },
126
+ { type: "p", text: `You can access, correct, export, or delete your saved templates and account details at any time from your dashboard, or by contacting us directly.` },
127
+ { type: "h3", text: "If you're in the European Economic Area or UK" },
128
+ { type: "p", text: `Under GDPR, you have the right to access, rectify, erase, or restrict processing of your personal data; to receive a copy in a portable format; to object to certain processing; and to lodge a complaint with your local data protection authority.` },
129
+ { type: "h3", text: "If you're a California resident" },
130
+ { type: "p", text: `Under the CCPA/CPRA, you have the right to know what personal information we collect, request its deletion or correction, and opt out of the sale or sharing of personal information — which, as noted above, we don't do. Exercising these rights won't result in discriminatory treatment.` },
131
+ { type: "p", text: `To exercise any of these rights, contact us at privacy@spine.io.` },
132
+ ],
133
+ },
134
+ {
135
+ id: "cookies",
136
+ title: "8. Cookies and Similar Technologies",
137
+ blocks: [
138
+ { type: "p", text: `We use cookies and similar technologies to keep you signed in, remember your preferences, and understand how the Service is used [list actual tools, e.g. an analytics provider, if you use one]. Most browsers let you block or delete cookies through their settings; doing so may affect parts of the Service that require you to be signed in.` },
139
+ ],
140
+ },
141
+ {
142
+ id: "security",
143
+ title: "9. Data Security",
144
+ blocks: [
145
+ { type: "p", text: `We use reasonable technical and organizational measures — such as encryption in transit and access controls — to protect your information. No method of transmission or storage is 100% secure, so we can't guarantee absolute security.` },
146
+ ],
147
+ },
148
+ {
149
+ id: "international-transfers",
150
+ title: "10. International Data Transfers",
151
+ blocks: [
152
+ { type: "p", text: `Your information may be processed in countries other than the one you live in, including [list relevant countries/regions], which may have different data protection laws. Where required, we use appropriate safeguards, such as standard contractual clauses, for these transfers.` },
153
+ ],
154
+ },
155
+ {
156
+ id: "children",
157
+ title: "11. Children's Privacy",
158
+ blocks: [
159
+ { type: "p", text: `Spine isn't directed at children under 13 (or 16, where local law requires), and we don't knowingly collect personal information from them. If you believe a child has provided us with personal information, contact us and we'll delete it.` },
160
+ ],
161
+ },
162
+ {
163
+ id: "third-party-links",
164
+ title: "12. Third-Party Links",
165
+ blocks: [
166
+ { type: "p", text: `The Service may link to third-party sites, like GitHub or our documentation host. We aren't responsible for the privacy practices of sites we don't operate, and we encourage you to review their policies.` },
167
+ ],
168
+ },
169
+ {
170
+ id: "changes",
171
+ title: "13. Changes to This Policy",
172
+ blocks: [
173
+ { type: "p", text: `We may update this policy from time to time. If we make material changes, we'll update the date at the top of this page and, where appropriate, notify you directly [describe how, e.g. by email or an in-product notice].` },
174
+ ],
175
+ },
176
+ {
177
+ id: "contact",
178
+ title: "14. Contact Us",
179
+ blocks: [
180
+ { type: "p", text: `Questions about this policy or your data? Reach out at privacy@spine.io.` },
181
+ { type: "p", text: `[Company Legal Name], [Registered Address], [Jurisdiction / Governing Law]` },
182
+ ],
183
+ },
184
+ ]
185
+
186
+ /* ════════════════════════════════════════════════════════════════
187
+ SMALL PIECES
188
+ ════════════════════════════════════════════════════════════════ */
189
+ function TocLink({ href, children }) {
190
+ return (
191
+ <a
192
+ href={href}
193
+ className="block text-xs text-zinc-500 hover:text-emerald-400 transition-colors py-1.5 pl-3 border-l-2 border-transparent hover:border-emerald-800"
194
+ >
195
+ {children}
196
+ </a>
197
+ )
198
+ }
199
+
200
+ function Block({ block }) {
201
+ if (block.type === "p") {
202
+ return <p className="text-sm text-zinc-400 leading-relaxed mb-4">{block.text}</p>
203
+ }
204
+ if (block.type === "h3") {
205
+ return <h3 className="text-sm font-semibold text-zinc-200 mt-6 mb-2">{block.text}</h3>
206
+ }
207
+ if (block.type === "list") {
208
+ return (
209
+ <ul className="flex flex-col gap-2 mb-4">
210
+ {block.items.map((item, j) => (
211
+ <li key={j} className="text-sm text-zinc-400 leading-relaxed flex gap-2.5">
212
+ <span className="w-1 h-1 rounded-full bg-emerald-400 shrink-0 mt-1.5" />
213
+ <span>{item}</span>
214
+ </li>
215
+ ))}
216
+ </ul>
217
+ )
218
+ }
219
+ if (block.type === "note") {
220
+ return (
221
+ <div className="flex gap-3 bg-amber-950/30 border border-amber-900 rounded-lg px-4 py-3 mb-4">
222
+ <Info className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
223
+ <p className="text-xs text-amber-200/90 leading-relaxed">{block.text}</p>
224
+ </div>
225
+ )
226
+ }
227
+ return null
228
+ }
229
+
230
+ /* ════════════════════════════════════════════════════════════════
231
+ PAGE
232
+ ════════════════════════════════════════════════════════════════ */
233
+ export default function PrivacyPolicyPage() {
234
+ return (
235
+ <div className="bg-black min-h-screen text-zinc-50 font-sans antialiased">
236
+ {/* ── NAV ─────────────────────────────────────────────── */}
237
+ <nav className="sticky top-0 z-50 h-14 flex items-center justify-between px-5 md:px-12 bg-black/85 backdrop-blur-md border-b border-zinc-800">
238
+ <a href="/" className="flex items-center gap-2.5">
239
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
240
+ <line x1="12" y1="1" x2="12" y2="23" stroke="#34d399" strokeWidth="2" strokeLinecap="round" />
241
+ <line x1="12" y1="5" x2="18" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
242
+ <line x1="12" y1="5" x2="6" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
243
+ <line x1="12" y1="11" x2="18" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
244
+ <line x1="12" y1="11" x2="6" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
245
+ <line x1="12" y1="17" x2="18" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
246
+ <line x1="12" y1="17" x2="6" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
247
+ </svg>
248
+ <span className="font-bold text-base tracking-tight">Spine</span>
249
+ </a>
250
+ <div className="hidden md:flex items-center gap-7">
251
+ <a href="/#templates" className="text-sm text-zinc-500 hover:text-zinc-100 transition-colors">Templates</a>
252
+ <a href="/#pricing" className="text-sm text-zinc-500 hover:text-zinc-100 transition-colors">Pricing</a>
253
+ <a href="/faq" className="text-sm text-zinc-500 hover:text-zinc-100 transition-colors">FAQ</a>
254
+ </div>
255
+ <div className="flex items-center gap-2">
256
+ <a href="https://github.com" className="text-zinc-500 hover:text-zinc-200 transition-colors">
257
+ <GithubIcon className="w-4 h-4" />
258
+ </a>
259
+ <Button
260
+ onClick={() => { window.location.href = "/" }}
261
+ size="sm"
262
+ className="bg-emerald-500 hover:bg-emerald-400 text-black font-semibold text-xs h-8"
263
+ >
264
+ Get started
265
+ </Button>
266
+ </div>
267
+ </nav>
268
+
269
+ {/* ── HEADER ──────────────────────────────────────────── */}
270
+ <header className="max-w-3xl mx-auto px-5 md:px-12 pt-16 md:pt-20 pb-10 border-b border-zinc-800">
271
+ <p className="text-xs text-zinc-500 tracking-widest uppercase mb-4">Legal</p>
272
+ <h1 className="text-3xl md:text-5xl font-extrabold tracking-tighter mb-4">Privacy Policy</h1>
273
+ <p className="text-xs text-zinc-600 font-mono mb-5">Last updated {LAST_UPDATED}</p>
274
+ <p className="text-sm md:text-base text-zinc-500 leading-relaxed max-w-lg">
275
+ This page explains what information Spine collects, why, and what control you have over it. If anything here is unclear, Section 14 has how to reach us.
276
+ </p>
277
+ </header>
278
+
279
+ {/* ── BODY ────────────────────────────────────────────── */}
280
+ <div className="max-w-5xl mx-auto px-5 md:px-12 py-12 md:py-16">
281
+ <details className="group lg:hidden mb-10 border border-zinc-800 rounded-lg bg-zinc-900 overflow-hidden">
282
+ <summary className="list-none cursor-pointer px-4 py-3 flex items-center justify-between text-sm font-medium text-zinc-200 hover:bg-zinc-800/30 transition-colors">
283
+ Table of contents
284
+ <ChevronDown className="w-4 h-4 text-zinc-500 transition-transform duration-300 group-open:rotate-180" />
285
+ </summary>
286
+ <nav className="px-4 pb-4 pt-1 flex flex-col gap-0.5 border-t border-zinc-800">
287
+ {SECTIONS.map((s) => <TocLink key={s.id} href={`#${s.id}`}>{s.title}</TocLink>)}
288
+ </nav>
289
+ </details>
290
+
291
+ <div className="flex gap-12">
292
+ <aside className="hidden lg:block w-56 shrink-0">
293
+ <nav className="sticky top-20 flex flex-col gap-0.5 border-l border-zinc-800">
294
+ {SECTIONS.map((s) => <TocLink key={s.id} href={`#${s.id}`}>{s.title}</TocLink>)}
295
+ </nav>
296
+ </aside>
297
+
298
+ <div className="flex-1 min-w-0 max-w-2xl">
299
+ {SECTIONS.map((s) => (
300
+ <section key={s.id} id={s.id} className="mb-12 scroll-mt-20">
301
+ <h2 className="text-lg font-bold tracking-tight text-zinc-100 mb-4">{s.title}</h2>
302
+ {s.blocks.map((b, i) => <Block key={i} block={b} />)}
303
+ </section>
304
+ ))}
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ {/* ── FOOTER ──────────────────────────────────────────── */}
310
+ <footer className="border-t border-zinc-800">
311
+ <div className="max-w-6xl mx-auto px-5 md:px-12 py-6 flex flex-col md:flex-row items-start md:items-center justify-between gap-3">
312
+ <span className="text-xs text-zinc-600">Spine · spine.io © {new Date().getFullYear()}</span>
313
+ <div className="flex gap-6 flex-wrap">
314
+ {[
315
+ { label: "Templates", href: "/#templates" },
316
+ { label: "Pricing", href: "/#pricing" },
317
+ { label: "FAQ", href: "/faq" },
318
+ { label: "Privacy", href: "/privacy" },
319
+ { label: "GitHub", href: "https://github.com" },
320
+ ].map((l) => (
321
+ <a key={l.label} href={l.href} className="text-xs text-zinc-600 hover:text-zinc-400 transition-colors">{l.label}</a>
322
+ ))}
323
+ </div>
324
+ </div>
325
+ </footer>
326
+ </div>
327
+ )
328
+ }
@@ -0,0 +1,12 @@
1
+ import type { Metadata } from "next"
2
+ import TermsClient from "./terms-client"
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Terms and Conditions — Spine",
6
+ description: "Terms and conditions for using Spine's AI-powered scaffold generator.",
7
+ robots: { index: true, follow: true },
8
+ }
9
+
10
+ export default function Page() {
11
+ return <TermsClient />
12
+ }
@@ -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 }
@@ -0,0 +1,58 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
12
+ outline:
13
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
14
+ secondary:
15
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
16
+ ghost:
17
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
18
+ destructive:
19
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default:
24
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
25
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
27
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
28
+ icon: "size-8",
29
+ "icon-xs":
30
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
31
+ "icon-sm":
32
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
33
+ "icon-lg": "size-9",
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ variant: "default",
38
+ size: "default",
39
+ },
40
+ }
41
+ )
42
+
43
+ function Button({
44
+ className,
45
+ variant = "default",
46
+ size = "default",
47
+ ...props
48
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
49
+ return (
50
+ <ButtonPrimitive
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ export { Button, buttonVariants }
@@ -0,0 +1,103 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-(--card-spacing)", className)}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
103
+ }
@@ -0,0 +1,25 @@
1
+ "use client"
2
+
3
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Separator({
8
+ className,
9
+ orientation = "horizontal",
10
+ ...props
11
+ }: SeparatorPrimitive.Props) {
12
+ return (
13
+ <SeparatorPrimitive
14
+ data-slot="separator"
15
+ orientation={orientation}
16
+ className={cn(
17
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ export { Separator }
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }