Item
Free, copy-and-go Item components built on the SevenUI Item primitive.Read the primitive docs.
Get a ZIP of every page, file, and comment. The download link expires after 24 hours.
"use client";
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/components/ui/item";
export default function Item01() {
const [requested, setRequested] = React.useState(false);
return (
<Item variant="outline" className="w-full max-w-md">
<ItemContent className="min-w-48">
<ItemTitle>
Export workspace data
{requested ? (
<span className="inline-flex items-center gap-1 text-xs font-normal text-muted-foreground">
<CheckIcon aria-hidden="true" className="size-3.5 text-success" />
Requested
</span>
) : null}
</ItemTitle>
<ItemDescription aria-live="polite" className="line-clamp-none">
{requested
? "We're preparing your ZIP and will email a download link when it's ready."
: "Get a ZIP of every page, file, and comment. The download link expires after 24 hours."}
</ItemDescription>
</ItemContent>
<ItemActions className="ml-auto">
{requested ? (
<Button size="sm" variant="ghost" onClick={() => setRequested(false)}>
Cancel request
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => setRequested(true)}
>
Request export
</Button>
)}
</ItemActions>
</Item>
);
}
npx shadcn@latest add @sevenui/component/item-01pnpm dlx shadcn@latest add @sevenui/component/item-01yarn dlx shadcn@latest add @sevenui/component/item-01bunx --bun shadcn@latest add @sevenui/component/item-01Shared across every workspace member, with no per-file limit.
Restore any file to an earlier revision in one click.
Every share, download, and permission change is recorded.
"use client";
import { CloudIcon, HistoryIcon, ShieldCheckIcon } from "lucide-react";
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const features = [
{
icon: CloudIcon,
title: "2 TB of encrypted storage",
description:
"Shared across every workspace member, with no per-file limit.",
},
{
icon: HistoryIcon,
title: "180-day version history",
description: "Restore any file to an earlier revision in one click.",
},
{
icon: ShieldCheckIcon,
title: "Admin audit log",
description: "Every share, download, and permission change is recorded.",
},
];
export default function Item02() {
return (
<ItemGroup className="w-full max-w-md gap-2">
{features.map(({ icon: Icon, title, description }) => (
<Item key={title} role="listitem" variant="muted">
<ItemMedia
variant="icon"
className="size-9 rounded-md border border-border bg-background text-foreground"
>
<Icon aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle>{title}</ItemTitle>
<ItemDescription className="line-clamp-none">
{description}
</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>
);
}
npx shadcn@latest add @sevenui/component/item-02pnpm dlx shadcn@latest add @sevenui/component/item-02yarn dlx shadcn@latest add @sevenui/component/item-02bunx --bun shadcn@latest add @sevenui/component/item-02"use client";
import * as React from "react";
import { AudioLinesIcon } from "lucide-react";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const tracks = [
{ title: "Harbor Lights", artist: "The Low Tides", duration: "3:42" },
{ title: "Paper Satellites", artist: "Mara Quinn", duration: "4:05" },
{ title: "Northbound", artist: "Glass Arcade", duration: "2:58" },
{ title: "Slow Weather", artist: "June Alder", duration: "5:11" },
];
export default function Item03() {
const [playing, setPlaying] = React.useState(tracks[1].title);
return (
<ul aria-label="Up next" className="flex w-full max-w-sm flex-col gap-1">
{tracks.map((track) => {
const isPlaying = track.title === playing;
return (
<li key={track.title}>
<Item
size="sm"
variant={isPlaying ? "muted" : "default"}
render={
<button
type="button"
aria-pressed={isPlaying}
aria-label={`Play ${track.title} by ${track.artist}`}
onClick={() => setPlaying(track.title)}
/>
}
className="cursor-pointer text-left hover:bg-muted/60"
>
<ItemMedia variant="image">
<img src="/placeholder.svg" alt="" />
</ItemMedia>
<ItemContent className="min-w-0 gap-0.5">
<ItemTitle>{track.title}</ItemTitle>
<ItemDescription className="line-clamp-1 text-xs">
{track.artist}
</ItemDescription>
</ItemContent>
<ItemActions className="text-xs text-muted-foreground tabular-nums">
{isPlaying ? (
<AudioLinesIcon
aria-hidden="true"
className="size-4 animate-pulse text-primary motion-reduce:animate-none"
/>
) : null}
<span>{track.duration}</span>
</ItemActions>
</Item>
</li>
);
})}
</ul>
);
}
npx shadcn@latest add @sevenui/component/item-03pnpm dlx shadcn@latest add @sevenui/component/item-03yarn dlx shadcn@latest add @sevenui/component/item-03bunx --bun shadcn@latest add @sevenui/component/item-03Editing
"use client";
import * as React from "react";
import {
ArrowUpDownIcon,
CommandIcon,
CornerDownLeftIcon,
SearchIcon,
SlashIcon,
} from "lucide-react";
import {
Item,
ItemActions,
ItemContent,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
const sections = [
{
label: "Navigation",
shortcuts: [
{ icon: SearchIcon, action: "Open search", keys: ["Cmd", "K"] },
{
icon: ArrowUpDownIcon,
action: "Move between results",
keys: ["Up", "Down"],
},
{
icon: CornerDownLeftIcon,
action: "Open selected result",
keys: ["Enter"],
},
],
},
{
label: "Editing",
shortcuts: [
{ icon: SlashIcon, action: "Insert block", keys: ["/"] },
{ icon: CommandIcon, action: "Duplicate block", keys: ["Cmd", "D"] },
],
},
];
export default function Item04() {
const id = React.useId();
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-1.5 text-card-foreground">
{sections.map((section, index) => (
<div key={section.label}>
{index > 0 ? <ItemSeparator className="my-1.5" /> : null}
<p
id={`${id}-${section.label.toLowerCase()}`}
className="px-2.5 pt-1.5 pb-1 text-xs font-medium text-muted-foreground"
>
{section.label}
</p>
<ItemGroup
aria-labelledby={`${id}-${section.label.toLowerCase()}`}
className="gap-0"
>
{section.shortcuts.map(({ icon: Icon, action, keys }) => (
<Item key={action} role="listitem" size="xs">
<ItemMedia variant="icon" className="text-muted-foreground">
<Icon aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle className="font-normal">{action}</ItemTitle>
</ItemContent>
<ItemActions>
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</ItemActions>
</Item>
))}
</ItemGroup>
</div>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/item-04pnpm dlx shadcn@latest add @sevenui/component/item-04yarn dlx shadcn@latest add @sevenui/component/item-04bunx --bun shadcn@latest add @sevenui/component/item-04Went well, to improve, and action items with owners.
Screener, question guide, and a synthesis grid.
"use client";
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Item,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemHeader,
ItemTitle,
} from "@/components/ui/item";
const templates = [
{
name: "Sprint retrospective",
description: "Went well, to improve, and action items with owners.",
uses: "12.4k",
},
{
name: "Customer interview",
description: "Screener, question guide, and a synthesis grid.",
uses: "8.1k",
},
];
export default function Item05() {
const [added, setAdded] = React.useState<string[]>([]);
function toggle(name: string) {
setAdded((current) =>
current.includes(name)
? current.filter((entry) => entry !== name)
: [...current, name],
);
}
return (
<ItemGroup className="grid w-full max-w-lg gap-3 sm:grid-cols-2">
{templates.map((template) => {
const isAdded = added.includes(template.name);
return (
<Item
key={template.name}
role="listitem"
variant="outline"
className="items-stretch gap-3 p-2 pb-3"
>
<ItemHeader>
<img
src="/placeholder.svg"
alt=""
className="aspect-video w-full rounded-md bg-muted object-cover"
/>
</ItemHeader>
<ItemContent className="px-1">
<ItemTitle>{template.name}</ItemTitle>
<ItemDescription>{template.description}</ItemDescription>
</ItemContent>
<ItemFooter className="px-1">
<span className="text-xs text-muted-foreground tabular-nums">
{template.uses} uses
</span>
<Button
size="sm"
variant={isAdded ? "secondary" : "outline"}
aria-pressed={isAdded}
onClick={() => toggle(template.name)}
>
{isAdded ? (
<CheckIcon aria-hidden="true" data-icon="inline-start" />
) : null}
{isAdded ? "Added" : "Use template"}
<span className="sr-only">: {template.name}</span>
</Button>
</ItemFooter>
</Item>
);
})}
</ItemGroup>
);
}
npx shadcn@latest add @sevenui/component/item-05pnpm dlx shadcn@latest add @sevenui/component/item-05yarn dlx shadcn@latest add @sevenui/component/item-05bunx --bun shadcn@latest add @sevenui/component/item-05"use client";
import {
ArrowUpRightIcon,
BellIcon,
ChevronRightIcon,
CreditCardIcon,
UserRoundIcon,
} from "lucide-react";
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item";
const links = [
{
icon: UserRoundIcon,
title: "Profile",
description: "Name, photo, and public handle",
href: "#profile",
},
{
icon: BellIcon,
title: "Notifications",
description: "Email digests and mobile push",
href: "#notifications",
},
{
icon: CreditCardIcon,
title: "Billing",
description: "Plan, invoices, and payment method",
href: "#billing",
},
];
export default function Item06() {
return (
<nav aria-label="Account settings" className="w-full max-w-sm">
<ul className="flex w-full flex-col overflow-hidden rounded-xl border bg-card">
{links.map(({ icon: Icon, title, description, href }) => (
<li key={title}>
<Item
className="rounded-none focus-visible:ring-inset"
render={<a href={href} />}
>
<ItemMedia variant="icon" className="text-muted-foreground">
<Icon aria-hidden="true" />
</ItemMedia>
<ItemContent className="gap-0.5">
<ItemTitle>{title}</ItemTitle>
<ItemDescription>{description}</ItemDescription>
</ItemContent>
<ChevronRightIcon
aria-hidden="true"
className="size-4 text-muted-foreground transition-transform group-hover/item:translate-x-0.5 motion-reduce:transition-none"
/>
</Item>
</li>
))}
<li>
<ItemSeparator className="my-0" />
<Item
size="sm"
className="rounded-none text-muted-foreground focus-visible:ring-inset"
render={<a href="#changelog" />}
>
<ItemContent>
<ItemTitle className="font-normal">What's new</ItemTitle>
</ItemContent>
<ArrowUpRightIcon
aria-hidden="true"
className="size-4 transition-transform group-hover/item:-translate-y-0.5 group-hover/item:translate-x-0.5 motion-reduce:transition-none"
/>
</Item>
</li>
</ul>
</nav>
);
}
npx shadcn@latest add @sevenui/component/item-06pnpm dlx shadcn@latest add @sevenui/component/item-06yarn dlx shadcn@latest add @sevenui/component/item-06bunx --bun shadcn@latest add @sevenui/component/item-06TypeScript · Updated 2 hours ago
Go · Updated yesterday
JSON · Updated 3 days ago
HCL · Updated last week
1 of 4 selected
"use client";
import * as React from "react";
import { GlobeIcon, LockIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const repositories = [
{
id: "storefront",
name: "northwind/storefront",
detail: "TypeScript · Updated 2 hours ago",
isPrivate: true,
},
{
id: "payments",
name: "northwind/payments-service",
detail: "Go · Updated yesterday",
isPrivate: true,
},
{
id: "design-tokens",
name: "northwind/design-tokens",
detail: "JSON · Updated 3 days ago",
isPrivate: false,
},
{
id: "infra",
name: "northwind/infra",
detail: "HCL · Updated last week",
isPrivate: true,
},
];
export default function Item07() {
const id = React.useId();
const [selected, setSelected] = React.useState<string[]>(["storefront"]);
const [imported, setImported] = React.useState<string[]>([]);
const allSelected = selected.length === repositories.length;
const someSelected = selected.length > 0 && !allSelected;
function toggle(id: string, checked: boolean) {
setImported([]);
setSelected((current) =>
checked ? [...current, id] : current.filter((value) => value !== id),
);
}
return (
<div className="w-full max-w-sm overflow-hidden rounded-xl border bg-card text-card-foreground">
<Item size="sm" className="relative rounded-none border-b-border">
<ItemMedia>
<Checkbox
id={`${id}-all`}
className="relative z-10 data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground data-indeterminate:before:h-0.5 data-indeterminate:before:w-2 data-indeterminate:before:rounded-full data-indeterminate:before:bg-current data-indeterminate:[&_svg]:hidden"
checked={allSelected}
indeterminate={someSelected}
onCheckedChange={(checked) => {
setImported([]);
setSelected(
checked ? repositories.map((repository) => repository.id) : [],
);
}}
/>
</ItemMedia>
<ItemContent>
<ItemTitle>
<label
htmlFor={`${id}-all`}
className="cursor-pointer after:absolute after:inset-0"
>
Select all repositories
</label>
</ItemTitle>
</ItemContent>
</Item>
<ItemGroup aria-label="Repositories" className="gap-0.5 p-1.5">
{repositories.map((repository) => {
const isChecked = selected.includes(repository.id);
const VisibilityIcon = repository.isPrivate ? LockIcon : GlobeIcon;
return (
<Item
key={repository.id}
role="listitem"
size="sm"
className="relative hover:bg-muted/50 has-data-checked:bg-muted"
>
<ItemMedia>
<Checkbox
id={`${id}-${repository.id}`}
className="relative z-10"
checked={isChecked}
onCheckedChange={(checked) => toggle(repository.id, checked)}
/>
</ItemMedia>
<ItemContent className="min-w-0 gap-0">
<ItemTitle className="w-full">
<label
htmlFor={`${id}-${repository.id}`}
className="min-w-0 cursor-pointer truncate after:absolute after:inset-0"
>
{repository.name}
</label>
</ItemTitle>
<ItemDescription className="line-clamp-1 text-xs">
{repository.detail}
</ItemDescription>
</ItemContent>
<VisibilityIcon
role="img"
aria-label={repository.isPrivate ? "Private" : "Public"}
className="size-3.5 shrink-0 text-muted-foreground"
/>
</Item>
);
})}
</ItemGroup>
<div className="flex items-center justify-between gap-2 border-t px-3 py-2.5">
<p
aria-live="polite"
className="text-xs text-muted-foreground tabular-nums"
>
{imported.length > 0
? `Imported ${imported.length} ${imported.length === 1 ? "repository" : "repositories"}`
: `${selected.length} of ${repositories.length} selected`}
</p>
<Button
size="sm"
disabled={selected.length === 0}
onClick={() => {
setImported(selected);
setSelected([]);
}}
>
{selected.length > 1
? `Import ${selected.length} repositories`
: "Import repository"}
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/item-07pnpm dlx shadcn@latest add @sevenui/component/item-07yarn dlx shadcn@latest add @sevenui/component/item-07bunx --bun shadcn@latest add @sevenui/component/item-07Released Sep 18, 2026 · 3 changes
- Scheduled exports now run in your workspace time zone.
- Comment threads can be resolved from the inbox.
- Fixed a crash when pasting tables with merged cells.
Released Sep 4, 2026 · 2 changes
"use client";
import { ChevronDownIcon, TagIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const releases = [
{
version: "v2.4.0",
date: "Sep 18, 2026",
latest: true,
changes: [
"Scheduled exports now run in your workspace time zone.",
"Comment threads can be resolved from the inbox.",
"Fixed a crash when pasting tables with merged cells.",
],
},
{
version: "v2.3.2",
date: "Sep 4, 2026",
latest: false,
changes: [
"Faster search indexing for workspaces over 10,000 pages.",
"Fixed avatar uploads failing on Safari.",
],
},
];
export default function Item08() {
return (
<ItemGroup aria-label="Release history" className="w-full max-w-md gap-2">
{releases.map((release) => (
<Collapsible
key={release.version}
defaultOpen={release.latest}
role="listitem"
>
<Item variant="outline" className="has-data-open:bg-muted/40">
<ItemMedia variant="icon" className="text-muted-foreground">
<TagIcon aria-hidden="true" />
</ItemMedia>
<ItemContent className="gap-0.5">
<ItemTitle>
{release.version}
{release.latest ? (
<Badge variant="secondary">Latest</Badge>
) : null}
</ItemTitle>
<ItemDescription className="text-xs">
Released {release.date} · {release.changes.length}{" "}
changes
</ItemDescription>
</ItemContent>
<ItemActions>
<CollapsibleTrigger
render={
<Button
size="icon-sm"
variant="ghost"
aria-label={`Toggle ${release.version} changes`}
className="group/trigger"
/>
}
>
<ChevronDownIcon
aria-hidden="true"
className="transition-transform duration-200 group-data-panel-open/trigger:rotate-180 motion-reduce:transition-none"
/>
</CollapsibleTrigger>
</ItemActions>
<CollapsibleContent className="basis-full">
<ul className="mt-1 ml-6.5 flex list-disc flex-col gap-1.5 border-t pt-3 pl-4 text-sm text-muted-foreground marker:text-muted-foreground/60">
{release.changes.map((change) => (
<li key={change}>{change}</li>
))}
</ul>
</CollapsibleContent>
</Item>
</Collapsible>
))}
</ItemGroup>
);
}
npx shadcn@latest add @sevenui/component/item-08pnpm dlx shadcn@latest add @sevenui/component/item-08yarn dlx shadcn@latest add @sevenui/component/item-08bunx --bun shadcn@latest add @sevenui/component/item-08Post deploy summaries to a channel.
Link pull requests to tasks automatically.
Sync issue status in both directions.
Available on the Business plan.
"use client";
import * as React from "react";
import {
CircleAlertIcon,
CircleCheckIcon,
GitBranchIcon,
KanbanIcon,
LockIcon,
MessageSquareIcon,
PenToolIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { Spinner } from "@/components/ui/spinner";
type Status = "idle" | "connecting" | "connected" | "error" | "locked";
const integrations = [
{
id: "chat",
icon: MessageSquareIcon,
name: "Team chat",
description: "Post deploy summaries to a channel.",
initial: "connected" as Status,
failsOnce: false,
},
{
id: "git",
icon: GitBranchIcon,
name: "Git provider",
description: "Link pull requests to tasks automatically.",
initial: "idle" as Status,
failsOnce: false,
},
{
id: "tracker",
icon: KanbanIcon,
name: "Issue tracker",
description: "Sync issue status in both directions.",
initial: "idle" as Status,
failsOnce: true,
},
{
id: "design",
icon: PenToolIcon,
name: "Design files",
description: "Embed live frames in any page.",
initial: "locked" as Status,
failsOnce: false,
},
];
const descriptions: Partial<Record<Status, string>> = {
connecting: "Waiting for authorization…",
error: "Authorization timed out. Check pop-up blockers and try again.",
locked: "Available on the Business plan.",
};
export default function Item09() {
const [status, setStatus] = React.useState<Record<string, Status>>(() =>
Object.fromEntries(integrations.map((entry) => [entry.id, entry.initial])),
);
const failed = React.useRef(new Set<string>());
const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]);
React.useEffect(() => {
const pending = timers.current;
return () => pending.forEach(clearTimeout);
}, []);
function connect(id: string, failsOnce: boolean) {
setStatus((current) => ({ ...current, [id]: "connecting" }));
const shouldFail = failsOnce && !failed.current.has(id);
timers.current.push(
setTimeout(() => {
if (shouldFail) failed.current.add(id);
setStatus((current) => ({
...current,
[id]: shouldFail ? "error" : "connected",
}));
}, 1400),
);
}
function disconnect(id: string) {
setStatus((current) => ({ ...current, [id]: "idle" }));
}
// Stands in for the upgrade flow: once the plan allows it, the
// integration becomes connectable.
function unlock(id: string) {
setStatus((current) => ({ ...current, [id]: "idle" }));
}
return (
<ItemGroup aria-label="Integrations" className="w-full max-w-md gap-2">
{integrations.map(({ id, icon: Icon, name, description, failsOnce }) => {
const state = status[id];
return (
<Item
key={id}
role="listitem"
variant="outline"
aria-busy={state === "connecting"}
data-status={state}
className="data-[status=error]:border-destructive/40 data-[status=error]:bg-destructive/5 data-[status=locked]:bg-muted/40"
>
<ItemMedia
variant="icon"
className="size-9 rounded-md bg-muted text-foreground in-data-[status=locked]:text-muted-foreground"
>
<Icon aria-hidden="true" />
</ItemMedia>
<ItemContent className="min-w-40 gap-0.5">
<ItemTitle>
{name}
{state === "connected" ? (
<CircleCheckIcon
role="img"
aria-label="Connected"
className="size-3.5 text-success"
/>
) : null}
</ItemTitle>
<ItemDescription
aria-live="polite"
className="flex items-start gap-1.5 in-data-[status=error]:text-destructive"
>
{state === "error" ? (
<CircleAlertIcon
aria-hidden="true"
className="mt-0.5 size-3.5 shrink-0"
/>
) : null}
<span>{descriptions[state] ?? description}</span>
</ItemDescription>
</ItemContent>
<ItemActions className="ml-auto">
{state === "idle" ? (
<Button size="sm" onClick={() => connect(id, failsOnce)}>
Connect
</Button>
) : null}
{state === "connecting" ? (
<Button size="sm" variant="outline" disabled>
<Spinner aria-hidden="true" />
Connecting
</Button>
) : null}
{state === "connected" ? (
<Button
size="sm"
variant="ghost"
onClick={() => disconnect(id)}
>
Disconnect
</Button>
) : null}
{state === "error" ? (
<Button
size="sm"
variant="outline"
onClick={() => connect(id, failsOnce)}
>
Try again
</Button>
) : null}
{state === "locked" ? (
<Button size="sm" variant="outline" onClick={() => unlock(id)}>
<LockIcon aria-hidden="true" />
Upgrade
</Button>
) : null}
</ItemActions>
</Item>
);
})}
</ItemGroup>
);
}
npx shadcn@latest add @sevenui/component/item-09pnpm dlx shadcn@latest add @sevenui/component/item-09yarn dlx shadcn@latest add @sevenui/component/item-09bunx --bun shadcn@latest add @sevenui/component/item-09Pinned projects
The order here is the order in your sidebar.
12 open tasks · Due Oct 3
31 open tasks · Due Nov 14
7 open tasks · Due Oct 20
4 open tasks · No due date
"use client";
import * as React from "react";
import { ArrowDownIcon, ArrowUpIcon, PinOffIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const INITIAL_PROJECTS = [
{
id: "checkout",
name: "Checkout redesign",
detail: "12 open tasks · Due Oct 3",
tone: "bg-chart-1",
},
{
id: "mobile",
name: "Mobile app 3.0",
detail: "31 open tasks · Due Nov 14",
tone: "bg-chart-2",
},
{
id: "billing",
name: "Billing API migration",
detail: "7 open tasks · Due Oct 20",
tone: "bg-chart-3",
},
{
id: "docs",
name: "Help center refresh",
detail: "4 open tasks · No due date",
tone: "bg-chart-4",
},
];
type Project = (typeof INITIAL_PROJECTS)[number];
export default function Item10() {
const headingId = React.useId();
const [projects, setProjects] = React.useState<Project[]>(INITIAL_PROJECTS);
const [announcement, setAnnouncement] = React.useState("");
function move(index: number, offset: -1 | 1) {
const target = index + offset;
const project = projects[index];
const next = [...projects];
next.splice(index, 1);
next.splice(target, 0, project);
setProjects(next);
setAnnouncement(
`${project.name} moved to position ${target + 1} of ${next.length}.`,
);
}
function unpin(project: Project) {
setProjects((prev) => prev.filter((entry) => entry.id !== project.id));
setAnnouncement(`${project.name} unpinned from the sidebar.`);
}
return (
<section aria-labelledby={headingId} className="w-full max-w-md">
<div className="mb-3 flex items-end justify-between gap-3 px-1">
<div className="space-y-0.5">
<h3 id={headingId} className="text-sm font-semibold">
Pinned projects
</h3>
<p className="text-sm text-muted-foreground">
The order here is the order in your sidebar.
</p>
</div>
{projects.length < INITIAL_PROJECTS.length ? (
<Button
size="xs"
variant="ghost"
onClick={() => {
setProjects(INITIAL_PROJECTS);
setAnnouncement("Pinned projects reset.");
}}
>
Reset
</Button>
) : null}
</div>
{projects.length === 0 ? (
<p className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
Nothing pinned. Pin a project from its menu to keep it one click away.
</p>
) : (
<ItemGroup
aria-labelledby={headingId}
className="gap-0 overflow-hidden rounded-xl border bg-card text-card-foreground"
>
{projects.map((project, index) => (
<Item
key={project.id}
role="listitem"
size="sm"
className="rounded-none border-b-border pr-2 last:border-b-transparent hover:bg-muted/40"
>
<ItemMedia className="size-6 rounded-md bg-muted text-xs font-medium text-muted-foreground tabular-nums">
{index + 1}
</ItemMedia>
<ItemContent className="min-w-0 gap-0.5">
<ItemTitle className="w-full max-sm:items-start">
<span
aria-hidden="true"
className={`size-2 shrink-0 rounded-full max-sm:mt-1.5 ${project.tone}`}
/>
<span className="min-w-0 sm:truncate">{project.name}</span>
</ItemTitle>
<ItemDescription className="pl-4 text-xs sm:truncate">
{project.detail}
</ItemDescription>
</ItemContent>
<ItemActions className="gap-0 sm:gap-0.5">
<Button
size="icon-sm"
variant="ghost"
className="max-sm:size-7"
aria-label={`Move ${project.name} up`}
disabled={index === 0}
onClick={() => move(index, -1)}
>
<ArrowUpIcon aria-hidden="true" />
</Button>
<Button
size="icon-sm"
variant="ghost"
className="max-sm:size-7"
aria-label={`Move ${project.name} down`}
disabled={index === projects.length - 1}
onClick={() => move(index, 1)}
>
<ArrowDownIcon aria-hidden="true" />
</Button>
<Button
size="icon-sm"
variant="ghost"
aria-label={`Unpin ${project.name}`}
className="text-muted-foreground hover:text-destructive max-sm:size-7"
onClick={() => unpin(project)}
>
<PinOffIcon aria-hidden="true" />
</Button>
</ItemActions>
</Item>
))}
</ItemGroup>
)}
<p aria-live="polite" className="sr-only">
{announcement}
</p>
</section>
);
}
npx shadcn@latest add @sevenui/component/item-10pnpm dlx shadcn@latest add @sevenui/component/item-10yarn dlx shadcn@latest add @sevenui/component/item-10bunx --bun shadcn@latest add @sevenui/component/item-10Payment method
Your Team plan renews on October 14 for $96.00.
Expires 08/2028
Expires 10/2026 · Expires soon
ACH debit · First Harbor Bank
"use client";
import * as React from "react";
import { CreditCardIcon, LandmarkIcon, PlusIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const METHODS = [
{
id: "visa-4242",
title: "Visa ending in 4242",
detail: "Expires 08/2028",
icon: CreditCardIcon,
expiring: false,
},
{
id: "mastercard-8210",
title: "Mastercard ending in 8210",
detail: "Expires 10/2026",
icon: CreditCardIcon,
expiring: true,
},
{
id: "ach-6019",
title: "Business checking ••6019",
detail: "ACH debit · First Harbor Bank",
icon: LandmarkIcon,
expiring: false,
},
];
const NEW_METHOD = {
id: "amex-1009",
title: "Amex ending in 1009",
detail: "Expires 04/2030",
icon: CreditCardIcon,
expiring: false,
};
export default function Item11() {
const headingId = React.useId();
const [defaultMethod, setDefaultMethod] = React.useState<string>("visa-4242");
const [added, setAdded] = React.useState(false);
const methods = added ? [...METHODS, NEW_METHOD] : METHODS;
function removeNewMethod() {
setAdded(false);
if (defaultMethod === NEW_METHOD.id) setDefaultMethod(METHODS[0].id);
}
return (
<div className="w-full max-w-md space-y-3">
<div className="space-y-1">
<h3 id={headingId} className="text-base font-semibold">
Payment method
</h3>
<p className="text-sm text-muted-foreground">
Your Team plan renews on October 14 for $96.00.
</p>
</div>
<RadioGroup
aria-labelledby={headingId}
value={defaultMethod}
onValueChange={(value) => setDefaultMethod(value as string)}
className="gap-2"
>
{methods.map((method) => {
const Icon = method.icon;
const isDefault = defaultMethod === method.id;
return (
<Item
key={method.id}
variant="outline"
className="relative hover:bg-muted/50 has-data-checked:border-primary has-data-checked:bg-primary/5 has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50"
>
<ItemMedia className="h-7 w-10 rounded-md border bg-background text-muted-foreground">
<Icon aria-hidden="true" className="size-4" />
</ItemMedia>
<ItemContent className="min-w-0">
<ItemTitle>
<label
htmlFor={`${headingId}-${method.id}`}
className="cursor-pointer after:absolute after:inset-0"
>
{method.title}
</label>
{isDefault && (
<Badge variant="outline" className="bg-background">
Default
</Badge>
)}
</ItemTitle>
<ItemDescription>
{method.detail}
{method.expiring && (
<span className="font-medium text-foreground">
{" "}
· Expires soon
</span>
)}
</ItemDescription>
</ItemContent>
<ItemActions>
{method.id === NEW_METHOD.id ? (
<Button
size="xs"
variant="ghost"
className="relative z-10 text-muted-foreground"
onClick={removeNewMethod}
>
Remove
<span className="sr-only"> {method.title}</span>
</Button>
) : null}
<RadioGroupItem
className="z-10"
id={`${headingId}-${method.id}`}
value={method.id}
/>
</ItemActions>
</Item>
);
})}
</RadioGroup>
{added ? null : (
<Item
variant="outline"
size="sm"
render={<button type="button" onClick={() => setAdded(true)} />}
className="cursor-pointer justify-center border-dashed text-muted-foreground hover:bg-muted/50 hover:text-foreground"
>
<PlusIcon aria-hidden="true" className="size-4" />
Add payment method
</Item>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/item-11pnpm dlx shadcn@latest add @sevenui/component/item-11yarn dlx shadcn@latest add @sevenui/component/item-11bunx --bun shadcn@latest add @sevenui/component/item-11Recent files
4 files2.4 MB · Edited by Priya 12 min ago
18.1 MB · Edited by you yesterday
5.7 MB · Edited by Marco on Sep 21
142 MB · Uploaded by Lena on Sep 18
"use client";
import * as React from "react";
import {
DownloadIcon,
FileSpreadsheetIcon,
FileTextIcon,
FileVideoIcon,
FolderOpenIcon,
ImageIcon,
LinkIcon,
MoreHorizontalIcon,
StarIcon,
Trash2Icon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
const FILES = [
{
id: "q3-forecast",
name: "Q3 revenue forecast.xlsx",
meta: "2.4 MB · Edited by Priya 12 min ago",
icon: FileSpreadsheetIcon,
tone: "bg-chart-2/15 text-chart-2",
},
{
id: "brand-guide",
name: "Brand guidelines v4.pdf",
meta: "18.1 MB · Edited by you yesterday",
icon: FileTextIcon,
tone: "bg-chart-1/15 text-chart-1",
},
{
id: "hero-shot",
name: "Homepage hero — final.png",
meta: "5.7 MB · Edited by Marco on Sep 21",
icon: ImageIcon,
tone: "bg-chart-4/15 text-chart-4",
},
{
id: "onboarding-walkthrough",
name: "Onboarding walkthrough.mp4",
meta: "142 MB · Uploaded by Lena on Sep 18",
icon: FileVideoIcon,
tone: "bg-chart-5/15 text-chart-5",
},
];
type DriveFile = (typeof FILES)[number];
export default function Item12() {
const [files, setFiles] = React.useState<DriveFile[]>(FILES);
const [starred, setStarred] = React.useState<string[]>(["q3-forecast"]);
const [lastTrashed, setLastTrashed] = React.useState<{
file: DriveFile;
index: number;
} | null>(null);
const [notice, setNotice] = React.useState<string | null>(null);
function toggleStar(id: string) {
setStarred((prev) =>
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
);
}
function trash(file: DriveFile) {
const index = files.findIndex((f) => f.id === file.id);
setFiles((prev) => prev.filter((f) => f.id !== file.id));
setLastTrashed({ file, index });
setNotice(null);
}
async function copyLink(file: DriveFile) {
const link = `${window.location.origin}/files/${file.id}`;
try {
await navigator.clipboard.writeText(link);
setNotice("Link copied to clipboard");
} catch {
setNotice("Couldn't copy the link. Check clipboard permissions.");
}
setLastTrashed(null);
}
function download(file: DriveFile) {
setNotice(`Downloading “${file.name}”…`);
setLastTrashed(null);
}
function restore() {
if (!lastTrashed) return;
setFiles((prev) => {
const next = [...prev];
next.splice(lastTrashed.index, 0, lastTrashed.file);
return next;
});
setLastTrashed(null);
}
return (
<div className="w-full max-w-lg space-y-2">
<div className="flex items-baseline justify-between gap-2 px-1">
<h3 className="text-sm font-semibold">Recent files</h3>
<span className="text-xs text-muted-foreground tabular-nums">
{files.length} {files.length === 1 ? "file" : "files"}
</span>
</div>
{files.length === 0 ? (
<div className="flex flex-col items-center gap-2 rounded-lg border border-dashed px-4 py-10 text-center">
<FolderOpenIcon
aria-hidden="true"
className="size-6 text-muted-foreground"
/>
<p className="text-sm font-medium">No recent files</p>
<p className="text-sm text-muted-foreground">
Files you open or edit will show up here.
</p>
</div>
) : (
<ItemGroup className="gap-1">
{files.map((file) => {
const Icon = file.icon;
const isStarred = starred.includes(file.id);
return (
<Item
key={file.id}
role="listitem"
size="sm"
className="hover:bg-muted/50"
>
<ItemMedia
className={`size-9 rounded-md [&_svg]:size-4.5 ${file.tone}`}
>
<Icon aria-hidden="true" />
</ItemMedia>
<ItemContent className="min-w-0">
<ItemTitle className="w-full">
<span className="min-w-0 truncate">{file.name}</span>
{isStarred && (
<StarIcon
role="img"
aria-label="Starred"
className="size-3.5 shrink-0 fill-warning text-warning"
/>
)}
</ItemTitle>
<ItemDescription className="truncate">
{file.meta}
</ItemDescription>
</ItemContent>
<ItemActions>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon"
aria-label={`Actions for ${file.name}`}
>
<MoreHorizontalIcon aria-hidden="true" />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem onClick={() => toggleStar(file.id)}>
<StarIcon aria-hidden="true" />
{isStarred ? "Remove star" : "Add star"}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyLink(file)}>
<LinkIcon aria-hidden="true" />
Copy link
</DropdownMenuItem>
<DropdownMenuItem onClick={() => download(file)}>
<DownloadIcon aria-hidden="true" />
Download
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => trash(file)}
>
<Trash2Icon aria-hidden="true" />
Move to trash
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ItemActions>
</Item>
);
})}
</ItemGroup>
)}
<div aria-live="polite" className="min-h-9">
{lastTrashed && (
<div className="flex items-center justify-between gap-2 rounded-lg bg-muted px-3 py-1.5 text-sm">
<span className="min-w-0 truncate">
Moved “{lastTrashed.file.name}” to trash
</span>
<Button size="sm" variant="ghost" onClick={restore}>
Undo
</Button>
</div>
)}
{notice && !lastTrashed && (
<div className="flex items-center justify-between gap-2 rounded-lg bg-muted px-3 py-1.5 text-sm">
<span className="min-w-0 truncate">{notice}</span>
<Button size="sm" variant="ghost" onClick={() => setNotice(null)}>
Dismiss
</Button>
</div>
)}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/item-12pnpm dlx shadcn@latest add @sevenui/component/item-12yarn dlx shadcn@latest add @sevenui/component/item-12bunx --bun shadcn@latest add @sevenui/component/item-12amara.okafor@lumen.studio
theo.lindqvist@lumen.studio
Invitation sent · expires in 7 days
"use client";
import * as React from "react";
import { ChevronDownIcon, MailIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item";
const ROLES = [
{ value: "admin", label: "Admin" },
{ value: "editor", label: "Editor" },
{ value: "viewer", label: "Viewer" },
] as const;
type Role = (typeof ROLES)[number]["value"];
type Member = {
email: string;
name: string | null;
initials: string;
role: Role;
isYou?: boolean;
};
const INITIAL_MEMBERS: Member[] = [
{
email: "amara.okafor@lumen.studio",
name: "Amara Okafor",
initials: "AO",
role: "admin",
isYou: true,
},
{
email: "theo.lindqvist@lumen.studio",
name: "Theo Lindqvist",
initials: "TL",
role: "editor",
},
{
email: "rosa.mendez@lumen.studio",
name: null,
initials: "RM",
role: "viewer",
},
];
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function Item13() {
const id = React.useId();
const [members, setMembers] = React.useState<Member[]>(INITIAL_MEMBERS);
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState<string | null>(null);
function invite(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!EMAIL_PATTERN.test(value)) {
setError("Enter a valid email address, like sam@company.com.");
return;
}
if (members.some((m) => m.email === value)) {
setError("That person is already on the team or invited.");
return;
}
setMembers((prev) => [
...prev,
{
email: value,
name: null,
initials: value.slice(0, 2).toUpperCase(),
role: "editor",
},
]);
setEmail("");
setError(null);
}
function setRole(target: string, role: Role) {
setMembers((prev) =>
prev.map((m) => (m.email === target ? { ...m, role } : m)),
);
}
function remove(target: string) {
setMembers((prev) => prev.filter((m) => m.email !== target));
}
return (
<div className="w-full max-w-lg rounded-xl border bg-card text-card-foreground">
<form onSubmit={invite} noValidate className="space-y-2 border-b p-4">
<label htmlFor={`${id}-email`} className="text-sm font-medium">
Invite teammates
</label>
<div className="flex gap-2">
<Input
id={`${id}-email`}
type="email"
placeholder="name@lumen.studio"
value={email}
onChange={(event) => {
setEmail(event.target.value);
if (error) setError(null);
}}
aria-invalid={error ? true : undefined}
aria-describedby={error ? `${id}-error` : undefined}
/>
<Button type="submit">Invite</Button>
</div>
{error && (
<p id={`${id}-error`} className="text-sm text-destructive">
{error}
</p>
)}
</form>
<ItemGroup className="gap-0 p-1.5" aria-label="Team members">
{members.map((member, index) => {
const pending = member.name === null;
const roleLabel =
ROLES.find((r) => r.value === member.role)?.label ?? "";
return (
<React.Fragment key={member.email}>
{index > 0 && <ItemSeparator className="mx-3 my-0 data-[orientation=horizontal]:w-auto" />}
<Item role="listitem" size="sm">
<ItemMedia>
{pending ? (
<span className="flex size-8 items-center justify-center rounded-full border border-dashed text-muted-foreground">
<MailIcon aria-hidden="true" className="size-3.5" />
</span>
) : (
<Avatar>
<AvatarFallback>{member.initials}</AvatarFallback>
</Avatar>
)}
</ItemMedia>
<ItemContent className="min-w-0">
<ItemTitle className="w-full flex-wrap gap-y-1">
<span className="min-w-0 truncate">
{member.name ?? member.email}
</span>
{member.isYou && (
<span className="shrink-0 text-muted-foreground">(you)</span>
)}
{pending && <Badge variant="outline">Invited</Badge>}
</ItemTitle>
<ItemDescription className="truncate">
{pending
? "Invitation sent · expires in 7 days"
: member.email}
</ItemDescription>
</ItemContent>
<ItemActions>
{member.isYou ? (
<span className="px-2.5 text-sm text-muted-foreground">
{roleLabel}
</span>
) : (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="sm"
aria-label={`Role for ${member.name ?? member.email}: ${roleLabel}`}
>
{roleLabel}
<ChevronDownIcon
aria-hidden="true"
data-icon="inline-end"
/>
</Button>
}
/>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuRadioGroup
value={member.role}
onValueChange={(value) =>
setRole(member.email, value as Role)
}
>
{ROLES.map((role) => (
<DropdownMenuRadioItem
key={role.value}
value={role.value}
closeOnClick
>
{role.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => remove(member.email)}
>
{pending ? "Revoke invite" : "Remove from team"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</ItemActions>
</Item>
</React.Fragment>
);
})}
</ItemGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/item-13pnpm dlx shadcn@latest add @sevenui/component/item-13yarn dlx shadcn@latest add @sevenui/component/item-13bunx --bun shadcn@latest add @sevenui/component/item-13About 18 minutes left
Add two DNS records so emails arrive from news@yourbrand.com instead of our shared domain.
"use client";
import * as React from "react";
import { CheckIcon, PartyPopperIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@/components/ui/progress";
const STEPS = [
{
id: "workspace",
title: "Create your workspace",
description: "Name it and pick the region where your data is stored.",
action: "Create workspace",
minutes: 1,
},
{
id: "domain",
title: "Verify your sending domain",
description:
"Add two DNS records so emails arrive from news@yourbrand.com instead of our shared domain.",
action: "Add DNS records",
minutes: 5,
},
{
id: "audience",
title: "Import your audience",
description: "Upload a CSV or sync contacts from your CRM.",
action: "Import contacts",
minutes: 3,
},
{
id: "campaign",
title: "Send your first campaign",
description: "Start from a template and send a test to yourself first.",
action: "Open editor",
minutes: 10,
},
] as const;
type StepId = (typeof STEPS)[number]["id"];
export default function Item14() {
const [done, setDone] = React.useState<StepId[]>(["workspace"]);
const currentIndex = STEPS.findIndex((step) => !done.includes(step.id));
const percent = Math.round((done.length / STEPS.length) * 100);
const remainingMinutes = STEPS.filter((s) => !done.includes(s.id)).reduce(
(sum, s) => sum + s.minutes,
0,
);
return (
<div className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground">
<Progress value={percent} className="mb-4 gap-2">
<ProgressLabel>Get ready to send</ProgressLabel>
<ProgressValue className="ml-auto text-sm text-muted-foreground tabular-nums" />
</Progress>
{currentIndex === -1 ? (
<div className="flex flex-col items-center gap-2 py-6 text-center">
<PartyPopperIcon aria-hidden="true" className="size-6 text-primary" />
<p className="text-sm font-medium">You're all set</p>
<p className="text-sm text-muted-foreground">
Your first campaign is on its way.
</p>
<Button
size="sm"
variant="outline"
className="mt-2"
onClick={() => setDone(["workspace"])}
>
Reset checklist
</Button>
</div>
) : (
<>
<p className="mb-2 text-sm text-muted-foreground">
About {remainingMinutes} minutes left
</p>
<ItemGroup className="gap-1" aria-label="Setup steps">
{STEPS.map((step, index) => {
const isDone = done.includes(step.id);
const isCurrent = index === currentIndex;
return (
<Item
key={step.id}
role="listitem"
variant={isCurrent ? "outline" : "default"}
aria-current={isCurrent ? "step" : undefined}
className={isCurrent ? "bg-muted/40" : undefined}
>
<ItemMedia>
<span
className={
isDone
? "flex size-6 items-center justify-center rounded-full bg-primary text-primary-foreground"
: isCurrent
? "flex size-6 items-center justify-center rounded-full border-2 border-primary text-xs font-semibold text-primary tabular-nums"
: "flex size-6 items-center justify-center rounded-full border text-xs text-muted-foreground tabular-nums"
}
>
{isDone ? (
<CheckIcon aria-hidden="true" className="size-3.5" />
) : (
index + 1
)}
</span>
</ItemMedia>
<ItemContent>
<ItemTitle
className={
isDone
? "text-muted-foreground line-through decoration-muted-foreground/60"
: undefined
}
>
{step.title}
<span className="sr-only">
{isDone ? "(completed)" : ""}
</span>
</ItemTitle>
{isCurrent && (
<ItemDescription className="line-clamp-none">
{step.description}
</ItemDescription>
)}
</ItemContent>
{isCurrent ? (
<ItemActions className="basis-full pl-8.5 sm:basis-auto sm:pl-0">
<Button
size="sm"
onClick={() => setDone((prev) => [...prev, step.id])}
>
{step.action}
</Button>
</ItemActions>
) : (
!isDone && (
<span className="text-xs text-muted-foreground tabular-nums">
{step.minutes} min
</span>
)
)}
</Item>
);
})}
</ItemGroup>
</>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/item-14pnpm dlx shadcn@latest add @sevenui/component/item-14yarn dlx shadcn@latest add @sevenui/component/item-14bunx --bun shadcn@latest add @sevenui/component/item-14Today
Thursday, Sep 25
Video call
Video call
Room 4B · Harbor floor
Cafe corner
"use client";
import * as React from "react";
import { CheckIcon, MapPinIcon, VideoIcon, XIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
type Status = "past" | "live" | "invite" | "upcoming";
type Response = "accepted" | "declined";
const EVENTS: {
id: string;
start: string;
end: string;
title: string;
where: string;
remote: boolean;
status: Status;
guests: string[];
}[] = [
{
id: "standup",
start: "9:30",
end: "9:45",
title: "Checkout squad standup",
where: "Video call",
remote: true,
status: "past",
guests: ["MK", "JP", "SL"],
},
{
id: "review",
start: "11:00",
end: "11:45",
title: "Pricing page design review",
where: "Video call",
remote: true,
status: "live",
guests: ["AO", "TL", "RM", "DS", "EW"],
},
{
id: "interview",
start: "14:00",
end: "15:00",
title: "Interview: Senior iOS engineer",
where: "Room 4B · Harbor floor",
remote: false,
status: "invite",
guests: ["NB", "OH"],
},
{
id: "one-on-one",
start: "16:30",
end: "17:00",
title: "1:1 with Priya",
where: "Cafe corner",
remote: false,
status: "upcoming",
guests: ["PR"],
},
];
export default function Item15() {
const headingId = React.useId();
const [joined, setJoined] = React.useState(false);
const [responses, setResponses] = React.useState<Record<string, Response>>(
{},
);
function respond(id: string, response: Response | null) {
setResponses((prev) => {
const next = { ...prev };
if (response) next[id] = response;
else delete next[id];
return next;
});
}
return (
<section
aria-labelledby={headingId}
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={headingId} className="text-sm font-semibold">
Today
</h3>
<p className="text-sm text-muted-foreground">Thursday, Sep 25</p>
</header>
<ItemGroup aria-labelledby={headingId} className="gap-1 p-1.5">
{EVENTS.map((event) => {
const response = responses[event.id];
const visibleGuests = event.guests.slice(0, 3);
const extraGuests = event.guests.length - visibleGuests.length;
const PlaceIcon = event.remote ? VideoIcon : MapPinIcon;
return (
<Item
key={event.id}
role="listitem"
data-status={event.status}
data-response={response}
className="items-start gap-3 data-[response=declined]:opacity-60 data-[status=live]:bg-muted/60 data-[status=past]:opacity-60"
>
<ItemMedia className="w-11 flex-col items-end gap-0 text-right tabular-nums">
<span className="text-sm font-medium">{event.start}</span>
<span className="text-xs text-muted-foreground">
{event.end}
</span>
</ItemMedia>
<div
aria-hidden="true"
className="w-0.5 self-stretch rounded-full bg-border in-data-[status=invite]:bg-chart-4 in-data-[status=live]:bg-primary"
/>
<ItemContent className="min-w-0">
<ItemTitle className="w-full flex-wrap gap-y-1">
<span className="min-w-0 in-data-[response=declined]:line-through sm:truncate">
{event.title}
</span>
{event.status === "live" && (
<Badge className="gap-1.5">
<span
aria-hidden="true"
className="size-1.5 animate-pulse rounded-full bg-primary-foreground motion-reduce:animate-none"
/>
Now
</Badge>
)}
</ItemTitle>
<ItemDescription className="flex items-center gap-1.5 text-xs">
<PlaceIcon aria-hidden="true" className="size-3.5 shrink-0" />
<span className="truncate">{event.where}</span>
</ItemDescription>
{event.status === "invite" && !response && (
<fieldset className="mt-1.5 flex min-w-0 flex-wrap gap-1.5">
<legend className="sr-only">
Respond to {event.title}
</legend>
<Button
size="xs"
onClick={() => respond(event.id, "accepted")}
>
<CheckIcon aria-hidden="true" data-icon="inline-start" />
Accept
</Button>
<Button
size="xs"
variant="outline"
onClick={() => respond(event.id, "declined")}
>
<XIcon aria-hidden="true" data-icon="inline-start" />
Decline
</Button>
</fieldset>
)}
{response && (
<p
aria-live="polite"
className="mt-1 flex items-center gap-2 text-xs text-muted-foreground"
>
{response === "accepted" ? "You're going" : "You declined"}
<Button
size="xs"
variant="link"
className="h-auto p-0 text-xs"
onClick={() => respond(event.id, null)}
>
Change
</Button>
</p>
)}
</ItemContent>
<ItemActions className="flex-col items-end gap-2">
<AvatarGroup
role="group"
className="-space-x-0.5 max-sm:hidden"
aria-label={`${event.guests.length} ${event.guests.length === 1 ? "guest" : "guests"}`}
>
{visibleGuests.map((initials) => (
<Avatar key={initials} size="sm">
<AvatarFallback className="text-[0.625rem]!">
{initials}
</AvatarFallback>
</Avatar>
))}
{extraGuests > 0 && (
<AvatarGroupCount className="size-6 text-xs">
+{extraGuests}
</AvatarGroupCount>
)}
</AvatarGroup>
{event.status === "live" && (
<Button
size="sm"
variant={joined ? "outline" : "default"}
aria-pressed={joined}
onClick={() => setJoined((current) => !current)}
>
<VideoIcon aria-hidden="true" data-icon="inline-start" />
{joined ? "Joined" : "Join"}
</Button>
)}
</ItemActions>
</Item>
);
})}
</ItemGroup>
</section>
);
}
npx shadcn@latest add @sevenui/component/item-15pnpm dlx shadcn@latest add @sevenui/component/item-15yarn dlx shadcn@latest add @sevenui/component/item-15bunx --bun shadcn@latest add @sevenui/component/item-15- HWHannah Weber2mUnread
Charged twice for the annual plan
I see two $480 charges on my card from this morning.
First reply overdue 4m - DADiego Alvarez18mUnread
SSO login loops back to the sign-in page
Our Okta users get redirected endlessly since yesterday.
First reply due in 12m - MTMei Tanaka1h
How do I export invoices as CSV?
You: Go to Billing, then Invoices, and pick Export.
"use client";
import * as React from "react";
import {
InboxIcon,
MailIcon,
MessageCircleIcon,
SearchIcon,
SmartphoneIcon,
} from "lucide-react";
import { Avatar, AvatarBadge, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import {
Item,
ItemContent,
ItemDescription,
ItemHeader,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
type Queue = "open" | "snoozed" | "resolved";
const CHANNEL_ICONS = {
email: MailIcon,
chat: MessageCircleIcon,
app: SmartphoneIcon,
};
const CHANNEL_LABELS = {
email: "Email",
chat: "Live chat",
app: "In-app",
};
const CONVERSATIONS: {
id: string;
customer: string;
initials: string;
channel: keyof typeof CHANNEL_ICONS;
subject: string;
preview: string;
time: string;
queue: Queue;
unread: boolean;
sla?: { label: string; overdue: boolean };
online?: boolean;
}[] = [
{
id: "c-2841",
customer: "Hannah Weber",
initials: "HW",
channel: "chat",
subject: "Charged twice for the annual plan",
preview: "I see two $480 charges on my card from this morning.",
time: "2m",
queue: "open",
unread: true,
sla: { label: "Overdue 4m", overdue: true },
online: true,
},
{
id: "c-2839",
customer: "Diego Alvarez",
initials: "DA",
channel: "email",
subject: "SSO login loops back to the sign-in page",
preview: "Our Okta users get redirected endlessly since yesterday.",
time: "18m",
queue: "open",
unread: true,
sla: { label: "Due in 12m", overdue: false },
},
{
id: "c-2833",
customer: "Mei Tanaka",
initials: "MT",
channel: "app",
subject: "How do I export invoices as CSV?",
preview: "You: Go to Billing, then Invoices, and pick Export.",
time: "1h",
queue: "open",
unread: false,
},
{
id: "c-2820",
customer: "Samuel Okoro",
initials: "SO",
channel: "email",
subject: "Waiting on legal review of the DPA",
preview: "We'll get back to you once our counsel signs off.",
time: "Mon",
queue: "snoozed",
unread: false,
},
{
id: "c-2807",
customer: "Lucia Romano",
initials: "LR",
channel: "chat",
subject: "Seat count after downgrade",
preview: "Perfect, that answers it. Thanks for the quick help!",
time: "Sep 22",
queue: "resolved",
unread: false,
},
];
const QUEUES: { value: Queue; label: string }[] = [
{ value: "open", label: "Open" },
{ value: "snoozed", label: "Snoozed" },
{ value: "resolved", label: "Resolved" },
];
export default function Item16() {
const [queue, setQueue] = React.useState<Queue>("open");
const [query, setQuery] = React.useState("");
const [selected, setSelected] = React.useState("c-2839");
const [read, setRead] = React.useState<string[]>([]);
const needle = query.trim().toLowerCase();
function open(id: string) {
setSelected(id);
setRead((prev) => (prev.includes(id) ? prev : [...prev, id]));
}
return (
<div className="w-full max-w-md overflow-hidden rounded-xl border bg-card text-card-foreground">
<div className="space-y-3 border-b p-3">
<InputGroup>
<InputGroupAddon>
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
type="search"
aria-label="Search conversations"
placeholder="Search by customer or subject"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</InputGroup>
</div>
<Tabs
value={queue}
onValueChange={(value) => setQueue(value as Queue)}
className="gap-0"
>
<TabsList variant="line" className="w-full justify-start border-b px-3">
{QUEUES.map(({ value, label }) => {
const count = CONVERSATIONS.filter((c) => c.queue === value).length;
return (
<TabsTrigger key={value} value={value} className="flex-none">
{label}
<span className="text-xs text-muted-foreground tabular-nums">
{count}
</span>
</TabsTrigger>
);
})}
</TabsList>
{QUEUES.map(({ value, label }) => {
const results = CONVERSATIONS.filter(
(c) =>
c.queue === value &&
(needle === "" ||
c.customer.toLowerCase().includes(needle) ||
c.subject.toLowerCase().includes(needle)),
);
return (
<TabsContent key={value} value={value}>
{results.length === 0 ? (
<div className="flex flex-col items-center gap-2 px-4 py-10 text-center">
<InboxIcon
aria-hidden="true"
className="size-6 text-muted-foreground"
/>
<p className="text-sm font-medium">No conversations found</p>
<p className="text-sm text-muted-foreground">
{needle
? `Nothing in ${label.toLowerCase()} matches “${query.trim()}”.`
: `Your ${label.toLowerCase()} queue is empty.`}
</p>
</div>
) : (
<ul
aria-label={`${label} conversations`}
className="flex flex-col gap-0.5 p-1.5"
>
{results.map((conversation) => {
const ChannelIcon = CHANNEL_ICONS[conversation.channel];
const isUnread =
conversation.unread && !read.includes(conversation.id);
const isSelected = selected === conversation.id;
return (
<li key={conversation.id}>
<Item
size="sm"
data-unread={isUnread || undefined}
render={
<a
href={`#${conversation.id}`}
aria-current={isSelected ? "true" : undefined}
onClick={(event) => {
event.preventDefault();
open(conversation.id);
}}
/>
}
className="items-start aria-[current=true]:bg-muted"
>
<ItemMedia>
<Avatar>
<AvatarFallback>
{conversation.initials}
</AvatarFallback>
{conversation.online && (
<AvatarBadge className="bg-success" />
)}
</Avatar>
</ItemMedia>
<ItemContent className="min-w-0 gap-0.5">
<ItemHeader className="gap-3">
<ItemTitle className="min-w-0 font-normal in-data-unread:font-semibold">
<span className="truncate">
{conversation.customer}
</span>
<ChannelIcon
role="img"
aria-label={
CHANNEL_LABELS[conversation.channel]
}
className="size-3.5 shrink-0 text-muted-foreground"
/>
</ItemTitle>
<span className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground tabular-nums">
{conversation.time}
{isUnread && (
<span
aria-hidden="true"
className="size-2 rounded-full bg-primary"
/>
)}
{isUnread && (
<span className="sr-only">Unread</span>
)}
</span>
</ItemHeader>
<p className="truncate text-sm in-data-unread:font-medium">
{conversation.subject}
</p>
<ItemDescription className="line-clamp-1 text-xs">
{conversation.preview}
</ItemDescription>
{conversation.sla && (
<Badge
variant="outline"
data-overdue={
conversation.sla.overdue || undefined
}
className="mt-1 gap-1.5 data-overdue:border-destructive/40 data-overdue:text-destructive"
>
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-warning in-data-overdue:bg-destructive"
/>
First reply{" "}
{conversation.sla.label.toLowerCase()}
</Badge>
)}
</ItemContent>
</Item>
</li>
);
})}
</ul>
)}
</TabsContent>
);
})}
</Tabs>
</div>
);
}
npx shadcn@latest add @sevenui/component/item-16pnpm dlx shadcn@latest add @sevenui/component/item-16yarn dlx shadcn@latest add @sevenui/component/item-16bunx --bun shadcn@latest add @sevenui/component/item-16sk_live_4f9c…Used 2 minutes ago
sk_live_a71e…Used Yesterday
sk_test_0b2d…Never used
"use client";
import * as React from "react";
import {
CheckIcon,
CopyIcon,
KeyRoundIcon,
TriangleAlertIcon,
} from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type Scope = "full" | "read" | "ingest";
const SCOPES: { value: Scope; label: string }[] = [
{ value: "full", label: "Full access" },
{ value: "read", label: "Read only" },
{ value: "ingest", label: "Ingest only" },
];
type ApiKey = {
id: string;
name: string;
prefix: string;
scope: Scope;
created: string;
lastUsed: string | null;
};
const INITIAL_KEYS: ApiKey[] = [
{
id: "key-prod",
name: "Production backend",
prefix: "sk_live_4f9c",
scope: "full",
created: "Mar 3, 2026",
lastUsed: "2 minutes ago",
},
{
id: "key-metrics",
name: "Metrics dashboard",
prefix: "sk_live_a71e",
scope: "read",
created: "Jul 19, 2026",
lastUsed: "Yesterday",
},
{
id: "key-ci",
name: "CI smoke tests",
prefix: "sk_test_0b2d",
scope: "ingest",
created: "Sep 20, 2026",
lastUsed: null,
},
];
const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
function randomSecret(length: number) {
const values = new Uint32Array(length);
crypto.getRandomValues(values);
return Array.from(values, (value) => ALPHABET[value % ALPHABET.length]).join(
"",
);
}
function scopeLabel(scope: Scope) {
return SCOPES.find((entry) => entry.value === scope)?.label ?? scope;
}
export default function Item17() {
const id = React.useId();
const [keys, setKeys] = React.useState<ApiKey[]>(INITIAL_KEYS);
const [name, setName] = React.useState("");
const [scope, setScope] = React.useState<Scope>("read");
const [error, setError] = React.useState<string | null>(null);
const [revealed, setRevealed] = React.useState<{
id: string;
secret: string;
} | null>(null);
const [copied, setCopied] = React.useState(false);
function createKey(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const trimmed = name.trim();
if (!trimmed) {
setError("Give the key a name so you can recognize it later.");
return;
}
if (keys.some((key) => key.name.toLowerCase() === trimmed.toLowerCase())) {
setError("A key with this name already exists.");
return;
}
const secret = `sk_live_${randomSecret(28)}`;
const keyId = `key-${Date.now()}`;
setKeys((prev) => [
{
id: keyId,
name: trimmed,
prefix: secret.slice(0, 12),
scope,
created: "Just now",
lastUsed: null,
},
...prev,
]);
setRevealed({ id: keyId, secret });
setCopied(false);
setName("");
setError(null);
}
async function copySecret(secret: string) {
try {
await navigator.clipboard.writeText(secret);
setCopied(true);
} catch {
setCopied(false);
}
}
function revoke(id: string) {
setKeys((prev) => prev.filter((key) => key.id !== id));
if (revealed?.id === id) setRevealed(null);
}
return (
<div className="w-full max-w-lg rounded-xl border bg-card text-card-foreground">
<form
onSubmit={createKey}
noValidate
aria-labelledby={`${id}-heading`}
className="space-y-3 border-b p-4"
>
<div className="space-y-1">
<h3 id={`${id}-heading`} className="text-sm font-semibold">
API keys
</h3>
<p className="text-sm text-muted-foreground">
Keys authenticate server-side requests. Never ship them in client
code.
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
aria-label="Key name"
placeholder="Key name, e.g. Billing worker"
value={name}
onChange={(event) => {
setName(event.target.value);
if (error) setError(null);
}}
aria-invalid={error ? true : undefined}
aria-describedby={error ? `${id}-error` : undefined}
className="sm:flex-1"
/>
<div className="flex gap-2">
<Select
items={SCOPES}
value={scope}
onValueChange={(value) => setScope(value as Scope)}
>
<SelectTrigger
aria-label="Permissions"
className="flex-1 sm:w-34"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{SCOPES.map((entry) => (
<SelectItem key={entry.value} value={entry.value}>
{entry.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button type="submit">Create</Button>
</div>
</div>
{error && (
<p id={`${id}-error`} className="text-sm text-destructive">
{error}
</p>
)}
</form>
{keys.length === 0 ? (
<div className="flex flex-col items-center gap-2 px-4 py-10 text-center">
<KeyRoundIcon
aria-hidden="true"
className="size-6 text-muted-foreground"
/>
<p className="text-sm font-medium">No active keys</p>
<p className="text-sm text-muted-foreground">
Requests without a key will be rejected with a 401.
</p>
</div>
) : (
<ItemGroup aria-label="Active API keys" className="gap-0 divide-y">
{keys.map((key) => {
const isNew = revealed?.id === key.id;
return (
<Item
key={key.id}
role="listitem"
className="rounded-none px-4 py-3"
>
<ItemMedia
variant="icon"
className="size-8 rounded-md border bg-muted/50 text-muted-foreground"
>
<KeyRoundIcon aria-hidden="true" />
</ItemMedia>
<ItemContent className="min-w-0">
<ItemTitle className="w-full flex-wrap gap-y-1">
<span className="min-w-0 truncate">{key.name}</span>
<Badge
variant={key.scope === "full" ? "secondary" : "outline"}
>
{scopeLabel(key.scope)}
</Badge>
</ItemTitle>
<ItemDescription className="flex flex-wrap gap-x-2 text-xs">
<code className="font-mono text-foreground">
{key.prefix}…
</code>
<span>
{key.lastUsed ? `Used ${key.lastUsed}` : "Never used"}
</span>
</ItemDescription>
</ItemContent>
<ItemActions>
<AlertDialog>
<AlertDialogTrigger
render={
<Button
size="sm"
variant="ghost"
className="text-muted-foreground hover:text-destructive"
/>
}
>
Revoke
<span className="sr-only"> {key.name}</span>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive">
<TriangleAlertIcon aria-hidden="true" />
</AlertDialogMedia>
<AlertDialogTitle>
Revoke “{key.name}”?
</AlertDialogTitle>
<AlertDialogDescription>
{key.lastUsed
? `This key was used ${key.lastUsed.toLowerCase()}. Requests signed with it will fail immediately.`
: "Requests signed with this key will fail immediately."}{" "}
This can't be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep key</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() => revoke(key.id)}
>
Revoke key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ItemActions>
{isNew && revealed && (
<ItemFooter className="flex-col items-stretch gap-2 rounded-lg border border-dashed bg-muted/40 p-3">
<p className="text-xs text-muted-foreground">
Copy this secret now. For your security it won't be shown
again.
</p>
<div className="flex items-center gap-2">
<code className="min-w-0 flex-1 truncate rounded-md border bg-background px-2 py-1.5 font-mono text-xs">
{revealed.secret}
</code>
<Button
size="icon-sm"
variant="outline"
aria-label={copied ? "Secret copied" : "Copy secret"}
onClick={() => copySecret(revealed.secret)}
>
{copied ? (
<CheckIcon aria-hidden="true" />
) : (
<CopyIcon aria-hidden="true" />
)}
</Button>
</div>
<Button
size="xs"
variant="ghost"
className="self-end"
onClick={() => setRevealed(null)}
>
I've saved it
</Button>
</ItemFooter>
)}
</Item>
);
})}
</ItemGroup>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/item-17pnpm dlx shadcn@latest add @sevenui/component/item-17yarn dlx shadcn@latest add @sevenui/component/item-17bunx --bun shadcn@latest add @sevenui/component/item-17