A device-posture badge grid marking each managed device compliant, at-risk, or unmanaged, with a rollup of counts and per-device posture detail.
npx shadcn@latest add @paragon/device-trust"use client";
import * as React from "react";
import { useReducedMotion } from "motion/react";
import {
Laptop,
Server,
ShieldAlert,
ShieldCheck,
ShieldX,
Smartphone,
} from "lucide-react";
import { cn } from "@/lib/utils";
export type DevicePosture = "compliant" | "at-risk" | "unmanaged";
export type DeviceKind = "laptop" | "phone" | "server";
export interface TrustedDevice {
name: string;
owner: string;
kind: DeviceKind;
posture: DevicePosture;
/** Short posture detail, e.g. "Disk encrypted · OS current". */
detail?: string;
}
export interface DeviceTrustProps extends React.ComponentProps<"div"> {
devices?: TrustedDevice[];
/** Disables the staggered enter. */
static?: boolean;
}
const KIND_ICON: Record<DeviceKind, React.ElementType> = {
laptop: Laptop,
phone: Smartphone,
server: Server,
};
const POSTURE_META: Record<
DevicePosture,
{ icon: React.ElementType; ring: string; badge: string; label: string }
> = {
compliant: {
icon: ShieldCheck,
ring: "text-success",
badge: "bg-success/12 text-success",
label: "Compliant",
},
"at-risk": {
icon: ShieldAlert,
ring: "text-warning",
badge: "bg-warning/12 text-warning",
label: "At risk",
},
unmanaged: {
icon: ShieldX,
ring: "text-destructive",
badge: "bg-destructive/12 text-destructive",
label: "Unmanaged",
},
};
const DEFAULT_DEVICES: TrustedDevice[] = [
{ name: "MBP-14 · Morgan", owner: "Morgan Diaz", kind: "laptop", posture: "compliant", detail: "FileVault on · OS current" },
{ name: "iPhone 15 · Priya", owner: "Priya Raman", kind: "phone", posture: "compliant", detail: "MDM enrolled · passcode set" },
{ name: "build-runner-03", owner: "Platform", kind: "server", posture: "at-risk", detail: "Patch pending · CVE-2024-3094" },
{ name: "XPS-13 · Contractor", owner: "L. Okafor", kind: "laptop", posture: "unmanaged", detail: "No agent · personal device" },
];
/**
* A device-posture badge grid — managed / compliant / at-risk at a glance.
* Cards fade and rise on mount with a small stagger driven by CSS custom
* properties (animation-delay per index), so there is no JS timeline. Reduced
* motion removes the movement and keeps everything visible. Fully static
* content — no timers or randomness.
*/
export function DeviceTrust({
devices = DEFAULT_DEVICES,
static: isStatic = false,
className,
...props
}: DeviceTrustProps) {
const reduced = useReducedMotion();
const animate = !isStatic && !reduced;
const counts = devices.reduce(
(acc, d) => {
acc[d.posture] += 1;
return acc;
},
{ compliant: 0, "at-risk": 0, unmanaged: 0 } as Record<DevicePosture, number>,
);
return (
<div
data-slot="device-trust"
className={cn(
"w-full max-w-md rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<style href="paragon-device-trust" precedence="paragon">{`
@keyframes device-trust-in {
from { opacity: 0; transform: translateY(10px); filter: blur(4px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
[data-device-card] { animation: none !important; opacity: 1 !important; }
}
`}</style>
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold">Device trust</h3>
<div className="flex items-center gap-3 text-[11px] font-medium">
<span className="flex items-center gap-1 text-success tabular-nums">
<span className="size-1.5 rounded-full bg-success" />
{counts.compliant} ok
</span>
<span className="flex items-center gap-1 text-warning tabular-nums">
<span className="size-1.5 rounded-full bg-warning" />
{counts["at-risk"]} risk
</span>
<span className="flex items-center gap-1 text-destructive tabular-nums">
<span className="size-1.5 rounded-full bg-destructive" />
{counts.unmanaged} open
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
{devices.map((device, i) => {
const meta = POSTURE_META[device.posture];
const Icon = KIND_ICON[device.kind];
const Badge = meta.icon;
return (
<div
key={device.name}
data-device-card
className="rounded-lg border bg-background p-3"
style={
animate
? ({
animation: `device-trust-in var(--duration-base) var(--ease-out) both`,
animationDelay: `${i * 60}ms`,
} as React.CSSProperties)
: undefined
}
>
<div className="flex items-start justify-between">
<span className="flex size-8 items-center justify-center rounded-lg bg-secondary text-muted-foreground">
<Icon className="size-4" />
</span>
<Badge className={cn("size-4", meta.ring)} />
</div>
<p className="mt-2 truncate text-[13px] font-medium">
{device.name}
</p>
{device.detail && (
<p className="mt-0.5 truncate text-[11px] text-muted-foreground">
{device.detail}
</p>
)}
<span
className={cn(
"mt-2 inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide",
meta.badge,
)}
>
{meta.label}
</span>
</div>
);
})}
</div>
</div>
);
}