Security

Device Trust

A device-posture badge grid marking each managed device compliant, at-risk, or unmanaged, with a rollup of counts and per-device posture detail.

Install

npx shadcn@latest add @paragon/device-trust

device-trust.tsx

"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>
  );
}