| 1 | import type { ReactNode } from "react"; |
| 2 | |
| 3 | const backdropClassNames = { |
| 4 | pairing: |
| 5 | "absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-emerald-500)_14%,transparent),transparent)]", |
| 6 | error: |
| 7 | "absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]", |
| 8 | brand: |
| 9 | "absolute inset-x-0 top-0 h-72 bg-[radial-gradient(48rem_20rem_at_top,color-mix(in_srgb,var(--color-blue-500)_12%,transparent),transparent)]", |
| 10 | }; |
| 11 | |
| 12 | /** Shared page and card geometry for entry points outside the app shell. */ |
| 13 | export function StandalonePage({ |
| 14 | tone, |
| 15 | masthead, |
| 16 | children, |
| 17 | }: { |
| 18 | readonly tone: keyof typeof backdropClassNames; |
| 19 | readonly masthead?: ReactNode; |
| 20 | readonly children: ReactNode; |
| 21 | }) { |
| 22 | return ( |
| 23 | <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6"> |
| 24 | <div className="pointer-events-none absolute inset-0 opacity-80" aria-hidden> |
| 25 | <div className={backdropClassNames[tone]} /> |
| 26 | {tone === "pairing" ? ( |
| 27 | <div className="absolute inset-y-0 left-0 w-72 bg-[radial-gradient(28rem_18rem_at_left,color-mix(in_srgb,var(--color-sky-500)_10%,transparent),transparent)]" /> |
| 28 | ) : null} |
| 29 | <div |
| 30 | className={ |
| 31 | tone === "brand" |
| 32 | ? "absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_94%,var(--color-black))_0%,var(--background)_62%)]" |
| 33 | : "absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" |
| 34 | } |
| 35 | /> |
| 36 | </div> |
| 37 | |
| 38 | <section |
| 39 | className={`relative w-full max-w-xl rounded-2xl border border-border/80 shadow-2xl shadow-black/20 backdrop-blur-md ${tone === "brand" ? "overflow-hidden bg-card/94" : "bg-card/90"}`} |
| 40 | > |
| 41 | {masthead} |
| 42 | <div className="p-6 sm:p-8">{children}</div> |
| 43 | </section> |
| 44 | </div> |
| 45 | ); |
| 46 | } |
| 47 | |
| 48 | /** Entry-page headings keep their typography and spacing together. */ |
| 49 | export function StandalonePageHeader({ |
| 50 | eyebrow, |
| 51 | title, |
| 52 | description, |
| 53 | }: { |
| 54 | readonly eyebrow: ReactNode; |
| 55 | readonly title: ReactNode; |
| 56 | readonly description: ReactNode; |
| 57 | }) { |
| 58 | return ( |
| 59 | <> |
| 60 | <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase"> |
| 61 | {eyebrow} |
| 62 | </p> |
| 63 | <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1> |
| 64 | <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{description}</p> |
| 65 | </> |
| 66 | ); |
| 67 | } |