diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/buzz-marquee.tsx | 21 | ||||
| -rw-r--r-- | components/cta-band.tsx | 34 | ||||
| -rw-r--r-- | components/dashboard-mock.tsx | 133 | ||||
| -rw-r--r-- | components/faq-list.tsx | 26 | ||||
| -rw-r--r-- | components/glow-orbs.tsx | 14 | ||||
| -rw-r--r-- | components/logo.tsx | 61 | ||||
| -rw-r--r-- | components/request-form.tsx | 145 | ||||
| -rw-r--r-- | components/section.tsx | 40 | ||||
| -rw-r--r-- | components/site-footer.tsx | 95 | ||||
| -rw-r--r-- | components/site-header.tsx | 130 | ||||
| -rw-r--r-- | components/solutions-tabs.tsx | 53 | ||||
| -rw-r--r-- | components/ui/accordion.tsx | 74 | ||||
| -rw-r--r-- | components/ui/badge.tsx | 52 | ||||
| -rw-r--r-- | components/ui/button.tsx | 59 | ||||
| -rw-r--r-- | components/ui/card.tsx | 103 | ||||
| -rw-r--r-- | components/ui/dialog.tsx | 160 | ||||
| -rw-r--r-- | components/ui/input.tsx | 20 | ||||
| -rw-r--r-- | components/ui/label.tsx | 20 | ||||
| -rw-r--r-- | components/ui/navigation-menu.tsx | 168 | ||||
| -rw-r--r-- | components/ui/separator.tsx | 25 | ||||
| -rw-r--r-- | components/ui/sheet.tsx | 138 | ||||
| -rw-r--r-- | components/ui/tabs.tsx | 82 | ||||
| -rw-r--r-- | components/ui/textarea.tsx | 18 |
23 files changed, 1671 insertions, 0 deletions
diff --git a/components/buzz-marquee.tsx b/components/buzz-marquee.tsx new file mode 100644 index 0000000..6d9b33c --- /dev/null +++ b/components/buzz-marquee.tsx @@ -0,0 +1,21 @@ +import { allTerms } from "@/lib/buzz"; + +export function BuzzMarquee() { + const loop = [...allTerms, ...allTerms]; + return ( + <div className="relative overflow-hidden border-y border-white/10 bg-violet-500/5 py-3"> + <div className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-background to-transparent" /> + <div className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-background to-transparent" /> + <div className="marquee-track flex w-max gap-8"> + {loop.map((term, i) => ( + <span + key={`${term}-${i}`} + className="font-mono text-[11px] uppercase tracking-[0.16em] whitespace-nowrap text-violet-200/70" + > + {term} + </span> + ))} + </div> + </div> + ); +} diff --git a/components/cta-band.tsx b/components/cta-band.tsx new file mode 100644 index 0000000..cf00d62 --- /dev/null +++ b/components/cta-band.tsx @@ -0,0 +1,34 @@ +import Link from "next/link"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export function CtaBand({ + title = "Request access to The Agentic Cloud.", + body = "Invitation-based. An outcomes partner will assess whether your operating thesis, your lake, and our attention-free runtime are prepared to compound.", +}: { + title?: string; + body?: string; +}) { + return ( + <section className="border-t border-white/10 px-5 py-20 sm:px-8 md:py-24"> + <div className="relative mx-auto flex max-w-6xl flex-col items-start justify-between gap-8 overflow-hidden rounded-3xl border border-white/10 bg-gradient-to-br from-violet-600/25 via-fuchsia-600/15 to-cyan-500/15 px-6 py-10 shadow-[0_0_80px_oklch(0.6_0.2_305/0.25)] sm:px-10 md:flex-row md:items-center"> + <div className="pointer-events-none absolute -right-10 -top-16 size-56 rounded-full bg-fuchsia-500/30 blur-3xl" /> + <div className="pointer-events-none absolute -bottom-16 -left-10 size-56 rounded-full bg-cyan-400/20 blur-3xl" /> + <div className="relative max-w-xl"> + <h2 className="font-heading text-3xl leading-tight text-foreground md:text-4xl"> + {title} + </h2> + <p className="mt-3 text-sm leading-relaxed text-muted-foreground md:text-base"> + {body} + </p> + </div> + <Link + href="/request-access" + className={cn(buttonVariants(), "relative h-11 shrink-0 px-5 text-sm")} + > + Request access + </Link> + </div> + </section> + ); +} diff --git a/components/dashboard-mock.tsx b/components/dashboard-mock.tsx new file mode 100644 index 0000000..e4407d4 --- /dev/null +++ b/components/dashboard-mock.tsx @@ -0,0 +1,133 @@ +export function DashboardMock() { + return ( + <div className="relative overflow-hidden rounded-3xl border border-white/10 bg-black/30 shadow-[0_0_80px_oklch(0.55_0.22_305/0.35)] backdrop-blur-xl"> + <div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-violet-500/15 via-transparent to-cyan-400/10" /> + <div className="relative flex items-center justify-between border-b border-white/10 px-4 py-3"> + <div className="flex items-center gap-2"> + <span className="size-2.5 rounded-full bg-fuchsia-400 shadow-[0_0_10px_#e879f9]" /> + <span className="size-2.5 rounded-full bg-white/20" /> + <span className="size-2.5 rounded-full bg-white/20" /> + </div> + <p className="font-mono text-[10px] uppercase tracking-[0.18em] text-violet-200/80"> + Agentic operating surface + </p> + <span className="hidden font-mono text-[10px] text-cyan-300 sm:inline"> + PPoE 94.2 + </span> + </div> + <div className="relative grid md:grid-cols-[188px_1fr_200px]"> + <aside className="hidden border-r border-white/10 p-4 md:block"> + {[ + "Agents", + "Data fabric", + "Mesh", + "Quantum", + "Zero trust", + "Bio-sync", + "Carbon", + ].map((item, i) => ( + <div + key={item} + className={`mb-1 rounded-lg px-2.5 py-1.5 text-xs ${ + i === 0 + ? "bg-gradient-to-r from-violet-500/30 to-fuchsia-500/20 text-violet-100" + : "text-violet-100/50" + }`} + > + {item} + </div> + ))} + </aside> + <div className="relative min-h-[280px] p-4 sm:min-h-[340px]"> + <svg viewBox="0 0 520 320" className="h-full w-full" aria-hidden> + <defs> + <linearGradient id="line" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stopColor="#c084fc" stopOpacity="0.2" /> + <stop offset="50%" stopColor="#e879f9" stopOpacity="0.7" /> + <stop offset="100%" stopColor="#67e8f9" stopOpacity="0.8" /> + </linearGradient> + <filter id="glow"> + <feGaussianBlur stdDeviation="3.5" result="coloredBlur" /> + <feMerge> + <feMergeNode in="coloredBlur" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> + </defs> + <line x1="80" y1="160" x2="250" y2="70" stroke="url(#line)" /> + <line x1="80" y1="160" x2="250" y2="250" stroke="url(#line)" /> + <line x1="250" y1="70" x2="430" y2="160" stroke="url(#line)" /> + <line x1="250" y1="250" x2="430" y2="160" stroke="url(#line)" /> + <line x1="250" y1="70" x2="250" y2="250" stroke="#a78bfa" strokeOpacity="0.25" /> + <line x1="80" y1="160" x2="430" y2="160" stroke="#67e8f9" strokeOpacity="0.15" /> + <circle cx="80" cy="160" r="9" className="pulse-node" fill="#c084fc" filter="url(#glow)" /> + <circle cx="250" cy="70" r="7" className="pulse-node-2" fill="#e879f9" filter="url(#glow)" /> + <circle cx="250" cy="250" r="7" className="pulse-node-3" fill="#818cf8" filter="url(#glow)" /> + <circle cx="430" cy="160" r="11" fill="#67e8f9" filter="url(#glow)" /> + <circle cx="170" cy="110" r="4" fill="#c084fc" fillOpacity="0.6" /> + <circle cx="340" cy="110" r="4" fill="#67e8f9" fillOpacity="0.6" /> + <circle cx="170" cy="210" r="4" fill="#e879f9" fillOpacity="0.5" /> + <circle cx="340" cy="210" r="4" fill="#a78bfa" fillOpacity="0.5" /> + <text + x="80" + y="188" + textAnchor="middle" + fill="#ddd6fe" + fontSize="9" + fontFamily="ui-monospace, monospace" + > + AGENT + </text> + <text + x="250" + y="58" + textAnchor="middle" + fill="#f5d0fe" + fontSize="9" + fontFamily="ui-monospace, monospace" + > + LAKE + </text> + <text + x="250" + y="278" + textAnchor="middle" + fill="#c4b5fd" + fontSize="9" + fontFamily="ui-monospace, monospace" + > + MESH + </text> + <text + x="430" + y="188" + textAnchor="middle" + fill="#a5f3fc" + fontSize="9" + fontFamily="ui-monospace, monospace" + > + SYNERGY + </text> + </svg> + </div> + <aside className="hidden space-y-3 border-l border-white/10 p-4 md:block"> + <Metric label="Agent velocity" value="+3.2×" /> + <Metric label="Semantic coverage" value="87%" /> + <Metric label="eBPF map health" value="99.4" /> + <Metric label="ESG score" value="A−" /> + </aside> + </div> + </div> + ); +} + +function Metric({ label, value }: { label: string; value: string }) { + return ( + <div className="rounded-xl border border-white/10 bg-white/5 px-3 py-2.5"> + <p className="font-mono text-[10px] uppercase tracking-wider text-violet-200/70"> + {label} + </p> + <p className="mt-1 font-heading text-xl text-white">{value}</p> + </div> + ); +} diff --git a/components/faq-list.tsx b/components/faq-list.tsx new file mode 100644 index 0000000..564df71 --- /dev/null +++ b/components/faq-list.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { faqs } from "@/lib/content"; + +export function FaqList() { + return ( + <Accordion className="w-full" defaultValue={["what"]}> + {faqs.map((item) => ( + <AccordionItem key={item.id} value={item.id}> + <AccordionTrigger className="py-4 text-base hover:no-underline"> + {item.q} + </AccordionTrigger> + <AccordionContent className="text-muted-foreground leading-relaxed"> + {item.a} + </AccordionContent> + </AccordionItem> + ))} + </Accordion> + ); +} diff --git a/components/glow-orbs.tsx b/components/glow-orbs.tsx new file mode 100644 index 0000000..f9a7072 --- /dev/null +++ b/components/glow-orbs.tsx @@ -0,0 +1,14 @@ +export function GlowOrbs() { + return ( + <div + className="pointer-events-none fixed inset-0 -z-10 overflow-hidden" + aria-hidden + > + <div className="orb orb-a" /> + <div className="orb orb-b" /> + <div className="orb orb-c" /> + <div className="orb orb-d" /> + <div className="noise" /> + </div> + ); +} diff --git a/components/logo.tsx b/components/logo.tsx new file mode 100644 index 0000000..a512203 --- /dev/null +++ b/components/logo.tsx @@ -0,0 +1,61 @@ +import { cn } from "@/lib/utils"; + +export function Logo({ + className, + markClassName, + wordmark = true, +}: { + className?: string; + markClassName?: string; + wordmark?: boolean; +}) { + return ( + <span className={cn("inline-flex items-center gap-2.5", className)}> + <svg + viewBox="0 0 32 32" + fill="none" + className={cn("size-7 shrink-0", markClassName)} + aria-hidden + > + <rect + x="1" + y="1" + width="30" + height="30" + rx="9" + className="fill-primary/10 stroke-primary/45" + strokeWidth="1" + /> + <ellipse + cx="16" + cy="18.5" + rx="9.5" + ry="5.2" + className="stroke-primary/80" + strokeWidth="1.4" + /> + <ellipse + cx="16" + cy="16" + rx="7" + ry="3.6" + className="stroke-primary" + strokeWidth="1.3" + /> + <circle cx="10.5" cy="16.2" r="1.35" className="fill-primary" /> + <circle cx="16" cy="14.4" r="1.5" className="fill-primary" /> + <circle cx="21.5" cy="16.2" r="1.35" className="fill-primary" /> + <path + d="M10.5 16.2L16 14.4L21.5 16.2" + className="stroke-primary/70" + strokeWidth="1" + /> + </svg> + {wordmark ? ( + <span className="text-[15px] font-medium tracking-tight text-foreground"> + The Agentic Cloud + </span> + ) : null} + </span> + ); +} diff --git a/components/request-form.tsx b/components/request-form.tsx new file mode 100644 index 0000000..8f61337 --- /dev/null +++ b/components/request-form.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +const consumerDomains = [ + "gmail.com", + "yahoo.com", + "hotmail.com", + "outlook.com", + "icloud.com", + "aol.com", + "proton.me", + "protonmail.com", +]; + +type Status = "idle" | "loading" | "success" | "error"; + +export function RequestForm() { + const [status, setStatus] = useState<Status>("idle"); + const [error, setError] = useState(""); + + async function onSubmit(e: React.FormEvent<HTMLFormElement>) { + e.preventDefault(); + setError(""); + const form = e.currentTarget; + const data = new FormData(form); + const name = String(data.get("name") || "").trim(); + const email = String(data.get("email") || "").trim(); + const company = String(data.get("company") || "").trim(); + const role = String(data.get("role") || "").trim(); + const outcomes = String(data.get("outcomes") || "").trim(); + + if (!name || !email || !company || !role || !outcomes) { + setStatus("error"); + setError("Every field is required. Alignment begins with completeness."); + return; + } + + const domain = email.split("@")[1]?.toLowerCase(); + if (!domain || !email.includes("@")) { + setStatus("error"); + setError("Please provide an institutional email."); + return; + } + if (consumerDomains.includes(domain)) { + setStatus("error"); + setError( + "Consumer inboxes cannot attest PPoE. Please use your institutional email." + ); + return; + } + + setStatus("loading"); + await new Promise((r) => setTimeout(r, 900)); + setStatus("success"); + } + + if (status === "success") { + return ( + <div className="rounded-3xl border border-fuchsia-400/30 bg-gradient-to-br from-violet-500/20 to-cyan-500/10 p-8"> + <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-primary"> + Request received + </p> + <h2 className="mt-3 font-heading text-3xl text-foreground"> + An outcomes partner will be in touch. + </h2> + <p className="mt-3 text-sm leading-relaxed text-muted-foreground"> + Fit is a mutual decision. If your operating thesis, your lake, and + our fabric are prepared to compound, we will schedule a working + session — not a demo. + </p> + </div> + ); + } + + return ( + <form onSubmit={onSubmit} className="space-y-5" noValidate> + <Field id="name" label="Full name" autoComplete="name" /> + <Field + id="email" + label="Institutional email" + type="email" + autoComplete="email" + /> + <Field id="company" label="Institution" autoComplete="organization" /> + <Field id="role" label="Role" placeholder="Chief Transformation Officer" /> + <div className="space-y-2"> + <Label htmlFor="outcomes"> + What outcomes are you looking to operationalize? + </Label> + <Textarea + id="outcomes" + name="outcomes" + rows={5} + className="min-h-28 text-sm" + placeholder="Be specific about the thesis, not the tools. The fabric will meet you there." + /> + </div> + {status === "error" && error ? ( + <p className="text-sm text-destructive" role="alert"> + {error} + </p> + ) : null} + <Button + type="submit" + disabled={status === "loading"} + className="h-11 w-full px-5 sm:w-auto" + > + {status === "loading" ? "Attesting request…" : "Request access"} + </Button> + </form> + ); +} + +function Field({ + id, + label, + type = "text", + autoComplete, + placeholder, +}: { + id: string; + label: string; + type?: string; + autoComplete?: string; + placeholder?: string; +}) { + return ( + <div className="space-y-2"> + <Label htmlFor={id}>{label}</Label> + <Input + id={id} + name={id} + type={type} + autoComplete={autoComplete} + placeholder={placeholder} + className="h-10" + /> + </div> + ); +} diff --git a/components/section.tsx b/components/section.tsx new file mode 100644 index 0000000..59120bf --- /dev/null +++ b/components/section.tsx @@ -0,0 +1,40 @@ +import { cn } from "@/lib/utils"; + +export function Section({ + children, + className, + innerClassName, + id, +}: { + children: React.ReactNode; + className?: string; + innerClassName?: string; + id?: string; +}) { + return ( + <section id={id} className={cn("px-5 py-20 sm:px-8 md:py-28", className)}> + <div className={cn("mx-auto w-full max-w-6xl", innerClassName)}> + {children} + </div> + </section> + ); +} + +export function Eyebrow({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( + <p + className={cn( + "inline-flex items-center rounded-full border border-violet-400/25 bg-violet-500/10 px-3 py-1 font-mono text-[11px] font-medium uppercase tracking-[0.22em] text-violet-200", + className + )} + > + {children} + </p> + ); +} diff --git a/components/site-footer.tsx b/components/site-footer.tsx new file mode 100644 index 0000000..983de05 --- /dev/null +++ b/components/site-footer.tsx @@ -0,0 +1,95 @@ +import Link from "next/link"; +import { Logo } from "@/components/logo"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +const columns = [ + { + title: "Product", + links: [ + { href: "/platform", label: "Platform" }, + { href: "/fabric", label: "Fabric" }, + { href: "/solutions", label: "Solutions" }, + { href: "/pricing", label: "Pricing" }, + { href: "/request-access", label: "Request access" }, + ], + }, + { + title: "Company", + links: [ + { href: "/about", label: "About" }, + { href: "/customers", label: "Customers" }, + { href: "/careers", label: "Careers" }, + { href: "/insights", label: "Insights" }, + ], + }, + { + title: "Trust", + links: [ + { href: "/trust", label: "Trust center" }, + { href: "/privacy", label: "Privacy" }, + { href: "/terms", label: "Terms" }, + { href: "/pricing", label: "Sovereign" }, + ], + }, +]; + +export function SiteFooter() { + return ( + <footer className="relative border-t border-white/10 bg-background/40 backdrop-blur-xl"> + <div className="mx-auto grid max-w-6xl gap-12 px-5 py-16 sm:px-8 md:grid-cols-5"> + <div className="md:col-span-2"> + <Logo /> + <p className="mt-4 max-w-sm text-sm leading-relaxed text-muted-foreground"> + The agentic cloud for enterprise outcomes. Space-aware, + entanglement-ready, hyperautomation-compliant — and surprisingly + serious about Dogecoin. + </p> + <Link + href="/request-access" + className={cn( + buttonVariants({ variant: "outline" }), + "mt-6 h-9 px-3.5" + )} + > + Request access + </Link> + </div> + {columns.map((col) => ( + <div key={col.title}> + <p className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground"> + {col.title} + </p> + <ul className="mt-4 space-y-2.5"> + {col.links.map((link) => ( + <li key={link.href + link.label}> + <Link + href={link.href} + className="text-sm text-foreground/85 transition-colors hover:text-primary" + > + {link.label} + </Link> + </li> + ))} + </ul> + </div> + ))} + </div> + <div className="border-t border-white/10"> + <div className="mx-auto flex max-w-6xl flex-col gap-3 px-5 py-6 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:px-8"> + <p> + © {new Date().getFullYear()} The Agentic Cloud, Inc. All rights + reserved. + </p> + <p className="flex flex-wrap gap-x-4 gap-y-1"> + <span>SOC 2 Type II</span> + <span>ISO 27001</span> + <span>FedRAMP High</span> + <span>SLSA L3</span> + <span>GDPR · CCPA · COPPA</span> + </p> + </div> + </div> + </footer> + ); +} diff --git a/components/site-header.tsx b/components/site-header.tsx new file mode 100644 index 0000000..4d6df15 --- /dev/null +++ b/components/site-header.tsx @@ -0,0 +1,130 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; +import { Menu } from "lucide-react"; +import { Logo } from "@/components/logo"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { nav } from "@/lib/content"; +import { cn } from "@/lib/utils"; + +export function SiteHeader() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + + return ( + <header className="sticky top-0 z-40 border-b border-white/10 bg-background/40 backdrop-blur-2xl"> + <div className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-4 px-5 sm:px-8"> + <Link href="/" aria-label="The Agentic Cloud home" className="min-w-0"> + <Logo className="max-[420px]:[&>span:last-child]:hidden" /> + </Link> + <nav className="hidden items-center gap-6 lg:flex"> + {nav.map((item) => ( + <Link + key={item.href} + href={item.href} + className={cn( + "text-sm transition-colors hover:text-foreground", + pathname === item.href || pathname.startsWith(`${item.href}/`) + ? "text-foreground" + : "text-muted-foreground" + )} + > + {item.label} + </Link> + ))} + </nav> + <div className="hidden items-center gap-2 lg:flex"> + <Link + href="/about" + className={cn( + buttonVariants({ variant: "ghost" }), + "h-8 px-3 text-muted-foreground" + )} + > + About + </Link> + <Link + href="/request-access" + className={cn(buttonVariants(), "h-8 px-3.5")} + > + Request access + </Link> + </div> + <Sheet open={open} onOpenChange={setOpen}> + <SheetTrigger + nativeButton={false} + render={ + <Button + variant="ghost" + size="icon" + className="lg:hidden" + aria-label="Open menu" + /> + } + > + <Menu className="size-5" /> + </SheetTrigger> + <SheetContent side="right" className="bg-background"> + <SheetHeader> + <SheetTitle className="font-sans text-left text-sm font-medium tracking-tight"> + The Agentic Cloud + </SheetTitle> + </SheetHeader> + <nav className="flex flex-col gap-1 px-4"> + {nav.map((item) => ( + <Link + key={item.href} + href={item.href} + onClick={() => setOpen(false)} + className="rounded-lg px-2 py-2.5 text-base text-foreground/90 hover:bg-muted" + > + {item.label} + </Link> + ))} + <Link + href="/about" + onClick={() => setOpen(false)} + className="rounded-lg px-2 py-2.5 text-base text-foreground/90 hover:bg-muted" + > + About + </Link> + <Link + href="/careers" + onClick={() => setOpen(false)} + className="rounded-lg px-2 py-2.5 text-base text-foreground/90 hover:bg-muted" + > + Careers + </Link> + <Link + href="/trust" + onClick={() => setOpen(false)} + className="rounded-lg px-2 py-2.5 text-base text-foreground/90 hover:bg-muted" + > + Trust + </Link> + <Link + href="/request-access" + onClick={() => setOpen(false)} + className={cn( + buttonVariants(), + "mt-4 h-10 justify-center px-4" + )} + > + Request access + </Link> + </nav> + </SheetContent> + </Sheet> + </div> + </header> + ); +} diff --git a/components/solutions-tabs.tsx b/components/solutions-tabs.tsx new file mode 100644 index 0000000..0bc055c --- /dev/null +++ b/components/solutions-tabs.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { solutions } from "@/lib/content"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; + +export function SolutionsTabs() { + return ( + <Tabs defaultValue={solutions[0].id} className="w-full"> + <TabsList + variant="line" + className="h-auto w-full flex-wrap justify-start gap-1 rounded-none bg-transparent p-0" + > + {solutions.map((s) => ( + <TabsTrigger + key={s.id} + value={s.id} + className="h-9 px-3 text-sm data-active:text-foreground" + > + {s.label} + </TabsTrigger> + ))} + </TabsList> + {solutions.map((s) => ( + <TabsContent key={s.id} value={s.id} className="mt-10"> + <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-primary"> + {s.label} + </p> + <h2 className="mt-3 max-w-3xl font-heading text-3xl leading-tight text-foreground md:text-4xl"> + {s.headline} + </h2> + <p className="mt-5 max-w-3xl text-base leading-relaxed text-muted-foreground"> + {s.body} + </p> + <ul className="mt-8 grid gap-3 sm:grid-cols-3"> + {s.outcomes.map((o) => ( + <li + key={o} + className="rounded-xl border border-white/10 bg-white/5 p-4 text-sm leading-relaxed text-foreground/90 backdrop-blur-xl" + > + {o} + </li> + ))} + </ul> + </TabsContent> + ))} + </Tabs> + ); +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..6142db9 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,74 @@ +"use client" + +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + <AccordionPrimitive.Root + data-slot="accordion" + className={cn("flex w-full flex-col", className)} + {...props} + /> + ) +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + <AccordionPrimitive.Item + data-slot="accordion-item" + className={cn("not-last:border-b", className)} + {...props} + /> + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + <AccordionPrimitive.Header className="flex"> + <AccordionPrimitive.Trigger + data-slot="accordion-trigger" + className={cn( + "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", + className + )} + {...props} + > + {children} + <ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" /> + <ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" /> + </AccordionPrimitive.Trigger> + </AccordionPrimitive.Header> + ) +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + <AccordionPrimitive.Panel + data-slot="accordion-content" + className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up" + {...props} + > + <div + className={cn( + "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", + className + )} + > + {children} + </div> + </AccordionPrimitive.Panel> + ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..b20959d --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,52 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "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!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "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", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props + ), + render, + state: { + slot: "badge", + variant, + }, + }) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..78b28b7 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,59 @@ +import { Button as ButtonPrimitive } from "@base-ui/react/button" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "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", + { + variants: { + variant: { + default: + "vibe-btn border-transparent text-white hover:brightness-110", + outline: + "border-white/15 bg-white/5 text-foreground backdrop-blur-md hover:bg-white/10 hover:text-foreground aria-expanded:bg-white/10 aria-expanded:text-foreground dark:border-white/15 dark:bg-white/5 dark:hover:bg-white/10", + secondary: + "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", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "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", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + 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", + 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", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) { + return ( + <ButtonPrimitive + data-slot="button" + className={cn(buttonVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..4458dae --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( + <div + data-slot="card" + data-size={size} + className={cn( + "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", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-header" + className={cn( + "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)", + className + )} + {...props} + /> + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-title" + className={cn( + "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", + className + )} + {...props} + /> + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-description" + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-action" + className={cn( + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", + className + )} + {...props} + /> + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("px-(--card-spacing)", className)} + {...props} + /> + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-footer" + className={cn( + "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)", + className + )} + {...props} + /> + ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..014f5aa --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Dialog({ ...props }: DialogPrimitive.Root.Props) { + return <DialogPrimitive.Root data-slot="dialog" {...props} /> +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> +} + +function DialogOverlay({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + <DialogPrimitive.Backdrop + data-slot="dialog-overlay" + className={cn( + "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", + className + )} + {...props} + /> + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean +}) { + return ( + <DialogPortal> + <DialogOverlay /> + <DialogPrimitive.Popup + data-slot="dialog-content" + className={cn( + "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <DialogPrimitive.Close + data-slot="dialog-close" + render={ + <Button + variant="ghost" + className="absolute top-2 right-2" + size="icon-sm" + /> + } + > + <XIcon + /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + )} + </DialogPrimitive.Popup> + </DialogPortal> + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="dialog-header" + className={cn("flex flex-col gap-2", className)} + {...props} + /> + ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( + <div + data-slot="dialog-footer" + className={cn( + "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <DialogPrimitive.Close render={<Button variant="outline" />}> + Close + </DialogPrimitive.Close> + )} + </div> + ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + <DialogPrimitive.Title + data-slot="dialog-title" + className={cn( + "font-heading text-base leading-none font-medium", + className + )} + {...props} + /> + ) +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + <DialogPrimitive.Description + data-slot="dialog-description" + className={cn( + "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..7d21bab --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import { Input as InputPrimitive } from "@base-ui/react/input" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + <InputPrimitive + type={type} + data-slot="input" + className={cn( + "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none 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", + className + )} + {...props} + /> + ) +} + +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..74da65c --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,20 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( + <label + data-slot="label" + className={cn( + "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", + className + )} + {...props} + /> + ) +} + +export { Label } diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..5958637 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -0,0 +1,168 @@ +import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu" +import { cva } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon } from "lucide-react" + +function NavigationMenu({ + align = "start", + className, + children, + ...props +}: NavigationMenuPrimitive.Root.Props & + Pick<NavigationMenuPrimitive.Positioner.Props, "align">) { + return ( + <NavigationMenuPrimitive.Root + data-slot="navigation-menu" + className={cn( + "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", + className + )} + {...props} + > + {children} + <NavigationMenuPositioner align={align} /> + </NavigationMenuPrimitive.Root> + ) +} + +function NavigationMenuList({ + className, + ...props +}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.List>) { + return ( + <NavigationMenuPrimitive.List + data-slot="navigation-menu-list" + className={cn( + "group flex flex-1 list-none items-center justify-center gap-0", + className + )} + {...props} + /> + ) +} + +function NavigationMenuItem({ + className, + ...props +}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Item>) { + return ( + <NavigationMenuPrimitive.Item + data-slot="navigation-menu-item" + className={cn("relative", className)} + {...props} + /> + ) +} + +const navigationMenuTriggerStyle = cva( + "group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted" +) + +function NavigationMenuTrigger({ + className, + children, + ...props +}: NavigationMenuPrimitive.Trigger.Props) { + return ( + <NavigationMenuPrimitive.Trigger + data-slot="navigation-menu-trigger" + className={cn(navigationMenuTriggerStyle(), "group", className)} + {...props} + > + {children}{" "} + <ChevronDownIcon className="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180" aria-hidden="true" /> + </NavigationMenuPrimitive.Trigger> + ) +} + +function NavigationMenuContent({ + className, + ...props +}: NavigationMenuPrimitive.Content.Props) { + return ( + <NavigationMenuPrimitive.Content + data-slot="navigation-menu-content" + className={cn( + "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95", + className + )} + {...props} + /> + ) +} + +function NavigationMenuPositioner({ + className, + side = "bottom", + sideOffset = 8, + align = "start", + alignOffset = 0, + ...props +}: NavigationMenuPrimitive.Positioner.Props) { + return ( + <NavigationMenuPrimitive.Portal> + <NavigationMenuPrimitive.Positioner + side={side} + sideOffset={sideOffset} + align={align} + alignOffset={alignOffset} + className={cn( + "isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0", + className + )} + {...props} + > + <NavigationMenuPrimitive.Popup className="data-[ending-style]:easing-[ease] xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-popover text-popover-foreground shadow ring-1 ring-foreground/10 transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0"> + <NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" /> + </NavigationMenuPrimitive.Popup> + </NavigationMenuPrimitive.Positioner> + </NavigationMenuPrimitive.Portal> + ) +} + +function NavigationMenuLink({ + className, + ...props +}: NavigationMenuPrimitive.Link.Props) { + return ( + <NavigationMenuPrimitive.Link + data-slot="navigation-menu-link" + className={cn( + "flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md data-active:bg-muted/50 data-active:hover:bg-muted data-active:focus:bg-muted [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + /> + ) +} + +function NavigationMenuIndicator({ + className, + ...props +}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Icon>) { + return ( + <NavigationMenuPrimitive.Icon + data-slot="navigation-menu-indicator" + className={cn( + "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in", + className + )} + {...props} + > + <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> + </NavigationMenuPrimitive.Icon> + ) +} + +export { + NavigationMenu, + NavigationMenuContent, + NavigationMenuIndicator, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, + NavigationMenuPositioner, +} diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 0000000..6e1369e --- /dev/null +++ b/components/ui/separator.tsx @@ -0,0 +1,25 @@ +"use client" + +import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + ...props +}: SeparatorPrimitive.Props) { + return ( + <SeparatorPrimitive + data-slot="separator" + orientation={orientation} + className={cn( + "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", + className + )} + {...props} + /> + ) +} + +export { Separator } diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..78c0a76 --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,138 @@ +"use client" + +import * as React from "react" +import { Dialog as SheetPrimitive } from "@base-ui/react/dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Sheet({ ...props }: SheetPrimitive.Root.Props) { + return <SheetPrimitive.Root data-slot="sheet" {...props} /> +} + +function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) { + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> +} + +function SheetClose({ ...props }: SheetPrimitive.Close.Props) { + return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> +} + +function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) { + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> +} + +function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) { + return ( + <SheetPrimitive.Backdrop + data-slot="sheet-overlay" + className={cn( + "fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs", + className + )} + {...props} + /> + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: SheetPrimitive.Popup.Props & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Popup + data-slot="sheet-content" + data-side={side} + className={cn( + "fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", + className + )} + {...props} + > + {children} + {showCloseButton && ( + <SheetPrimitive.Close + data-slot="sheet-close" + render={ + <Button + variant="ghost" + className="absolute top-3 right-3" + size="icon-sm" + /> + } + > + <XIcon + /> + <span className="sr-only">Close</span> + </SheetPrimitive.Close> + )} + </SheetPrimitive.Popup> + </SheetPortal> + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-header" + className={cn("flex flex-col gap-0.5 p-4", className)} + {...props} + /> + ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sheet-footer" + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> + ) +} + +function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) { + return ( + <SheetPrimitive.Title + data-slot="sheet-title" + className={cn( + "font-heading text-base font-medium text-foreground", + className + )} + {...props} + /> + ) +} + +function SheetDescription({ + className, + ...props +}: SheetPrimitive.Description.Props) { + return ( + <SheetPrimitive.Description + data-slot="sheet-description" + className={cn("text-sm text-muted-foreground", className)} + {...props} + /> + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..8ee8054 --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,82 @@ +"use client" + +import { Tabs as TabsPrimitive } from "@base-ui/react/tabs" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: TabsPrimitive.Root.Props) { + return ( + <TabsPrimitive.Root + data-slot="tabs" + data-orientation={orientation} + className={cn( + "group/tabs flex gap-2 data-horizontal:flex-col", + className + )} + {...props} + /> + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) { + return ( + <TabsPrimitive.List + data-slot="tabs-list" + data-variant={variant} + className={cn(tabsListVariants({ variant }), className)} + {...props} + /> + ) +} + +function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { + return ( + <TabsPrimitive.Tab + data-slot="tabs-trigger" + className={cn( + "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", + "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", + "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", + className + )} + {...props} + /> + ) +} + +function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) { + return ( + <TabsPrimitive.Panel + data-slot="tabs-content" + className={cn("flex-1 text-sm outline-none", className)} + {...props} + /> + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx new file mode 100644 index 0000000..04d27f7 --- /dev/null +++ b/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( + <textarea + data-slot="textarea" + className={cn( + "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", + className + )} + {...props} + /> + ) +} + +export { Textarea } |
