Hover Card
Free, copy-and-go Hover Card components built on the SevenUI Hover Card primitive.Read the primitive docs.
Upgrading mid-cycle moves you to the new plan immediately. The difference is charged as proration on your next invoice, and unused time on the old plan is credited back.
"use client";
import { ArrowUpRightIcon } from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
export default function HoverCard01() {
return (
<p className="w-full max-w-md text-sm leading-relaxed text-muted-foreground">
Upgrading mid-cycle moves you to the new plan immediately. The difference
is charged as{" "}
<HoverCard>
<HoverCardTrigger
href="#proration"
className="rounded-sm font-medium text-foreground underline decoration-muted-foreground/60 decoration-dotted underline-offset-4 outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:decoration-foreground"
>
proration
</HoverCardTrigger>
<HoverCardContent side="top" className="flex w-72 flex-col gap-2 p-3">
<div className="flex items-baseline justify-between gap-2">
<span className="font-medium">Proration</span>
<span className="text-xs text-muted-foreground">Billing term</span>
</div>
<p className="text-muted-foreground">
A partial charge for the days left in your billing period. Moving
from Team to Business on day 18 of 30 bills 12 days at the new
rate, minus what you already paid.
</p>
<a
href="#billing-glossary"
className="inline-flex w-fit items-center gap-1 rounded-sm text-xs font-medium text-foreground underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
>
Billing glossary
<ArrowUpRightIcon className="size-3" aria-hidden="true" />
</a>
</HoverCardContent>
</HoverCard>{" "}
on your next invoice, and unused time on the old plan is credited back.
</p>
);
}
npx shadcn@latest add @sevenui/component/hover-card-01pnpm dlx shadcn@latest add @sevenui/component/hover-card-01yarn dlx shadcn@latest add @sevenui/component/hover-card-01bunx --bun shadcn@latest add @sevenui/component/hover-card-01Before you ship the new palette, skim Designing for dark mode from the design guild. It covers the contrast checks we run in review.
"use client";
import { ClockIcon, GlobeIcon } from "lucide-react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
export default function HoverCard02() {
return (
<p className="w-full max-w-md text-sm leading-relaxed text-muted-foreground">
Before you ship the new palette, skim{" "}
<HoverCard>
<HoverCardTrigger
href="#designing-for-dark-mode"
className="rounded-sm font-medium text-foreground underline decoration-muted-foreground/60 underline-offset-4 outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:decoration-foreground"
>
Designing for dark mode
</HoverCardTrigger>
<HoverCardContent className="w-80 max-w-[calc(100vw-2rem)] overflow-hidden p-0">
<AspectRatio ratio={16 / 9} className="bg-muted">
<img
src="/placeholder.svg"
alt="Two versions of a settings screen, one light and one dark"
className="size-full object-cover"
/>
</AspectRatio>
<div className="flex flex-col gap-1.5 p-3">
<span className="font-medium leading-snug">
Designing for dark mode: contrast, elevation, and color
</span>
<p className="line-clamp-2 text-muted-foreground">
Why pure black backgrounds fail, how to express elevation
without shadows, and which tokens need a second value.
</p>
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<GlobeIcon className="size-3" aria-hidden="true" />
sevenui.dev
</span>
<span className="inline-flex items-center gap-1">
<ClockIcon className="size-3" aria-hidden="true" />
7 min read
</span>
</div>
</div>
</HoverCardContent>
</HoverCard>{" "}
from the design guild. It covers the contrast checks we run in review.
</p>
);
}
npx shadcn@latest add @sevenui/component/hover-card-02pnpm dlx shadcn@latest add @sevenui/component/hover-card-02yarn dlx shadcn@latest add @sevenui/component/hover-card-02bunx --bun shadcn@latest add @sevenui/component/hover-card-02"use client";
import * as React from "react";
import {
AlignCenterHorizontalIcon,
AlignEndHorizontalIcon,
AlignStartHorizontalIcon,
ArrowDownIcon,
ArrowLeftIcon,
ArrowRightIcon,
ArrowUpIcon,
PackageIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Side = "top" | "right" | "bottom" | "left";
type Align = "start" | "center" | "end";
const sides: { value: Side; label: string; icon: typeof ArrowUpIcon }[] = [
{ value: "top", label: "Top", icon: ArrowUpIcon },
{ value: "right", label: "Right", icon: ArrowRightIcon },
{ value: "bottom", label: "Bottom", icon: ArrowDownIcon },
{ value: "left", label: "Left", icon: ArrowLeftIcon },
];
const aligns: { value: Align; label: string; icon: typeof ArrowUpIcon }[] = [
{ value: "start", label: "Start", icon: AlignStartHorizontalIcon },
{ value: "center", label: "Center", icon: AlignCenterHorizontalIcon },
{ value: "end", label: "End", icon: AlignEndHorizontalIcon },
];
export default function HoverCard03() {
const [side, setSide] = React.useState<Side>("top");
const [align, setAlign] = React.useState<Align>("center");
return (
<div className="flex w-full max-w-sm flex-col items-center gap-6">
<div className="flex w-full flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<span id="hover-card-side-label" className="text-sm font-medium">
Side
</span>
<ToggleGroup
aria-labelledby="hover-card-side-label"
variant="outline"
size="sm"
spacing={0}
value={[side]}
onValueChange={(next) => {
if (next.length > 0) setSide(next[0] as Side);
}}
>
{sides.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={item.label}
>
<item.icon aria-hidden="true" />
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<div className="flex items-center justify-between gap-3">
<span id="hover-card-align-label" className="text-sm font-medium">
Align
</span>
<ToggleGroup
aria-labelledby="hover-card-align-label"
variant="outline"
size="sm"
spacing={0}
value={[align]}
onValueChange={(next) => {
if (next.length > 0) setAlign(next[0] as Align);
}}
>
{aligns.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={item.label}
>
<item.icon aria-hidden="true" />
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</div>
<div className="flex w-full justify-center rounded-lg border border-dashed border-border py-10">
<HoverCard>
<HoverCardTrigger
render={<Button variant="outline" />}
>
<PackageIcon aria-hidden="true" />
Order 48213
</HoverCardTrigger>
<HoverCardContent
side={side}
align={align}
className="flex w-60 flex-col gap-2 p-3"
>
<div className="flex items-center justify-between gap-2">
<span className="font-medium">Arriving Thursday</span>
<span className="text-xs text-muted-foreground">2 of 3</span>
</div>
<p className="text-muted-foreground">
Left the Rotterdam hub at 06:40. The courier will text a
one-hour window on the morning of delivery.
</p>
<code className="w-fit rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
side="{side}" align="{align}"
</code>
</HoverCardContent>
</HoverCard>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/hover-card-03pnpm dlx shadcn@latest add @sevenui/component/hover-card-03yarn dlx shadcn@latest add @sevenui/component/hover-card-03bunx --bun shadcn@latest add @sevenui/component/hover-card-03Hotel in Lisbon, 3 nights: €412.80 is reimbursed in your home currency. Tune the delays, then hover the amount.
"use client";
import * as React from "react";
import { ArrowLeftRightIcon } from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Slider } from "@/components/ui/slider";
function describeDelay(ms: number) {
if (ms === 0) return "Instant";
if (ms < 400) return "Quick";
if (ms < 800) return "Deliberate";
return "Patient";
}
export default function HoverCard04() {
const [delay, setDelay] = React.useState(600);
const [closeDelay, setCloseDelay] = React.useState(300);
return (
<div className="flex w-full max-w-sm flex-col gap-6 rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span id="open-delay-label" className="font-medium">
Open delay
</span>
<span className="text-muted-foreground tabular-nums">
{describeDelay(delay)} · {delay} ms
</span>
</div>
<Slider
aria-labelledby="open-delay-label"
min={0}
max={1200}
step={100}
value={delay}
onValueChange={(next) => setDelay(next as number)}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span id="close-delay-label" className="font-medium">
Close delay
</span>
<span className="text-muted-foreground tabular-nums">
{closeDelay} ms
</span>
</div>
<Slider
aria-labelledby="close-delay-label"
min={0}
max={1000}
step={50}
value={closeDelay}
onValueChange={(next) => setCloseDelay(next as number)}
/>
</div>
</div>
<p className="border-t border-border pt-4 text-sm text-muted-foreground">
Hotel in Lisbon, 3 nights:{" "}
<HoverCard>
<HoverCardTrigger
href="#expense-lisbon-hotel"
delay={delay}
closeDelay={closeDelay}
className="rounded-sm font-medium text-foreground tabular-nums underline decoration-muted-foreground/60 decoration-dotted underline-offset-4 outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:decoration-foreground"
>
€412.80
</HoverCardTrigger>
<HoverCardContent className="flex w-64 flex-col gap-2 p-3">
<div className="flex items-baseline justify-between gap-2">
<span className="text-lg font-semibold tabular-nums">
$448.31
</span>
<span className="text-xs text-muted-foreground">USD</span>
</div>
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
<dt className="text-muted-foreground">Card rate</dt>
<dd className="text-right tabular-nums">1 EUR = 1.0860 USD</dd>
<dt className="text-muted-foreground">Foreign fee</dt>
<dd className="text-right tabular-nums">$0.00</dd>
</dl>
<p className="inline-flex items-center gap-1 border-t pt-2 text-xs text-muted-foreground">
<ArrowLeftRightIcon className="size-3" aria-hidden="true" />
Converted on Sep 22 at settlement
</p>
</HoverCardContent>
</HoverCard>{" "}
is reimbursed in your home currency. Tune the delays, then hover the
amount.
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/hover-card-04pnpm dlx shadcn@latest add @sevenui/component/hover-card-04yarn dlx shadcn@latest add @sevenui/component/hover-card-04bunx --bun shadcn@latest add @sevenui/component/hover-card-04"use client";
import * as React from "react";
import { CircleAlertIcon, PlaneIcon, RotateCwIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Skeleton } from "@/components/ui/skeleton";
type Flight = {
number: string;
from: { code: string; city: string; time: string };
to: { code: string; city: string; time: string };
gate: string;
status: string;
delayed?: boolean;
// Simulates a flaky request: the first fetch fails, the retry succeeds.
failsFirst?: boolean;
};
type LoadState = "idle" | "loading" | "ready" | "error";
const flights: Flight[] = [
{
number: "NW 1403",
from: { code: "FRA", city: "Frankfurt", time: "07:15" },
to: { code: "LIS", city: "Lisbon", time: "09:20" },
gate: "B44",
status: "On time",
},
{
number: "NW 2218",
from: { code: "LIS", city: "Lisbon", time: "14:05" },
to: { code: "OPO", city: "Porto", time: "15:30" },
gate: "12",
status: "Delayed 25 min",
delayed: true,
},
{
number: "NW 1407",
from: { code: "OPO", city: "Porto", time: "18:40" },
to: { code: "FRA", city: "Frankfurt", time: "22:35" },
gate: "Not assigned",
status: "Scheduled",
failsFirst: true,
},
];
function FlightReference({ flight }: { flight: Flight }) {
const [state, setState] = React.useState<LoadState>("idle");
const attempts = React.useRef(0);
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
function load() {
if (timer.current) clearTimeout(timer.current);
setState("loading");
timer.current = setTimeout(() => {
attempts.current += 1;
setState(flight.failsFirst && attempts.current === 1 ? "error" : "ready");
}, 900);
}
return (
<HoverCard
onOpenChange={(open) => {
if (open && (state === "idle" || state === "error")) load();
}}
>
<HoverCardTrigger
href={`#flight-${flight.number.replace(" ", "")}`}
className="rounded-sm font-medium whitespace-nowrap text-foreground underline decoration-muted-foreground/60 underline-offset-4 outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:decoration-foreground"
>
{flight.number}
</HoverCardTrigger>
<HoverCardContent className="w-72 p-3">
<div aria-live="polite" aria-busy={state === "loading"}>
{state === "loading" || state === "idle" ? (
<div className="flex flex-col gap-2.5">
<span className="sr-only">
Loading live status for {flight.number}
</span>
<div className="flex items-center justify-between">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-5 w-20 rounded-full" />
</div>
<div className="flex items-center justify-between gap-3">
<Skeleton className="h-7 w-14" />
<Skeleton className="h-px flex-1" />
<Skeleton className="h-7 w-14" />
</div>
<Skeleton className="h-3 w-32" />
</div>
) : state === "error" ? (
<div className="flex flex-col items-start gap-2">
<span className="inline-flex items-center gap-1.5 font-medium text-destructive">
<CircleAlertIcon className="size-4" aria-hidden="true" />
Couldn't load {flight.number}
</span>
<p className="text-muted-foreground">
The flight status service didn't respond. Your booking is
not affected.
</p>
<Button variant="outline" size="xs" onClick={load}>
<RotateCwIcon aria-hidden="true" />
Try again
</Button>
</div>
) : (
<div className="flex flex-col gap-2.5">
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">
{flight.number} · Thu, Oct 8
</span>
<Badge variant={flight.delayed ? "destructive" : "secondary"}>
{flight.status}
</Badge>
</div>
<div className="flex items-center justify-between gap-3">
<div className="flex flex-col">
<span className="text-lg font-semibold tabular-nums">
{flight.from.time}
</span>
<span className="text-xs text-muted-foreground">
{flight.from.code} · {flight.from.city}
</span>
</div>
<PlaneIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<div className="flex flex-col items-end">
<span className="text-lg font-semibold tabular-nums">
{flight.to.time}
</span>
<span className="text-xs text-muted-foreground">
{flight.to.city} · {flight.to.code}
</span>
</div>
</div>
<p className="border-t pt-2 text-xs text-muted-foreground">
Departure gate{" "}
<span className="font-medium text-foreground">
{flight.gate}
</span>
</p>
</div>
)}
</div>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard05() {
return (
<div className="flex w-full max-w-sm flex-col gap-3 rounded-xl border border-border bg-card p-4 text-sm text-card-foreground">
<span className="font-medium">Offsite travel, Oct 8</span>
<ul className="flex flex-col gap-2 text-muted-foreground">
<li>
Morning: fly out on <FlightReference flight={flights[0]} />
</li>
<li>
Afternoon: hop to Porto on <FlightReference flight={flights[1]} />
</li>
<li>
Evening: head home on <FlightReference flight={flights[2]} />
</li>
</ul>
<p className="text-xs text-muted-foreground">
Live status loads on first hover. NW 1407 fails once to show the error
state.
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/hover-card-05pnpm dlx shadcn@latest add @sevenui/component/hover-card-05yarn dlx shadcn@latest add @sevenui/component/hover-card-05bunx --bun shadcn@latest add @sevenui/component/hover-card-05"use client";
import * as React from "react";
import {
CircleCheckIcon,
ClockIcon,
GitBranchIcon,
GitCommitHorizontalIcon,
PinIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
const details = [
{ icon: GitBranchIcon, label: "Branch", value: "feat/checkout-v2" },
{ icon: GitCommitHorizontalIcon, label: "Commit", value: "a41f9c2" },
{ icon: ClockIcon, label: "Build time", value: "1m 48s" },
];
export default function HoverCard06() {
const [open, setOpen] = React.useState(false);
const [pinned, setPinned] = React.useState(false);
const controlsRef = React.useRef<HTMLDivElement>(null);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-sm text-card-foreground">
<div className="flex min-w-0 items-center gap-2">
<CircleCheckIcon
className="size-4 shrink-0 text-success"
aria-hidden="true"
/>
<span className="min-w-0 text-muted-foreground">
Preview deployed to{" "}
<HoverCard
open={open}
onOpenChange={(next, event) => {
// While pinned, pointer and focus movement can't close the
// card. Escape and outside clicks still dismiss it.
if (
!next &&
pinned &&
(event.reason === "trigger-hover" ||
event.reason === "trigger-focus")
) {
return;
}
// The pin switch and Close button below manage the card
// themselves, so a press on them isn't an outside dismiss.
if (
!next &&
event.reason === "outside-press" &&
event.event.target instanceof Node &&
controlsRef.current?.contains(event.event.target)
) {
return;
}
setOpen(next);
if (!next) setPinned(false);
}}
>
<HoverCardTrigger
href="#deployment-dpl-7hq2"
className="rounded-sm font-medium whitespace-nowrap text-foreground underline decoration-muted-foreground/60 underline-offset-4 outline-none hover:decoration-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:decoration-foreground"
>
checkout-v2.acme.dev
</HoverCardTrigger>
<HoverCardContent
align="start"
className="flex w-72 flex-col gap-3 p-3"
>
<div className="flex items-center justify-between gap-2">
<span className="font-medium">Deployment dpl_7Hq2</span>
{pinned && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<PinIcon className="size-3" aria-hidden="true" />
Pinned
</span>
)}
</div>
<dl className="flex flex-col gap-1.5">
{details.map((item) => (
<div
key={item.label}
className="flex items-center justify-between gap-3"
>
<dt className="inline-flex items-center gap-1.5 text-muted-foreground">
<item.icon className="size-3.5" aria-hidden="true" />
{item.label}
</dt>
<dd className="truncate font-mono text-xs">
{item.value}
</dd>
</div>
))}
</dl>
<p className="text-xs text-muted-foreground">
Ready 4 minutes ago · triggered by Priya Raman
</p>
</HoverCardContent>
</HoverCard>
</span>
</div>
</div>
<div
ref={controlsRef}
className="flex items-center justify-between gap-3"
>
<div className="flex items-center gap-2">
<Switch
id="pin-deployment-preview"
checked={pinned}
onCheckedChange={(checked) => {
setPinned(checked);
setOpen(checked);
}}
/>
<Label htmlFor="pin-deployment-preview">Pin preview open</Label>
</div>
<Button
variant="ghost"
size="sm"
disabled={!open}
onClick={() => {
setPinned(false);
setOpen(false);
}}
>
Close
</Button>
</div>
<p className="text-xs text-muted-foreground" aria-live="polite">
{open
? pinned
? "Preview is pinned. Press Escape or click outside to close it."
: "Preview is open and follows the pointer."
: "Preview is closed. Hover the link or pin it open."}
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/hover-card-06pnpm dlx shadcn@latest add @sevenui/component/hover-card-06yarn dlx shadcn@latest add @sevenui/component/hover-card-06bunx --bun shadcn@latest add @sevenui/component/hover-card-06Public API
99.91% uptime"use client";
import {
CircleAlertIcon,
CircleCheckIcon,
TriangleAlertIcon,
} from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type DayStatus = "operational" | "degraded" | "outage";
type Day = {
date: string;
uptime: number;
status: DayStatus;
incident?: { title: string; duration: string };
};
// Days with incidents, keyed by their index in the 30-day window.
const incidents: Record<number, Omit<Day, "date">> = {
6: {
uptime: 99.42,
status: "degraded",
incident: {
title: "Elevated latency on webhook delivery in eu-west",
duration: "38 min",
},
},
17: {
uptime: 97.91,
status: "outage",
incident: {
title: "API returned 503 errors after a failed database failover",
duration: "1 hr 12 min",
},
},
24: {
uptime: 99.86,
status: "degraded",
incident: {
title: "Dashboard charts loaded slowly for some workspaces",
duration: "14 min",
},
},
};
const dateFormat = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
});
const days: Day[] = Array.from({ length: 30 }, (_, index) => {
// Aug 27 through Sep 25, 2026.
const date = dateFormat.format(new Date(2026, 7, 27 + index));
return incidents[index]
? { date, ...incidents[index] }
: { date, uptime: 100, status: "operational" };
});
const statusMeta: Record<
DayStatus,
{ label: string; bar: string; icon: typeof CircleCheckIcon; text: string }
> = {
operational: {
label: "No incidents",
bar: "bg-success/70",
icon: CircleCheckIcon,
text: "text-success",
},
degraded: {
label: "Degraded performance",
bar: "bg-warning",
icon: TriangleAlertIcon,
text: "text-warning",
},
outage: {
label: "Partial outage",
bar: "bg-destructive",
icon: CircleAlertIcon,
text: "text-destructive",
},
};
const average =
days.reduce((sum, day) => sum + day.uptime, 0) / Math.max(days.length, 1);
export default function HoverCard07() {
return (
<section
aria-labelledby="hover-card-07-title"
className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground"
>
<header className="flex items-baseline justify-between gap-3">
<h3 id="hover-card-07-title" className="text-sm font-medium">
Public API
</h3>
<span className="text-xs text-muted-foreground tabular-nums">
{average.toFixed(2)}% uptime
</span>
</header>
{/* One card serves all 30 bars: each trigger passes its day as payload. */}
<HoverCard>
{({ payload }) => {
const day = typeof payload === "number" ? days[payload] : undefined;
const meta = day ? statusMeta[day.status] : undefined;
return (
<>
<ol
aria-label="Daily uptime, last 30 days"
className="mt-3 flex h-9 items-stretch gap-px sm:gap-0.5"
>
{days.map((item, index) => (
<li key={item.date} className="flex min-w-0 flex-1">
<HoverCardTrigger
payload={index}
delay={0}
closeDelay={100}
render={
<button
type="button"
aria-label={`${item.date}: ${statusMeta[item.status].label}, ${item.uptime}% uptime`}
className={`w-full rounded-[2px] outline-none transition-opacity hover:opacity-70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card data-popup-open:opacity-70 ${statusMeta[item.status].bar}`}
/>
}
/>
</li>
))}
</ol>
<HoverCardContent side="top" className="w-64 p-3">
{day && meta && (
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between gap-2">
<span className="font-medium">{day.date}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{day.uptime.toFixed(2)}%
</span>
</div>
<span className="inline-flex items-center gap-1.5 text-xs">
<meta.icon
aria-hidden="true"
className={`size-3.5 shrink-0 ${meta.text}`}
/>
{meta.label}
</span>
{day.incident && (
<div className="flex flex-col gap-0.5 border-t pt-2">
<p className="leading-snug">{day.incident.title}</p>
<p className="text-xs text-muted-foreground">
Resolved after {day.incident.duration}
</p>
</div>
)}
</div>
)}
</HoverCardContent>
</>
);
}}
</HoverCard>
<div
aria-hidden="true"
className="mt-2 flex justify-between text-xs text-muted-foreground"
>
<span>30 days ago</span>
<span>Today</span>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-07pnpm dlx shadcn@latest add @sevenui/component/hover-card-07yarn dlx shadcn@latest add @sevenui/component/hover-card-07bunx --bun shadcn@latest add @sevenui/component/hover-card-07"use client";
import {
CircleCheckIcon,
CircleDashedIcon,
CircleDotIcon,
GitCommitHorizontalIcon,
SignalHighIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type IssueStatus = "todo" | "in-progress" | "done";
type Issue = {
key: string;
title: string;
status: IssueStatus;
priority: string;
labels: string[];
assignee: { name: string; initials: string };
updated: string;
};
const issues: Record<string, Issue> = {
"ENG-482": {
key: "ENG-482",
title: "Webhook retries fire twice after a 502 from the receiver",
status: "in-progress",
priority: "High",
labels: ["webhooks", "bug"],
assignee: { name: "Priya Raman", initials: "PR" },
updated: "Updated 2 hours ago",
},
"ENG-517": {
key: "ENG-517",
title: "Expose retry backoff settings in the dashboard",
status: "todo",
priority: "Medium",
labels: ["webhooks", "settings"],
assignee: { name: "Marcus Lee", initials: "ML" },
updated: "Updated yesterday",
},
"ENG-455": {
key: "ENG-455",
title: "Add idempotency keys to outbound event payloads",
status: "done",
priority: "High",
labels: ["api"],
assignee: { name: "Priya Raman", initials: "PR" },
updated: "Closed on Sep 19",
},
};
const commits = [
{
sha: "a91f3c2",
message: "Guard retry scheduler against duplicate 502 handling",
issue: "ENG-482",
author: "Priya Raman",
time: "2h ago",
},
{
sha: "7d0e1b8",
message: "Read backoff multiplier from workspace settings",
issue: "ENG-517",
author: "Marcus Lee",
time: "5h ago",
},
{
sha: "3c55a90",
message: "Attach Idempotency-Key header to every delivery",
issue: "ENG-455",
author: "Priya Raman",
time: "Sep 19",
},
];
const statusMeta: Record<
IssueStatus,
{ label: string; icon: typeof CircleDotIcon; className: string }
> = {
todo: {
label: "Todo",
icon: CircleDashedIcon,
className: "text-muted-foreground",
},
"in-progress": {
label: "In progress",
icon: CircleDotIcon,
className: "text-warning",
},
done: { label: "Done", icon: CircleCheckIcon, className: "text-success" },
};
function IssueLink({ issue }: { issue: Issue }) {
const status = statusMeta[issue.status];
const StatusIcon = status.icon;
return (
<HoverCard>
<HoverCardTrigger
href={`#issue-${issue.key}`}
delay={300}
className="rounded-sm font-mono text-xs font-medium text-primary underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring/50"
>
{issue.key}
</HoverCardTrigger>
<HoverCardContent
align="start"
className="w-80 max-w-[calc(100vw-2rem)] p-0"
>
<div className="grid gap-2 p-3">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-mono">{issue.key}</span>
<span aria-hidden="true">·</span>
<span>{issue.updated}</span>
</div>
<p className="text-sm leading-snug font-medium text-balance">
{issue.title}
</p>
<div className="flex flex-wrap gap-1">
{issue.labels.map((label) => (
<Badge key={label} variant="outline">
{label}
</Badge>
))}
</div>
</div>
<dl className="grid grid-cols-3 gap-2 border-t px-3 py-2.5 text-xs">
<div className="grid gap-1">
<dt className="text-muted-foreground">Status</dt>
<dd className="flex items-center gap-1 font-medium">
<StatusIcon
aria-hidden="true"
className={`size-3.5 ${status.className}`}
/>
{status.label}
</dd>
</div>
<div className="grid gap-1">
<dt className="text-muted-foreground">Priority</dt>
<dd className="flex items-center gap-1 font-medium">
<SignalHighIcon aria-hidden="true" className="size-3.5" />
{issue.priority}
</dd>
</div>
<div className="grid gap-1">
<dt className="text-muted-foreground">Assignee</dt>
<dd className="flex min-w-0 items-center gap-1 font-medium">
<Avatar size="sm" className="size-4">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback className="text-[0.5rem]">
{issue.assignee.initials}
</AvatarFallback>
</Avatar>
<span className="truncate">
{issue.assignee.name.split(" ")[0]}
</span>
</dd>
</div>
</dl>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard08() {
return (
<section
aria-labelledby="hover-card-08-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-baseline justify-between gap-2 border-b px-4 py-3">
<h3 id="hover-card-08-title" className="text-sm font-medium">
Commits on fix/webhook-retries
</h3>
<span className="shrink-0 text-xs whitespace-nowrap text-muted-foreground tabular-nums">
3 commits
</span>
</header>
<ol className="divide-y">
{commits.map((commit) => {
const issue = issues[commit.issue];
return (
<li key={commit.sha} className="flex gap-3 px-4 py-3">
<GitCommitHorizontalIcon
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-muted-foreground"
/>
<div className="grid min-w-0 flex-1 gap-1">
<p className="text-sm leading-snug">{commit.message}</p>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
{issue && <IssueLink issue={issue} />}
<span aria-hidden="true">·</span>
<span>{commit.author}</span>
<span aria-hidden="true">·</span>
<span>{commit.time}</span>
</div>
</div>
<code className="hidden shrink-0 self-start rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground sm:block">
{commit.sha}
</code>
</li>
);
})}
</ol>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-08pnpm dlx shadcn@latest add @sevenui/component/hover-card-08yarn dlx shadcn@latest add @sevenui/component/hover-card-08bunx --bun shadcn@latest add @sevenui/component/hover-card-08Q4 Pricing Proposal
Edited 2 minutes ago
"use client";
import { ClockIcon, EyeIcon, FileTextIcon, MailIcon } from "lucide-react";
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type Collaborator = {
name: string;
initials: string;
email: string;
title: string;
access: "Owner" | "Can edit" | "Can comment";
localTime: string;
timezone: string;
activity: string;
online: boolean;
};
const collaborators: Collaborator[] = [
{
name: "Elena Novak",
initials: "EN",
email: "elena@northwind.io",
title: "Head of Product",
access: "Owner",
localTime: "4:12 PM",
timezone: "Berlin",
activity: "Editing section 3 now",
online: true,
},
{
name: "Daniel Okafor",
initials: "DO",
email: "daniel@northwind.io",
title: "Staff Engineer",
access: "Can edit",
localTime: "3:12 PM",
timezone: "Lagos",
activity: "Viewed 20 minutes ago",
online: true,
},
{
name: "Sofia Martins",
initials: "SM",
email: "sofia@northwind.io",
title: "Product Designer",
access: "Can comment",
localTime: "11:12 AM",
timezone: "São Paulo",
activity: "Left 4 comments yesterday",
online: false,
},
];
const hiddenCount = 4;
function CollaboratorAvatar({ person }: { person: Collaborator }) {
return (
<HoverCard>
<HoverCardTrigger
delay={200}
render={
<button
type="button"
aria-label={`${person.name}, ${person.access.toLowerCase()}`}
className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
/>
}
>
<Avatar className="ring-2 ring-card">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{person.initials}</AvatarFallback>
{person.online && <AvatarBadge className="bg-success ring-card" />}
</Avatar>
</HoverCardTrigger>
<HoverCardContent
align="end"
className="w-72 max-w-[calc(100vw-2rem)] p-0"
>
<div className="flex items-start gap-3 p-3">
<Avatar size="lg">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
<div className="grid min-w-0 flex-1 gap-0.5">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-sm font-medium">{person.name}</p>
<Badge
variant={person.access === "Owner" ? "default" : "secondary"}
>
{person.access}
</Badge>
</div>
<p className="truncate text-xs text-muted-foreground">
{person.title}
</p>
</div>
</div>
<ul className="grid gap-2 border-t px-3 py-2.5 text-xs text-muted-foreground">
<li className="flex items-center gap-2">
<MailIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span className="truncate">{person.email}</span>
</li>
<li className="flex items-center gap-2">
<ClockIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span>
<span className="text-foreground tabular-nums">
{person.localTime}
</span>{" "}
local time in {person.timezone}
</span>
</li>
<li className="flex items-center gap-2">
<EyeIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span className={person.online ? "text-foreground" : undefined}>
{person.activity}
</span>
</li>
</ul>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard09() {
return (
<div className="flex w-full max-w-md flex-wrap items-center justify-between gap-3 rounded-xl border bg-card px-4 py-3 text-card-foreground">
<div className="flex min-w-0 items-center gap-2.5">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
<FileTextIcon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
</span>
<div className="min-w-0">
<p className="truncate text-sm font-medium">Q4 Pricing Proposal</p>
<p className="truncate text-xs text-muted-foreground">
Edited 2 minutes ago
</p>
</div>
</div>
<div className="flex items-center gap-2">
<AvatarGroup role="group" aria-label="Shared with">
{collaborators.map((person) => (
<CollaboratorAvatar key={person.email} person={person} />
))}
<AvatarGroupCount className="text-xs ring-card">
<span aria-hidden="true">+{hiddenCount}</span>
<span className="sr-only">and {hiddenCount} more people</span>
</AvatarGroupCount>
</AvatarGroup>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/hover-card-09pnpm dlx shadcn@latest add @sevenui/component/hover-card-09yarn dlx shadcn@latest add @sevenui/component/hover-card-09bunx --bun shadcn@latest add @sevenui/component/hover-card-09Upcoming invoice
Charged to Visa ending 4242 on Oct 1
- Scale planSep 1 – Sep 30 · 12 seats$588.00
- API requests$144.00
- Storage$15.50
"use client";
import { InfoIcon, ReceiptTextIcon } from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
type Breakdown = {
heading: string;
note: string;
rows: { label: string; detail: string; amount: string; share: number }[];
};
type LineItem = {
label: string;
meta: string;
amount: string;
breakdown?: Breakdown;
};
const lineItems: LineItem[] = [
{
label: "Scale plan",
meta: "Sep 1 – Sep 30 · 12 seats",
amount: "$588.00",
},
{
label: "API requests",
meta: "4.2M of 3M included",
amount: "$144.00",
breakdown: {
heading: "1.2M requests over your included quota",
note: "Billed at $0.12 per 1,000 requests above 3M.",
rows: [
{
label: "checkout-service",
detail: "680K over",
amount: "$81.60",
share: 57,
},
{
label: "search-indexer",
detail: "390K over",
amount: "$46.80",
share: 32,
},
{
label: "staging",
detail: "130K over",
amount: "$15.60",
share: 11,
},
],
},
},
{
label: "Storage",
meta: "312 GB of 250 GB included",
amount: "$15.50",
breakdown: {
heading: "62 GB over your included storage",
note: "Billed at $0.25 per GB-month, prorated daily.",
rows: [
{
label: "Media uploads",
detail: "41 GB over",
amount: "$10.25",
share: 66,
},
{
label: "Database backups",
detail: "21 GB over",
amount: "$5.25",
share: 34,
},
],
},
},
];
function BreakdownCard({
item,
breakdown,
}: {
item: LineItem;
breakdown: Breakdown;
}) {
return (
<HoverCard>
<HoverCardTrigger
delay={250}
render={
<button
type="button"
className="group inline-flex items-center gap-1 rounded-sm text-left text-xs text-muted-foreground underline decoration-dotted underline-offset-4 outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50"
/>
}
>
{item.meta}
<InfoIcon aria-hidden="true" className="size-3 shrink-0" />
<span className="sr-only">, show {item.label} breakdown</span>
</HoverCardTrigger>
<HoverCardContent
side="top"
align="start"
className="w-80 max-w-[calc(100vw-2rem)] p-3"
>
<p className="text-sm font-medium">{breakdown.heading}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{breakdown.note}
</p>
<ul className="mt-3 grid gap-3">
{breakdown.rows.map((row) => (
<li key={row.label} className="grid gap-1.5">
<div className="flex items-baseline justify-between gap-2 text-xs">
<span className="truncate font-mono">{row.label}</span>
<span className="shrink-0 font-medium tabular-nums">
{row.amount}
</span>
</div>
<Progress
value={row.share}
aria-label={`${row.label}: ${row.share}% of overage, ${row.detail}`}
className="gap-0 [&_[data-slot=progress-track]]:h-1"
/>
</li>
))}
</ul>
<div className="mt-3 flex items-center justify-between border-t pt-2.5 text-xs">
<span className="text-muted-foreground">Overage total</span>
<span className="font-medium tabular-nums">{item.amount}</span>
</div>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard10() {
return (
<section
aria-labelledby="hover-card-10-title"
className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground"
>
<header className="flex items-start justify-between gap-3">
<div>
<h3 id="hover-card-10-title" className="text-sm font-medium">
Upcoming invoice
</h3>
<p className="text-xs text-muted-foreground">
Charged to Visa ending 4242 on Oct 1
</p>
</div>
<ReceiptTextIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
</header>
<ul className="mt-4 grid gap-3">
{lineItems.map((item) => (
<li
key={item.label}
className="flex items-start justify-between gap-3"
>
<div className="grid min-w-0 gap-0.5">
<span className="text-sm">{item.label}</span>
{item.breakdown ? (
<BreakdownCard item={item} breakdown={item.breakdown} />
) : (
<span className="text-xs text-muted-foreground">
{item.meta}
</span>
)}
</div>
<span className="text-sm tabular-nums">{item.amount}</span>
</li>
))}
</ul>
<Separator className="my-4" />
<div className="flex items-baseline justify-between gap-3">
<span className="text-sm font-medium">Estimated total</span>
<span className="text-lg font-semibold tabular-nums">$747.50</span>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-10pnpm dlx shadcn@latest add @sevenui/component/hover-card-10yarn dlx shadcn@latest add @sevenui/component/hover-card-10bunx --bun shadcn@latest add @sevenui/component/hover-card-10Autumn launch
4 files · 151 MB- storefront-hero.pngToday, 9:41 AM2.4 MB
- launch-brief.mdYesterday18 KB
- q3-returns.csvSep 21640 KB
- unboxing-cut-v2.mp4Sep 18148 MB
"use client";
import {
FileSpreadsheetIcon,
FileTextIcon,
FileVideoIcon,
ImageIcon,
LinkIcon,
LockIcon,
UsersIcon,
} from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type Sharing = "private" | "team" | "link";
type FileEntry = {
name: string;
kind: string;
icon: typeof FileTextIcon;
size: string;
modified: string;
modifiedBy: string;
sharing: Sharing;
facts: { label: string; value: string }[];
preview: "image" | "text" | "none";
excerpt?: string;
};
const files: FileEntry[] = [
{
name: "storefront-hero.png",
kind: "PNG image",
icon: ImageIcon,
size: "2.4 MB",
modified: "Today, 9:41 AM",
modifiedBy: "Hana Ito",
sharing: "team",
facts: [
{ label: "Dimensions", value: "2880 × 1620" },
{ label: "Color profile", value: "Display P3" },
],
preview: "image",
},
{
name: "launch-brief.md",
kind: "Markdown",
icon: FileTextIcon,
size: "18 KB",
modified: "Yesterday",
modifiedBy: "Omar Haddad",
sharing: "link",
facts: [
{ label: "Words", value: "1,284" },
{ label: "Versions", value: "7" },
],
preview: "text",
excerpt:
"The autumn launch moves the storefront to the new checkout. Goals: cut drop-off at the shipping step by 15% and ship gift cards to every region.",
},
{
name: "q3-returns.csv",
kind: "CSV spreadsheet",
icon: FileSpreadsheetIcon,
size: "640 KB",
modified: "Sep 21",
modifiedBy: "Hana Ito",
sharing: "private",
facts: [
{ label: "Rows", value: "9,812" },
{ label: "Columns", value: "14" },
],
preview: "none",
},
{
name: "unboxing-cut-v2.mp4",
kind: "MPEG-4 video",
icon: FileVideoIcon,
size: "148 MB",
modified: "Sep 18",
modifiedBy: "Lucas Brandt",
sharing: "team",
facts: [
{ label: "Duration", value: "1:42" },
{ label: "Resolution", value: "1080p" },
],
preview: "image",
},
];
const sharingMeta: Record<Sharing, { label: string; icon: typeof LockIcon }> =
{
private: { label: "Private to you", icon: LockIcon },
team: { label: "Shared with Marketing", icon: UsersIcon },
link: { label: "Anyone with the link can view", icon: LinkIcon },
};
function FilePreview({ file }: { file: FileEntry }) {
const Icon = file.icon;
if (file.preview === "image") {
return (
<img
src="/placeholder.svg"
alt={`Preview of ${file.name}`}
className="aspect-video w-full rounded-t-lg bg-muted object-cover"
/>
);
}
if (file.preview === "text") {
return (
<div className="rounded-t-lg bg-muted px-3 py-2.5">
<p className="line-clamp-4 font-mono text-[0.7rem] leading-relaxed text-muted-foreground">
{file.excerpt}
</p>
</div>
);
}
return (
<div className="flex aspect-[3/1] w-full flex-col items-center justify-center gap-1 rounded-t-lg bg-muted text-muted-foreground">
<Icon aria-hidden="true" className="size-5" />
<span className="text-xs">No preview for this file type</span>
</div>
);
}
export default function HoverCard11() {
return (
<section
aria-labelledby="hover-card-11-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-baseline justify-between gap-2 border-b px-4 py-3">
<h3 id="hover-card-11-title" className="text-sm font-medium">
Autumn launch
</h3>
<span className="text-xs text-muted-foreground">4 files · 151 MB</span>
</header>
<ul className="divide-y">
{files.map((file) => {
const Icon = file.icon;
const sharing = sharingMeta[file.sharing];
const SharingIcon = sharing.icon;
return (
<li
key={file.name}
className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50"
>
<Icon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<HoverCard>
<HoverCardTrigger
href={`#file-${file.name}`}
delay={400}
className="min-w-0 flex-1 truncate rounded-sm text-sm outline-none hover:underline hover:underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring/50"
>
{file.name}
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className="w-64 max-w-[calc(100vw-2rem)] p-0"
>
<FilePreview file={file} />
<div className="grid gap-2.5 p-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{file.kind} · {file.size}
</p>
</div>
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
{file.facts.map((fact) => (
<div key={fact.label} className="contents">
<dt className="text-muted-foreground">
{fact.label}
</dt>
<dd className="text-right tabular-nums">
{fact.value}
</dd>
</div>
))}
<dt className="text-muted-foreground">Modified</dt>
<dd className="truncate text-right">
{file.modified} by {file.modifiedBy.split(" ")[0]}
</dd>
</dl>
<p className="flex items-center gap-1.5 border-t pt-2.5 text-xs text-muted-foreground">
<SharingIcon
aria-hidden="true"
className="size-3.5 shrink-0"
/>
{sharing.label}
</p>
</div>
</HoverCardContent>
</HoverCard>
<span className="hidden shrink-0 text-xs text-muted-foreground sm:inline">
{file.modified}
</span>
<span className="w-14 shrink-0 text-right text-xs text-muted-foreground tabular-nums">
{file.size}
</span>
</li>
);
})}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-11pnpm dlx shadcn@latest add @sevenui/component/hover-card-11yarn dlx shadcn@latest add @sevenui/component/hover-card-11bunx --bun shadcn@latest add @sevenui/component/hover-card-11Friday, Sep 25
3 events"use client";
import {
CheckIcon,
CircleHelpIcon,
MapPinIcon,
VideoIcon,
XIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type Rsvp = "yes" | "no" | "maybe";
type CalendarEvent = {
id: string;
title: string;
start: number;
end: number;
location: { kind: "room" | "video"; label: string };
tone: string;
description: string;
attendees: { name: string; initials: string; rsvp: Rsvp }[];
};
const dayStart = 9;
const dayEnd = 14;
const hourHeight = 3.5; // rem
const events: CalendarEvent[] = [
{
id: "standup",
title: "Checkout squad standup",
start: 9.5,
end: 9.75,
location: { kind: "video", label: "meet.northwind.io/checkout" },
tone: "border-chart-1/40 bg-chart-1/10",
description: "Blockers first, then yesterday's deploy notes.",
attendees: [
{ name: "Amara Diallo", initials: "AD", rsvp: "yes" },
{ name: "Tomás Rivera", initials: "TR", rsvp: "yes" },
{ name: "Grace Kim", initials: "GK", rsvp: "maybe" },
],
},
{
id: "review",
title: "Payment flow design review",
start: 10.5,
end: 12,
location: { kind: "room", label: "Harbor room, 4th floor" },
tone: "border-chart-2/40 bg-chart-2/10",
description:
"Walk through the new 3-D Secure fallback and the saved-card picker.",
attendees: [
{ name: "Grace Kim", initials: "GK", rsvp: "yes" },
{ name: "Noah Fischer", initials: "NF", rsvp: "yes" },
{ name: "Amara Diallo", initials: "AD", rsvp: "no" },
{ name: "Leila Karimi", initials: "LK", rsvp: "maybe" },
],
},
{
id: "interview",
title: "Interview: Senior Frontend",
start: 12.5,
end: 13.5,
location: { kind: "video", label: "meet.northwind.io/hiring-fe" },
tone: "border-chart-4/40 bg-chart-4/10",
description: "System design round. Scorecard due by end of day.",
attendees: [
{ name: "Tomás Rivera", initials: "TR", rsvp: "yes" },
{ name: "Leila Karimi", initials: "LK", rsvp: "yes" },
],
},
];
const rsvpMeta: Record<
Rsvp,
{ label: string; icon: typeof CheckIcon; className: string }
> = {
yes: {
label: "Going",
icon: CheckIcon,
className: "bg-success text-background",
},
no: {
label: "Declined",
icon: XIcon,
className: "bg-destructive text-background",
},
maybe: {
label: "Maybe",
icon: CircleHelpIcon,
className: "bg-muted-foreground text-background",
},
};
function formatTime(value: number) {
const hours = Math.floor(value);
const minutes = Math.round((value - hours) * 60);
const suffix = hours >= 12 ? "PM" : "AM";
const display = hours > 12 ? hours - 12 : hours;
return `${display}:${minutes.toString().padStart(2, "0")} ${suffix}`;
}
function formatDuration(event: CalendarEvent) {
const minutes = Math.round((event.end - event.start) * 60);
return minutes >= 60
? `${minutes / 60} hr${minutes > 60 ? "s" : ""}`
: `${minutes} min`;
}
const hours = Array.from(
{ length: dayEnd - dayStart },
(_, index) => dayStart + index,
);
export default function HoverCard12() {
return (
<section
aria-labelledby="hover-card-12-title"
className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground"
>
<header className="mb-3 flex items-baseline justify-between gap-2">
<h3 id="hover-card-12-title" className="text-sm font-medium">
Friday, Sep 25
</h3>
<span className="text-xs text-muted-foreground">3 events</span>
</header>
<div
className="relative mt-2"
style={{ height: `${(dayEnd - dayStart) * hourHeight}rem` }}
>
<ol aria-hidden="true">
{hours.map((hour) => (
<li
key={hour}
className="absolute left-0 w-10 -translate-y-1/2 text-right text-xs text-muted-foreground tabular-nums"
style={{ top: `${(hour - dayStart) * hourHeight}rem` }}
>
{formatTime(hour).replace(":00", "")}
</li>
))}
</ol>
<ul aria-label="Events" className="absolute inset-y-0 right-0 left-12">
{hours.map((hour) => (
<li
key={hour}
aria-hidden="true"
className="absolute inset-x-0 border-t border-dashed"
style={{ top: `${(hour - dayStart) * hourHeight}rem` }}
/>
))}
{events.map((event) => {
const going = event.attendees.filter((a) => a.rsvp === "yes");
const LocationIcon =
event.location.kind === "video" ? VideoIcon : MapPinIcon;
const short = event.end - event.start < 0.5;
return (
<li
key={event.id}
className="absolute inset-x-1 rounded-md bg-card"
style={{
top: `${(event.start - dayStart) * hourHeight}rem`,
height: `${(event.end - event.start) * hourHeight}rem`,
}}
>
<HoverCard>
<HoverCardTrigger
delay={300}
render={
<button
type="button"
className={`flex size-full min-h-0 flex-col overflow-hidden rounded-md border px-2 text-left outline-none hover:brightness-95 focus-visible:ring-2 focus-visible:ring-ring dark:hover:brightness-125 ${event.tone} ${short ? "justify-center" : "py-1"}`}
/>
}
>
<span
className={`truncate font-medium ${short ? "text-[0.7rem] leading-none" : "text-xs"}`}
>
{event.title}
</span>
{!short && (
<span className="truncate text-[0.7rem] text-muted-foreground tabular-nums">
{formatTime(event.start)} – {formatTime(event.end)}
</span>
)}
</HoverCardTrigger>
<HoverCardContent
side="right"
align="start"
className="w-72 max-w-[calc(100vw-2rem)] p-3"
>
<p className="text-sm font-medium">{event.title}</p>
<p className="text-xs text-muted-foreground tabular-nums">
{formatTime(event.start)} – {formatTime(event.end)} ·{" "}
{formatDuration(event)}
</p>
<p className="mt-2 flex items-center gap-1.5 text-xs">
<LocationIcon
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground"
/>
<span className="truncate">{event.location.label}</span>
</p>
<p className="mt-2 text-xs text-muted-foreground">
{event.description}
</p>
<div className="mt-3 border-t pt-2.5">
<p className="mb-2 text-xs text-muted-foreground">
{going.length} of {event.attendees.length} going
</p>
<ul className="grid gap-1.5">
{event.attendees.map((person) => {
const rsvp = rsvpMeta[person.rsvp];
const RsvpIcon = rsvp.icon;
return (
<li
key={person.name}
className="flex items-center gap-2 text-xs"
>
<Avatar size="sm" className="relative">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>
{person.initials}
</AvatarFallback>
</Avatar>
<span className="min-w-0 flex-1 truncate">
{person.name}
</span>
<span className="flex items-center gap-1 text-muted-foreground">
<span
className={`flex size-3.5 items-center justify-center rounded-full ${rsvp.className}`}
>
<RsvpIcon
aria-hidden="true"
className="size-2.5"
strokeWidth={3}
/>
</span>
{rsvp.label}
</span>
</li>
);
})}
</ul>
</div>
</HoverCardContent>
</HoverCard>
</li>
);
})}
</ul>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-12pnpm dlx shadcn@latest add @sevenui/component/hover-card-12yarn dlx shadcn@latest add @sevenui/component/hover-card-12bunx --bun shadcn@latest add @sevenui/component/hover-card-12Top pages
Last 7 days- /pricingUp12.4%18.4K
- /blog/usage-based-billingUp64.1%11.2K
- /docs/quickstartDown3.2%9.9K
- /changelogUp8.9%4.3K
"use client";
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type PageStat = {
path: string;
title: string;
views: number;
change: number;
daily: number[];
avgTime: string;
bounceRate: string;
topReferrer: string;
};
const days = ["Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu"];
const pages: PageStat[] = [
{
path: "/pricing",
title: "Pricing — Plans for every team",
views: 18420,
change: 12.4,
daily: [2310, 1540, 1380, 2890, 3240, 3610, 3450],
avgTime: "1m 48s",
bounceRate: "31%",
topReferrer: "google.com",
},
{
path: "/blog/usage-based-billing",
title: "How we moved to usage-based billing",
views: 11205,
change: 64.1,
daily: [620, 580, 710, 1240, 3980, 2410, 1665],
avgTime: "4m 12s",
bounceRate: "58%",
topReferrer: "news.ycombinator.com",
},
{
path: "/docs/quickstart",
title: "Quickstart — Send your first event",
views: 9876,
change: -3.2,
daily: [1510, 820, 760, 1620, 1740, 1730, 1696],
avgTime: "3m 05s",
bounceRate: "22%",
topReferrer: "github.com",
},
{
path: "/changelog",
title: "Changelog",
views: 4310,
change: 8.9,
daily: [540, 310, 290, 690, 820, 860, 800],
avgTime: "0m 57s",
bounceRate: "44%",
topReferrer: "x.com",
},
];
const numberFormat = new Intl.NumberFormat("en-US");
const compactFormat = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
function DailyBars({ page }: { page: PageStat }) {
const max = Math.max(...page.daily);
const peakIndex = page.daily.indexOf(max);
return (
<figure className="grid gap-1.5">
<figcaption className="flex items-baseline justify-between text-xs">
<span className="text-muted-foreground">Views, last 7 days</span>
<span className="tabular-nums">
Peak {numberFormat.format(max)} on {days[peakIndex]}
</span>
</figcaption>
<ol className="flex h-16 items-end gap-1">
{page.daily.map((value, index) => (
<li
key={days[index]}
className="flex h-full flex-1 flex-col justify-end"
>
<span className="sr-only">
{days[index]}: {numberFormat.format(value)} views
</span>
<span
aria-hidden="true"
className={
index === peakIndex
? "rounded-sm bg-chart-2"
: "rounded-sm bg-chart-2/35"
}
style={{ height: `${Math.max((value / max) * 100, 4)}%` }}
/>
</li>
))}
</ol>
<div
aria-hidden="true"
className="flex gap-1 text-center text-[0.65rem] text-muted-foreground"
>
{days.map((day) => (
<span key={day} className="flex-1">
{day.charAt(0)}
</span>
))}
</div>
</figure>
);
}
export default function HoverCard13() {
const total = pages.reduce((sum, page) => sum + page.views, 0);
return (
<section
aria-labelledby="hover-card-13-title"
className="w-full max-w-sm rounded-xl border bg-card p-4 text-card-foreground"
>
<header className="flex items-baseline justify-between gap-2">
<h3 id="hover-card-13-title" className="text-sm font-medium">
Top pages
</h3>
<span className="text-xs text-muted-foreground">Last 7 days</span>
</header>
<div className="mt-3 flex justify-between border-b pb-2 text-xs text-muted-foreground">
<span>Page</span>
<span>Views</span>
</div>
<ol className="mt-1 grid">
{pages.map((page) => {
const up = page.change >= 0;
const TrendIcon = up ? ArrowUpRightIcon : ArrowDownRightIcon;
const share = (page.views / total) * 100;
return (
<li key={page.path} className="relative py-1">
<span
aria-hidden="true"
className="absolute inset-y-1 left-0 rounded-sm bg-muted"
style={{ width: `${share}%` }}
/>
<div className="relative flex items-center justify-between gap-3 px-2 py-1">
<HoverCard>
<HoverCardTrigger
href={`#page${page.path}`}
delay={350}
className="min-w-0 truncate rounded-sm font-mono text-xs outline-none hover:underline hover:underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring/50"
>
{page.path}
</HoverCardTrigger>
<HoverCardContent
side="left"
align="start"
className="grid w-72 max-w-[calc(100vw-2rem)] gap-3 p-3"
>
<div className="min-w-0">
<p className="truncate text-sm font-medium">
{page.title}
</p>
<p className="truncate font-mono text-xs text-muted-foreground">
acme.dev{page.path}
</p>
</div>
<DailyBars page={page} />
<dl className="grid grid-cols-3 gap-2 border-t pt-2.5 text-xs">
<div className="grid gap-0.5">
<dt className="text-muted-foreground">Avg. time</dt>
<dd className="font-medium tabular-nums">
{page.avgTime}
</dd>
</div>
<div className="grid gap-0.5">
<dt className="text-muted-foreground">Bounce</dt>
<dd className="font-medium tabular-nums">
{page.bounceRate}
</dd>
</div>
<div className="grid min-w-0 gap-0.5">
<dt className="text-muted-foreground">Top source</dt>
<dd className="truncate font-medium">
{page.topReferrer}
</dd>
</div>
</dl>
</HoverCardContent>
</HoverCard>
<span className="flex shrink-0 items-center gap-2 text-xs tabular-nums">
<span
className={`flex items-center ${up ? "text-success" : "text-destructive"}`}
>
<TrendIcon aria-hidden="true" className="size-3" />
<span className="sr-only">{up ? "Up" : "Down"}</span>
{Math.abs(page.change)}%
</span>
<span className="w-10 text-right font-medium">
{compactFormat.format(page.views)}
</span>
</span>
</div>
</li>
);
})}
</ol>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-13pnpm dlx shadcn@latest add @sevenui/component/hover-card-13yarn dlx shadcn@latest add @sevenui/component/hover-card-13bunx --bun shadcn@latest add @sevenui/component/hover-card-13Unassigned
2 new- UnreadIngrid LindqvistFjord FreightWaiting 12m
SSO login loops after IdP certificate rotation
Since this morning nobody on our warehouse team can sign in…
- UnreadKwame MensahBolt BakeryWaiting 38m
Can I print receipts from the iPad app?
We just got a Bluetooth printer and wondered if it works with…
- Neha AroraLumen ClinicsWaiting 2h
Refund still not showing on September invoice
Following up again on the refund you confirmed on the 12th…
"use client";
import { BuildingIcon, MessageCircleIcon, StarIcon } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type Customer = {
id: string;
name: string;
initials: string;
role: string;
company: string;
plan: "Enterprise" | "Growth" | "Starter";
mrr: string;
customerSince: string;
openTickets: number;
csat: number;
tags: string[];
note?: string;
};
type Conversation = {
id: string;
customer: Customer;
subject: string;
preview: string;
waiting: string;
unread: boolean;
};
const conversations: Conversation[] = [
{
id: "c-1",
customer: {
id: "cus-lindqvist",
name: "Ingrid Lindqvist",
initials: "IL",
role: "VP Operations",
company: "Fjord Freight",
plan: "Enterprise",
mrr: "$4,800",
customerSince: "Mar 2023",
openTickets: 3,
csat: 4.9,
tags: ["Renewal in 21 days", "SSO"],
note: "Escalate billing issues to Maya, their account manager.",
},
subject: "SSO login loops after IdP certificate rotation",
preview: "Since this morning nobody on our warehouse team can sign in…",
waiting: "12m",
unread: true,
},
{
id: "c-2",
customer: {
id: "cus-mensah",
name: "Kwame Mensah",
initials: "KM",
role: "Founder",
company: "Bolt Bakery",
plan: "Starter",
mrr: "$29",
customerSince: "Aug 2026",
openTickets: 1,
csat: 4.5,
tags: ["Trial converted"],
},
subject: "Can I print receipts from the iPad app?",
preview: "We just got a Bluetooth printer and wondered if it works with…",
waiting: "38m",
unread: true,
},
{
id: "c-3",
customer: {
id: "cus-arora",
name: "Neha Arora",
initials: "NA",
role: "Finance Lead",
company: "Lumen Clinics",
plan: "Growth",
mrr: "$640",
customerSince: "Nov 2024",
openTickets: 2,
csat: 3.8,
tags: ["At risk", "Invoice dispute"],
note: "Two late-refund complaints this quarter.",
},
subject: "Refund still not showing on September invoice",
preview: "Following up again on the refund you confirmed on the 12th…",
waiting: "2h",
unread: false,
},
];
function CustomerCard({ customer }: { customer: Customer }) {
const atRisk = customer.tags.includes("At risk");
return (
<HoverCard>
<HoverCardTrigger
href={`#customer-${customer.id}`}
delay={300}
className="truncate rounded-sm text-sm font-medium outline-none hover:underline hover:underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring/50"
>
{customer.name}
</HoverCardTrigger>
<HoverCardContent
side="bottom"
align="start"
className="w-80 max-w-[calc(100vw-2rem)] p-0"
>
<div className="flex items-start gap-3 p-3">
<Avatar size="lg">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{customer.initials}</AvatarFallback>
</Avatar>
<div className="grid min-w-0 flex-1 gap-0.5">
<p className="truncate text-sm font-medium">{customer.name}</p>
<p className="truncate text-xs text-muted-foreground">
{customer.role}
</p>
<p className="flex items-center gap-1 text-xs text-muted-foreground">
<BuildingIcon aria-hidden="true" className="size-3 shrink-0" />
<span className="truncate">{customer.company}</span>
</p>
</div>
<Badge
variant={customer.plan === "Enterprise" ? "default" : "secondary"}
>
{customer.plan}
</Badge>
</div>
<dl className="grid grid-cols-3 border-y text-xs">
<div className="grid gap-0.5 px-3 py-2">
<dt className="text-muted-foreground">MRR</dt>
<dd className="font-medium tabular-nums">{customer.mrr}</dd>
</div>
<div className="grid gap-0.5 border-x px-3 py-2">
<dt className="text-muted-foreground">Open</dt>
<dd className="font-medium tabular-nums">
{customer.openTickets}{" "}
{customer.openTickets === 1 ? "ticket" : "tickets"}
</dd>
</div>
<div className="grid gap-0.5 px-3 py-2">
<dt className="text-muted-foreground">CSAT</dt>
<dd
className={`flex items-center gap-1 font-medium tabular-nums ${atRisk ? "text-destructive" : ""}`}
>
<StarIcon aria-hidden="true" className="size-3 fill-current" />
{customer.csat.toFixed(1)}
</dd>
</div>
</dl>
<div className="grid gap-2 p-3">
<div className="flex flex-wrap gap-1">
{customer.tags.map((tag) => (
<Badge
key={tag}
variant={tag === "At risk" ? "destructive" : "outline"}
>
{tag}
</Badge>
))}
</div>
{customer.note && (
<p className="rounded-md bg-muted px-2 py-1.5 text-xs text-muted-foreground">
{customer.note}
</p>
)}
<p className="text-xs text-muted-foreground">
Customer since {customer.customerSince}
</p>
</div>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard14() {
const unreadCount = conversations.filter((c) => c.unread).length;
return (
<section
aria-labelledby="hover-card-14-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-center justify-between gap-2 border-b px-4 py-3">
<h3
id="hover-card-14-title"
className="flex items-center gap-2 text-sm font-medium"
>
<MessageCircleIcon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
Unassigned
</h3>
<Badge variant="secondary">{unreadCount} new</Badge>
</header>
<ul className="divide-y">
{conversations.map((conversation) => (
<li key={conversation.id} className="flex gap-3 px-4 py-3">
<span className="mt-1.5 flex size-2 shrink-0">
{conversation.unread && (
<span className="size-2 rounded-full bg-primary">
<span className="sr-only">Unread</span>
</span>
)}
</span>
<div className="grid min-w-0 flex-1 gap-0.5">
<div className="flex items-baseline justify-between gap-2">
<div className="flex min-w-0 items-baseline gap-1.5">
<CustomerCard customer={conversation.customer} />
<span className="hidden truncate text-xs text-muted-foreground sm:inline">
{conversation.customer.company}
</span>
</div>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
<span className="sr-only">Waiting </span>
{conversation.waiting}
</span>
</div>
<p
className={`truncate text-sm ${conversation.unread ? "font-medium" : "text-muted-foreground"}`}
>
{conversation.subject}
</p>
<p className="truncate text-xs text-muted-foreground">
{conversation.preview}
</p>
</div>
</li>
))}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-14pnpm dlx shadcn@latest add @sevenui/component/hover-card-14yarn dlx shadcn@latest add @sevenui/component/hover-card-14bunx --bun shadcn@latest add @sevenui/component/hover-card-14"use client";
import * as React from "react";
import {
ArrowRightIcon,
CircleCheckIcon,
PackageIcon,
TriangleAlertIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
type Bump = "patch" | "minor" | "major";
type Dependency = {
name: string;
current: string;
latest: string;
bump: Bump;
description: string;
license: string;
weeklyDownloads: string;
size: string;
published: string;
changes: { text: string; breaking?: boolean }[];
};
const dependencies: Dependency[] = [
{
name: "zod",
current: "4.1.8",
latest: "4.1.12",
bump: "patch",
description: "TypeScript-first schema validation with static type inference.",
license: "MIT",
weeklyDownloads: "48.2M",
size: "4.1 MB",
published: "3 days ago",
changes: [
{ text: "Fix discriminated unions with optional keys" },
{ text: "Faster error formatting for deep objects" },
],
},
{
name: "date-fns",
current: "4.0.0",
latest: "4.3.0",
bump: "minor",
description: "Modern JavaScript date utility library.",
license: "MIT",
weeklyDownloads: "31.7M",
size: "21.9 MB",
published: "2 weeks ago",
changes: [
{ text: "Add intervalToDuration rounding option" },
{ text: "New locales: Kazakh, Uzbek (Cyrillic)" },
],
},
{
name: "stripe",
current: "17.7.0",
latest: "19.1.0",
bump: "major",
description: "Stripe API wrapper for Node.js.",
license: "MIT",
weeklyDownloads: "4.6M",
size: "8.3 MB",
published: "6 days ago",
changes: [
{ text: "Pins API version 2026-08-27", breaking: true },
{ text: "Drops Node.js 18 support", breaking: true },
{ text: "Typed webhook event payloads" },
],
},
];
const bumpVariant: Record<Bump, "outline" | "secondary" | "destructive"> = {
patch: "outline",
minor: "secondary",
major: "destructive",
};
function PackageDetails({
dependency,
updated,
}: {
dependency: Dependency;
updated: boolean;
}) {
const breaking = dependency.changes.filter((change) => change.breaking);
return (
<HoverCard>
<HoverCardTrigger
href={`https://www.npmjs.com/package/${dependency.name}`}
target="_blank"
rel="noreferrer"
delay={300}
className="truncate rounded-sm font-mono text-sm font-medium outline-none hover:underline hover:underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring/50"
>
{dependency.name}
</HoverCardTrigger>
<HoverCardContent
align="start"
className="w-80 max-w-[calc(100vw-2rem)] p-0"
>
<div className="grid gap-1 p-3">
<div className="flex items-center justify-between gap-2">
<p className="font-mono text-sm font-medium">{dependency.name}</p>
<Badge variant="outline">{dependency.license}</Badge>
</div>
<p className="text-xs text-muted-foreground">
{dependency.description}
</p>
</div>
<dl className="grid grid-cols-3 border-y text-xs">
<div className="grid gap-0.5 px-3 py-2">
<dt className="text-muted-foreground">Weekly</dt>
<dd className="font-medium tabular-nums">
{dependency.weeklyDownloads}
</dd>
</div>
<div className="grid gap-0.5 border-x px-3 py-2">
<dt className="text-muted-foreground">Unpacked</dt>
<dd className="font-medium tabular-nums">{dependency.size}</dd>
</div>
<div className="grid gap-0.5 px-3 py-2">
<dt className="text-muted-foreground">Published</dt>
<dd className="font-medium">{dependency.published}</dd>
</div>
</dl>
<div className="grid gap-2 p-3">
<p className="text-xs font-medium">
What changed in{" "}
<span className="font-mono">{dependency.latest}</span>
</p>
<ul className="grid gap-1.5 text-xs">
{dependency.changes.map((change) => (
<li key={change.text} className="flex items-start gap-2">
{change.breaking ? (
<TriangleAlertIcon
aria-hidden="true"
className="mt-px size-3.5 shrink-0 text-destructive"
/>
) : (
<span
aria-hidden="true"
className="mt-1.5 size-1 shrink-0 rounded-full bg-muted-foreground"
/>
)}
<span
className={change.breaking ? "text-foreground" : "text-muted-foreground"}
>
{change.breaking && <span className="sr-only">Breaking: </span>}
{change.text}
</span>
</li>
))}
</ul>
<p
className={`mt-1 rounded-md px-2 py-1.5 text-xs ${
updated
? "bg-success/10 text-foreground"
: breaking.length > 0
? "bg-destructive/10 text-foreground"
: "bg-muted text-muted-foreground"
}`}
>
{updated
? `Installed ${dependency.latest}. Run your test suite before merging.`
: breaking.length > 0
? `${breaking.length} breaking changes. Review the migration guide before updating.`
: "Safe to update: no breaking changes reported."}
</p>
</div>
</HoverCardContent>
</HoverCard>
);
}
export default function HoverCard15() {
const [updated, setUpdated] = React.useState<Set<string>>(() => new Set());
const pending = dependencies.filter((dep) => !updated.has(dep.name));
const safe = pending.filter((dep) => dep.bump !== "major");
function update(names: string[]) {
setUpdated((previous) => new Set([...previous, ...names]));
}
return (
<section
aria-labelledby="hover-card-15-title"
className="w-full max-w-lg rounded-xl border bg-card text-card-foreground"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex min-w-0 items-center gap-2.5">
<PackageIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<div className="min-w-0">
<h3 id="hover-card-15-title" className="text-sm font-medium">
Dependency updates
</h3>
<p
className="text-xs text-muted-foreground"
aria-live="polite"
>
{pending.length === 0
? "All packages are up to date"
: `${pending.length} of ${dependencies.length} packages outdated`}
</p>
</div>
</div>
<Button
size="sm"
variant="outline"
disabled={safe.length === 0}
onClick={() => update(safe.map((dep) => dep.name))}
>
{safe.length > 0 ? `Update ${safe.length} safe` : "Safe updates done"}
</Button>
</header>
<ul className="divide-y">
{dependencies.map((dependency) => {
const isUpdated = updated.has(dependency.name);
return (
<li
key={dependency.name}
className="flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-3"
>
<div className="grid min-w-0 flex-1 gap-1">
<div className="flex min-w-0 items-center gap-2">
<PackageDetails dependency={dependency} updated={isUpdated} />
{!isUpdated && (
<Badge variant={bumpVariant[dependency.bump]}>
{dependency.bump}
</Badge>
)}
</div>
<p className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground tabular-nums">
{isUpdated ? (
<span>{dependency.latest}</span>
) : (
<>
<span>{dependency.current}</span>
<ArrowRightIcon aria-hidden="true" className="size-3" />
<span className="sr-only">to</span>
<span className="text-foreground">
{dependency.latest}
</span>
</>
)}
</p>
</div>
{isUpdated ? (
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<CircleCheckIcon
aria-hidden="true"
className="size-4 text-success"
/>
Up to date
</span>
) : (
<Button
size="sm"
variant={
dependency.bump === "major" ? "outline" : "secondary"
}
onClick={() => update([dependency.name])}
>
Update
<span className="sr-only">
{dependency.name} to {dependency.latest}
</span>
</Button>
)}
</li>
);
})}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/hover-card-15pnpm dlx shadcn@latest add @sevenui/component/hover-card-15yarn dlx shadcn@latest add @sevenui/component/hover-card-15bunx --bun shadcn@latest add @sevenui/component/hover-card-15