An SSO/SAML connection card showing provider, protocol, verified domain, and live status, with a test-connection button that reports pass or fail inline.
npx shadcn@latest add @paragon/sso-connection"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
CheckCircle2,
Loader2,
Lock,
RefreshCw,
XCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
export type SsoStatus = "connected" | "error" | "disabled";
export interface SsoConnectionProps extends React.ComponentProps<"div"> {
/** Identity provider name. */
provider?: string;
/** Protocol label. */
protocol?: "SAML 2.0" | "OIDC" | "SCIM";
/** Metadata / ACS endpoint shown as the connection subject. */
endpoint?: string;
status?: SsoStatus;
/** Verified email domain. */
domain?: string;
/** Disables enter animation. */
static?: boolean;
}
type TestState = "idle" | "testing" | "passed" | "failed";
const STATUS_META: Record<
SsoStatus,
{ label: string; dot: string; text: string }
> = {
connected: {
label: "Connected",
dot: "bg-success",
text: "text-success",
},
error: { label: "Needs attention", dot: "bg-destructive", text: "text-destructive" },
disabled: { label: "Disabled", dot: "bg-muted-foreground", text: "text-muted-foreground" },
};
/**
* An SSO/SAML connection card with live status and a test-connection button.
* Running a test spins the trigger, then blur-crossfades to a pass/fail chip
* that clears itself after a moment; the swap uses AnimatePresence popLayout so
* the button width stays put. Reduced motion drops the spinner. Deterministic —
* pass/fail is driven by the given status, no randomness.
*/
export function SsoConnection({
provider = "Okta",
protocol = "SAML 2.0",
endpoint = "acme.okta.com/app/acme_prod/sso/saml",
status = "connected",
domain = "acme.com",
static: isStatic = false,
className,
...props
}: SsoConnectionProps) {
const reduced = useReducedMotion();
const [test, setTest] = React.useState<TestState>("idle");
const runTimer = React.useRef<ReturnType<typeof setTimeout>>(null);
const clearTimer = React.useRef<ReturnType<typeof setTimeout>>(null);
React.useEffect(() => {
return () => {
if (runTimer.current) clearTimeout(runTimer.current);
if (clearTimer.current) clearTimeout(clearTimer.current);
};
}, []);
const runTest = () => {
if (test === "testing") return;
setTest("testing");
if (runTimer.current) clearTimeout(runTimer.current);
runTimer.current = setTimeout(() => {
const result: TestState = status === "connected" ? "passed" : "failed";
setTest(result);
if (clearTimer.current) clearTimeout(clearTimer.current);
clearTimer.current = setTimeout(() => setTest("idle"), 2600);
}, 900);
};
const meta = STATUS_META[status];
return (
<div
data-slot="sso-connection"
className={cn(
"w-full max-w-md rounded-xl bg-card p-5 text-card-foreground shadow-border",
className,
)}
style={
!isStatic && !reduced
? ({
animation: "sso-enter var(--duration-base) var(--ease-out)",
} as React.CSSProperties)
: undefined
}
{...props}
>
<style href="paragon-sso-connection" precedence="paragon">{`
@keyframes sso-enter {
from { opacity: 0; transform: translateY(8px); filter: blur(4px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
[data-slot="sso-connection"] { animation: none !important; }
}
`}</style>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-lg bg-secondary text-foreground">
<Lock className="size-5" />
</span>
<div>
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">{provider}</h3>
<span className="rounded-md bg-secondary px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground">
{protocol}
</span>
</div>
<p className="mt-0.5 text-xs text-muted-foreground">
Verified domain{" "}
<span className="font-medium text-foreground">{domain}</span>
</p>
</div>
</div>
<span className={cn("flex items-center gap-1.5 text-xs font-medium", meta.text)}>
<span className={cn("size-2 rounded-full", meta.dot)} />
{meta.label}
</span>
</div>
<div className="mt-4 rounded-lg border bg-background px-3 py-2.5">
<p className="text-[11px] font-medium text-muted-foreground">
Single sign-on URL
</p>
<p className="mt-0.5 truncate font-mono text-[13px]">{endpoint}</p>
</div>
<div className="mt-4 flex items-center justify-between">
<p className="text-xs text-muted-foreground">
Last verified 2 hours ago
</p>
<button
type="button"
onClick={runTest}
disabled={test === "testing" || status === "disabled"}
className={cn(
"pressable relative flex h-8 min-w-[7.5rem] items-center justify-center gap-1.5 rounded-lg bg-card px-3 text-[13px] font-medium shadow-border transition-[box-shadow] duration-(--duration-fast) hover:shadow-border-hover",
"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:opacity-50",
)}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={test}
className="flex items-center gap-1.5 whitespace-nowrap"
initial={reduced ? false : { opacity: 0, filter: "blur(4px)", scale: 0.9 }}
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, filter: "blur(4px)", scale: 0.9 }}
transition={{ type: "spring", duration: 0.3, bounce: 0 }}
>
{test === "testing" && (
<>
<Loader2 className="size-3.5 animate-spin" />
Testing…
</>
)}
{test === "passed" && (
<>
<CheckCircle2 className="size-3.5 text-success" />
Test passed
</>
)}
{test === "failed" && (
<>
<XCircle className="size-3.5 text-destructive" />
Test failed
</>
)}
{test === "idle" && (
<>
<RefreshCw className="size-3.5" />
Test connection
</>
)}
</motion.span>
</AnimatePresence>
</button>
</div>
</div>
);
}