The frame an app lives in: a sidebar with workspace switcher, nav, colour-dotted projects and an account menu that folds down to an icon rail, beside a rounded inset page with live breadcrumbs and a floating drawer on phones.
registry/devl/inset-shell.tsx
"use client";
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from "motion/react";
import {
BellIcon,
CalendarIcon,
ChartColumnIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ChevronsUpDownIcon,
CircleDotIcon,
FileTextIcon,
HouseIcon,
InboxIcon,
KeyboardIcon,
LayoutGridIcon,
LifeBuoyIcon,
LogOutIcon,
type LucideIcon,
MenuIcon,
PanelLeftIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
Share2Icon,
UserRoundIcon,
XIcon,
} from "lucide-react";
import { type ReactElement, type ReactNode, useEffect, useId, useRef, useState } from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
Menu,
MenuGroup,
MenuGroupLabel,
MenuItem,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuShortcut,
MenuTrigger,
} from "@/components/ui/menu";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export type ShellTone = "orange" | "teal" | "cyan" | "violet" | "rose" | "info" | "success" | "warning";
export interface ShellWorkspace {
id: string;
name: string;
plan: string;
initials: string;
tone: ShellTone;
}
export interface ShellNavItem {
id: string;
label: string;
icon: LucideIcon;
count?: number;
}
export interface ShellProject {
id: string;
name: string;
tone: ShellTone;
open?: number;
}
export interface ShellUser {
name: string;
email: string;
initials: string;
}
export interface InsetShellProps {
workspaces?: ShellWorkspace[];
nav?: ShellNavItem[];
projects?: ShellProject[];
user?: ShellUser;
breadcrumbs?: string[];
defaultCollapsed?: boolean;
children?: ReactNode;
className?: string;
}
const dot: Record<ShellTone, string> = {
"orange": "bg-orange-500",
"teal": "bg-teal-500",
"cyan": "bg-cyan-500",
"violet": "bg-violet-500",
"rose": "bg-rose-500",
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
};
const soft: Record<ShellTone, string> = {
orange: "bg-orange-500/14",
teal: "bg-teal-500/14",
cyan: "bg-cyan-500/14",
violet: "bg-violet-500/14",
rose: "bg-rose-500/14",
info: "bg-info/14",
success: "bg-success/14",
warning: "bg-warning/14",
};
const spring = { type: "spring", stiffness: 420, damping: 36 } as const;
const widthSpring = { type: "spring", stiffness: 380, damping: 38 } as const;
const defaultWorkspaces: ShellWorkspace[] = [
{ id: "northwind", name: "Northwind Labs", plan: "Team", initials: "NL", tone: "info" },
{ id: "fable", name: "Fable & Co", plan: "Free", initials: "FC", tone: "orange" },
{ id: "pilot", name: "Pilot Studio", plan: "Business", initials: "PS", tone: "teal" },
];
const defaultNav: ShellNavItem[] = [
{ id: "home", label: "Home", icon: HouseIcon },
{ id: "inbox", label: "Inbox", icon: InboxIcon, count: 4 },
{ id: "issues", label: "My issues", icon: CircleDotIcon, count: 6 },
{ id: "projects", label: "Projects", icon: LayoutGridIcon },
{ id: "docs", label: "Docs", icon: FileTextIcon },
{ id: "reports", label: "Reports", icon: ChartColumnIcon },
];
const defaultProjects: ShellProject[] = [
{ id: "atlas", name: "Atlas redesign", tone: "orange", open: 24 },
{ id: "billing", name: "Billing v2", tone: "teal", open: 9 },
{ id: "mobile", name: "Mobile beta", tone: "info", open: 17 },
{ id: "growth", name: "Growth experiments", tone: "violet", open: 6 },
{ id: "infra", name: "Infra hardening", tone: "rose", open: 3 },
];
const defaultUser: ShellUser = { name: "Ines Moreau", email: "ines@northwind.dev", initials: "IM" };
function SidebarRow({
collapsed,
label,
tip,
children,
}: {
collapsed: boolean;
label: string;
tip?: ReactNode;
children: (props: { "aria-label"?: string }) => ReactNode;
}) {
return (
<Tooltip disabled={!collapsed}>
<TooltipTrigger render={children({ "aria-label": collapsed ? label : undefined }) as ReactElement} />
<TooltipPopup side="right" sideOffset={10}>
{tip ?? label}
</TooltipPopup>
</Tooltip>
);
}
function Label({ collapsed, children, className }: { collapsed: boolean; children: ReactNode; className?: string }) {
return (
<span
aria-hidden={collapsed || undefined}
className={cn(
"min-w-0 truncate transition-[opacity,translate] duration-200",
collapsed && "pointer-events-none -translate-x-1 opacity-0",
className,
)}
>
{children}
</span>
);
}
interface SidebarProps {
collapsed: boolean;
workspaces: ShellWorkspace[];
workspace: string;
onWorkspace: (id: string) => void;
nav: ShellNavItem[];
active: string;
onNavigate: (id: string, label: string) => void;
projects: ShellProject[];
user: ShellUser;
onClose?: () => void;
}
function SidebarBody({
collapsed,
workspaces,
workspace,
onWorkspace,
nav,
active,
onNavigate,
projects,
user,
onClose,
}: SidebarProps) {
const [projectsOpen, setProjectsOpen] = useState(true);
const ws = workspaces.find((w) => w.id === workspace) ?? workspaces[0];
const pill = useId();
const row =
"group/row relative flex h-8 w-full cursor-pointer items-center gap-2.5 overflow-hidden rounded-lg px-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring";
return (
<LayoutGroup id={pill}>
<div className="flex h-full min-h-0 flex-col whitespace-nowrap">
<div className="flex items-center gap-1 p-2">
<Menu>
<MenuTrigger
aria-label={`Workspace: ${ws.name}`}
className="flex h-11 min-w-0 flex-1 cursor-pointer items-center gap-2.5 overflow-hidden rounded-xl px-1.5 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent"
>
<span
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-lg font-semibold text-[11px] text-white",
dot[ws.tone],
)}
>
{ws.initials}
</span>
<span className={cn("flex min-w-0 flex-1 flex-col transition-opacity duration-200", collapsed && "opacity-0")}>
<span className="truncate font-medium leading-tight">{ws.name}</span>
<span className="truncate text-muted-foreground text-xs leading-tight">{ws.plan} plan</span>
</span>
<ChevronsUpDownIcon className={cn("size-4 shrink-0 text-muted-foreground", collapsed && "opacity-0")} />
</MenuTrigger>
<MenuPopup align="start" className="w-60">
<MenuGroup>
<MenuGroupLabel>Workspaces</MenuGroupLabel>
<MenuRadioGroup onValueChange={(v) => onWorkspace(v as string)} value={workspace}>
{workspaces.map((w) => (
<MenuRadioItem key={w.id} value={w.id}>
<span className="flex items-center gap-2">
<span className={cn("size-2 rounded-full", dot[w.tone])} />
{w.name}
<span className="text-muted-foreground text-xs">{w.plan}</span>
</span>
</MenuRadioItem>
))}
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator />
<MenuItem>
<PlusIcon />
Create workspace
</MenuItem>
<MenuItem>
<SettingsIcon />
Workspace settings
</MenuItem>
</MenuPopup>
</Menu>
{onClose && (
<Button aria-label="Close navigation" onClick={onClose} size="icon-sm" variant="ghost">
<XIcon />
</Button>
)}
</div>
<div className="px-2 pb-2">
<SidebarRow collapsed={collapsed} label="Search" tip={<span>Search <Kbd className="ms-1">⌘K</Kbd></span>}>
{(a) => (
<button
{...a}
className={cn(
row,
"border bg-background text-muted-foreground shadow-xs/5 hover:text-foreground dark:bg-input/32",
collapsed && "border-transparent bg-transparent shadow-none dark:bg-transparent",
)}
type="button"
>
<SearchIcon className="size-4 shrink-0" />
<Label collapsed={collapsed}>Search</Label>
<Kbd className={cn("ms-auto transition-opacity", collapsed && "opacity-0")}>⌘K</Kbd>
</button>
)}
</SidebarRow>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-2 [scrollbar-width:thin]">
<nav aria-label="Primary" className="flex flex-col gap-0.5">
{nav.map((n) => {
const on = active === n.id;
return (
<SidebarRow collapsed={collapsed} key={n.id} label={n.label}>
{(a) => (
<button
{...a}
aria-current={on ? "page" : undefined}
className={cn(row, on ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
onClick={() => onNavigate(n.id, n.label)}
type="button"
>
{on && (
<motion.span
className="absolute inset-0 rounded-lg bg-background shadow-xs/5 ring-1 ring-border dark:bg-accent dark:ring-0"
layoutId="shell-nav-pill"
transition={spring}
/>
)}
{!on && <span className="absolute inset-0 rounded-lg transition-colors group-hover/row:bg-accent" />}
<n.icon className="relative size-4 shrink-0" />
<Label className="relative font-medium" collapsed={collapsed}>
{n.label}
</Label>
{n.count !== undefined && (
<span
className={cn(
"relative ms-auto rounded-full px-1.5 text-xs tabular-nums transition-opacity",
n.id === "inbox" ? "bg-info/12 font-medium text-info-foreground" : "text-muted-foreground",
collapsed && "opacity-0",
)}
>
{n.count}
</span>
)}
{collapsed && n.id === "inbox" && (
<span className="absolute top-1.5 left-7 size-1.5 rounded-full bg-info ring-2 ring-sidebar" />
)}
</button>
)}
</SidebarRow>
);
})}
</nav>
<div className="mt-5">
<div className="relative flex h-7 items-center px-3">
<button
aria-expanded={projectsOpen}
className={cn(
"flex cursor-pointer items-center gap-1 rounded font-medium text-muted-foreground text-xs outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring",
collapsed && "pointer-events-none opacity-0",
)}
onClick={() => setProjectsOpen((o) => !o)}
tabIndex={collapsed ? -1 : 0}
type="button"
>
Projects
<ChevronDownIcon
className={cn("size-3 transition-transform duration-200", !projectsOpen && "-rotate-90")}
/>
</button>
{collapsed && <span className="absolute left-4 h-px w-6 bg-border" />}
<Button
aria-label="New project"
className={cn("ms-auto", collapsed && "pointer-events-none opacity-0")}
size="icon-xs"
tabIndex={collapsed ? -1 : 0}
variant="ghost"
>
<PlusIcon />
</Button>
</div>
<AnimatePresence initial={false}>
{(projectsOpen || collapsed) && (
<motion.ul
animate={{ height: "auto", opacity: 1 }}
className="flex flex-col gap-0.5 overflow-hidden"
exit={{ height: 0, opacity: 0 }}
initial={{ height: 0, opacity: 0 }}
transition={spring}
>
{projects.map((p) => {
const on = active === `project-${p.id}`;
return (
<li key={p.id}>
<SidebarRow collapsed={collapsed} label={p.name}>
{(a) => (
<button
{...a}
aria-current={on ? "page" : undefined}
className={cn(row, on ? "text-foreground" : "text-muted-foreground hover:text-foreground")}
onClick={() => onNavigate(`project-${p.id}`, p.name)}
type="button"
>
{on && (
<motion.span
className="absolute inset-0 rounded-lg bg-background shadow-xs/5 ring-1 ring-border dark:bg-accent dark:ring-0"
layoutId="shell-nav-pill"
transition={spring}
/>
)}
{!on && (
<span className="absolute inset-0 rounded-lg transition-colors group-hover/row:bg-accent" />
)}
<span className="relative flex size-4 shrink-0 items-center justify-center">
<span className={cn("size-2 rounded-full ring-3 ring-current/0", dot[p.tone])} />
</span>
<Label className="relative" collapsed={collapsed}>
{p.name}
</Label>
{p.open !== undefined && (
<span
className={cn(
"relative ms-auto text-muted-foreground/80 text-xs tabular-nums opacity-0 transition-opacity group-hover/row:opacity-100",
on && "opacity-100",
collapsed && "opacity-0!",
)}
>
{p.open}
</span>
)}
</button>
)}
</SidebarRow>
</li>
);
})}
</motion.ul>
)}
</AnimatePresence>
</div>
</div>
<div className="flex flex-col gap-0.5 p-2">
<SidebarRow collapsed={collapsed} label="Help & support">
{(a) => (
<button {...a} className={cn(row, "text-muted-foreground hover:bg-accent hover:text-foreground")} type="button">
<LifeBuoyIcon className="size-4 shrink-0" />
<Label collapsed={collapsed}>Help & support</Label>
</button>
)}
</SidebarRow>
<Menu>
<MenuTrigger
aria-label={`Account: ${user.name}`}
className="mt-1 flex h-12 w-full cursor-pointer items-center gap-2.5 overflow-hidden rounded-xl px-1.5 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-popup-open:bg-accent"
>
<Avatar className="size-7 shrink-0">
<AvatarFallback className="bg-teal-500/16 text-[11px] text-teal-600 dark:text-teal-400">{user.initials}</AvatarFallback>
</Avatar>
<span className={cn("flex min-w-0 flex-1 flex-col transition-opacity duration-200", collapsed && "opacity-0")}>
<span className="truncate font-medium leading-tight">{user.name}</span>
<span className="truncate text-muted-foreground text-xs leading-tight">{user.email}</span>
</span>
<ChevronsUpDownIcon className={cn("size-4 shrink-0 text-muted-foreground", collapsed && "opacity-0")} />
</MenuTrigger>
<MenuPopup align="start" className="w-56" side="top">
<div className="flex items-center gap-2.5 px-2 py-1.5">
<Avatar className="size-8">
<AvatarFallback className="bg-teal-500/16 text-teal-600 dark:text-teal-400 text-xs">{user.initials}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<div className="truncate font-medium text-sm">{user.name}</div>
<div className="truncate text-muted-foreground text-xs">{user.email}</div>
</div>
</div>
<MenuSeparator />
<MenuItem>
<UserRoundIcon />
Profile
</MenuItem>
<MenuItem>
<SettingsIcon />
Preferences
<MenuShortcut>⌘,</MenuShortcut>
</MenuItem>
<MenuItem>
<KeyboardIcon />
Shortcuts
<MenuShortcut>?</MenuShortcut>
</MenuItem>
<MenuSeparator />
<MenuItem>
<LogOutIcon />
Sign out
</MenuItem>
</MenuPopup>
</Menu>
</div>
</div>
</LayoutGroup>
);
}
type Status = "on-track" | "at-risk" | "done";
interface ProjectDetail {
summary: string;
status: Status;
progress: number;
week: number;
open: number;
total: number;
target: string;
targetIn: string;
targetNote: string;
milestones: { name: string; date: string; done: number }[];
updates: { who: string; what: string; target: string; to?: string; time: string }[];
}
const people: Record<string, { initials: string; tone: string }> = {
"Kai Tanaka": { initials: "KT", tone: "bg-blue-500/14 text-blue-700 dark:text-blue-300" },
"Rosa Lind": { initials: "RL", tone: "bg-orange-500/14 text-orange-700 dark:text-orange-300" },
"Ines Moreau": { initials: "IM", tone: "bg-teal-500/16 text-teal-700 dark:text-teal-300" },
"Theo Brandt": { initials: "TB", tone: "bg-violet-500/16 text-violet-700 dark:text-violet-300" },
};
const details: Record<string, ProjectDetail> = {
atlas: {
summary: "A calmer navigation model for Orbit: fewer top-level items, projects one click away, and a sidebar that gets out of the way.",
status: "on-track",
progress: 68,
week: 9,
open: 24,
total: 76,
target: "Nov 14",
targetIn: "in 21 days",
targetNote: "Beta review with 50 teams",
milestones: [
{ name: "Research synthesis", date: "Oct 4", done: 100 },
{ name: "Navigation & IA", date: "Oct 22", done: 82 },
{ name: "Component audit", date: "Nov 3", done: 46 },
{ name: "Beta to 50 teams", date: "Nov 14", done: 12 },
],
updates: [
{ who: "Kai Tanaka", what: "moved", target: "Sidebar density pass", to: "In review", time: "12m" },
{ who: "Rosa Lind", what: "commented on", target: "Breadcrumb overflow", time: "48m" },
{ who: "Ines Moreau", what: "closed", target: "Empty states for projects", time: "2h" },
{ who: "Theo Brandt", what: "added", target: "Keyboard map for nav", to: "Backlog", time: "3h" },
],
},
billing: {
summary: "Usage-based pricing with prorated seat changes, a clearer invoice and fewer failed renewals.",
status: "at-risk",
progress: 41,
week: 3,
open: 9,
total: 22,
target: "Dec 2",
targetIn: "in 39 days",
targetNote: "Migrate the first 200 workspaces",
milestones: [
{ name: "Pricing model sign-off", date: "Oct 1", done: 100 },
{ name: "Proration engine", date: "Oct 28", done: 64 },
{ name: "Invoice redesign", date: "Nov 12", done: 20 },
{ name: "Staged migration", date: "Dec 2", done: 0 },
],
updates: [
{ who: "Theo Brandt", what: "flagged", target: "Tax rounding for EU", to: "Blocked", time: "25m" },
{ who: "Ines Moreau", what: "moved", target: "Retry failed charges", to: "In progress", time: "1h" },
{ who: "Kai Tanaka", what: "closed", target: "Seat count webhook", time: "4h" },
],
},
mobile: {
summary: "A native iOS and Android app for triage on the go: inbox, quick replies and push for mentions.",
status: "on-track",
progress: 55,
week: 6,
open: 17,
total: 38,
target: "Nov 28",
targetIn: "in 35 days",
targetNote: "TestFlight to 400 testers",
milestones: [
{ name: "Navigation shell", date: "Oct 10", done: 100 },
{ name: "Inbox and replies", date: "Nov 1", done: 70 },
{ name: "Push notifications", date: "Nov 18", done: 30 },
{ name: "Public beta", date: "Nov 28", done: 0 },
],
updates: [
{ who: "Rosa Lind", what: "shipped", target: "Swipe to archive", time: "40m" },
{ who: "Kai Tanaka", what: "commented on", target: "Offline drafts", time: "2h" },
{ who: "Theo Brandt", what: "moved", target: "Deep links", to: "In review", time: "5h" },
],
},
growth: {
summary: "Small weekly bets on activation: a shorter signup, a guided first project and better invite emails.",
status: "on-track",
progress: 30,
week: 4,
open: 6,
total: 14,
target: "Dec 15",
targetIn: "in 52 days",
targetNote: "Lift week-one activation to 45%",
milestones: [
{ name: "Signup in two steps", date: "Oct 18", done: 100 },
{ name: "Guided first project", date: "Nov 8", done: 35 },
{ name: "Invite email tests", date: "Nov 25", done: 10 },
{ name: "Readout", date: "Dec 15", done: 0 },
],
updates: [
{ who: "Ines Moreau", what: "started", target: "Invite subject line test", time: "1h" },
{ who: "Rosa Lind", what: "closed", target: "Remove company size field", time: "1d" },
],
},
infra: {
summary: "Fewer pages at night: faster deploy rollbacks, regional failover for the API and saner alert thresholds.",
status: "done",
progress: 92,
week: 5,
open: 3,
total: 41,
target: "Oct 31",
targetIn: "in 7 days",
targetNote: "Failover drill with on-call",
milestones: [
{ name: "One-click rollback", date: "Sep 20", done: 100 },
{ name: "Alert threshold review", date: "Oct 6", done: 100 },
{ name: "Regional failover", date: "Oct 24", done: 100 },
{ name: "Failover drill", date: "Oct 31", done: 60 },
],
updates: [
{ who: "Kai Tanaka", what: "closed", target: "Failover for eu-west", time: "3h" },
{ who: "Theo Brandt", what: "scheduled", target: "Drill with on-call", to: "Oct 31", time: "6h" },
],
},
};
const statusBadge: Record<Status, { label: string; variant: "success" | "warning" | "info" }> = {
"on-track": { label: "On track", variant: "success" },
"at-risk": { label: "At risk", variant: "warning" },
done: { label: "Wrapping up", variant: "info" },
};
const ease = [0.23, 1, 0.32, 1] as const;
const owners: Record<string, { lead: string; start: string; team: string }> = {
atlas: { lead: "Kai Tanaka", start: "Sep 8", team: "Design" },
billing: { lead: "Theo Brandt", start: "Sep 15", team: "Payments" },
mobile: { lead: "Rosa Lind", start: "Sep 1", team: "Mobile" },
growth: { lead: "Ines Moreau", start: "Oct 6", team: "Growth" },
infra: { lead: "Kai Tanaka", start: "Aug 25", team: "Platform" },
};
function PersonDot({ name, className }: { name: string; className?: string }) {
return (
<Avatar className={cn("size-5", className)}>
<AvatarFallback className={cn("font-medium text-[9px]", people[name]?.tone)}>{people[name]?.initials}</AvatarFallback>
</Avatar>
);
}
function ProgressRing({ value, tone }: { value: number; tone: ShellTone }) {
const r = 6;
const c = 2 * Math.PI * r;
return (
<svg aria-hidden className="size-4 -rotate-90" viewBox="0 0 16 16">
<circle className="stroke-muted" cx="8" cy="8" fill="none" r={r} strokeWidth="2.5" />
<motion.circle
animate={{ strokeDashoffset: c * (1 - value / 100) }}
className={ringTone[tone]}
cx="8"
cy="8"
fill="none"
initial={{ strokeDashoffset: c }}
r={r}
strokeDasharray={c}
strokeLinecap="round"
strokeWidth="2.5"
transition={{ duration: 0.6, ease, delay: 0.1 }}
/>
</svg>
);
}
const ringTone: Record<ShellTone, string> = {
orange: "stroke-orange-500",
teal: "stroke-teal-500",
cyan: "stroke-cyan-500",
violet: "stroke-violet-500",
rose: "stroke-rose-500",
info: "stroke-info",
success: "stroke-success",
warning: "stroke-warning",
};
function Properties({ project, d, className }: { project: ShellProject; d: ProjectDetail; className?: string }) {
const o = owners[project.id] ?? owners.atlas;
const badge = statusBadge[d.status];
const members = Array.from(new Set([o.lead, ...d.updates.map((u) => u.who)]));
const row = "flex min-h-8 items-center gap-2";
return (
<dl className={cn("grid grid-cols-[88px_minmax(0,1fr)] gap-x-3 gap-y-1 text-sm", className)}>
<dt className={cn(row, "text-muted-foreground text-xs")}>Status</dt>
<dd className={row}>
<Badge variant={badge.variant}>{badge.label}</Badge>
</dd>
<dt className={cn(row, "text-muted-foreground text-xs")}>Lead</dt>
<dd className={cn(row, "min-w-0")}>
<PersonDot name={o.lead} />
<span className="truncate">{o.lead}</span>
</dd>
<dt className={cn(row, "text-muted-foreground text-xs")}>Members</dt>
<dd className={row}>
<span className="flex -space-x-1">
{members.map((m) => (
<PersonDot className="bg-background ring-2 ring-background" key={m} name={m} />
))}
</span>
<span className="text-muted-foreground text-xs tabular-nums">{members.length}</span>
</dd>
<dt className={cn(row, "text-muted-foreground text-xs")}>Dates</dt>
<dd className={cn(row, "tabular-nums")}>
<CalendarIcon className="size-3.5 text-muted-foreground" />
{o.start}
<ChevronRightIcon className="size-3 text-muted-foreground" />
{d.target}
</dd>
<dt className={cn(row, "text-muted-foreground text-xs")}>Team</dt>
<dd className={row}>
<span className="inline-flex h-6 items-center gap-1.5 rounded-md border px-2 text-xs">
<span className={cn("size-1.5 rounded-full", dot[project.tone])} />
{o.team}
</span>
</dd>
</dl>
);
}
function ProjectOverview({ project }: { project: ShellProject }) {
const d = details[project.id] ?? details.atlas;
const [tab, setTab] = useState("overview");
const tabId = useId();
const tabs = [
{ id: "overview", label: "Overview" },
{ id: "issues", label: "Issues", count: d.open },
{ id: "docs", label: "Docs", count: d.milestones.length + 3 },
{ id: "activity", label: "Activity" },
];
const closed = d.total - d.open;
return (
<div className="mx-auto flex w-full max-w-5xl gap-10 px-4 py-6 @3xl:px-8 @3xl:py-8">
<div className="flex min-w-0 flex-1 flex-col gap-6">
<div className="flex flex-col gap-2">
<span className={cn("flex size-8 items-center justify-center rounded-lg", soft[project.tone])}>
<span className={cn("size-2.5 rounded-full", dot[project.tone])} />
</span>
<h1 className="mt-2 text-balance font-heading font-semibold text-2xl tracking-tight">{project.name}</h1>
<p className="max-w-xl text-pretty text-muted-foreground">{d.summary}</p>
</div>
<Properties className="rounded-xl border bg-card px-4 py-3 @4xl:hidden" d={d} project={project} />
<LayoutGroup id={tabId}>
<div className="flex gap-1 overflow-x-auto border-b [scrollbar-width:none]" role="tablist">
{tabs.map((t) => (
<button
aria-selected={tab === t.id}
className={cn(
"relative flex h-9 shrink-0 cursor-pointer items-center gap-1.5 px-2.5 font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring",
tab === t.id ? "text-foreground" : "text-muted-foreground hover:text-foreground",
)}
key={t.id}
onClick={() => setTab(t.id)}
role="tab"
type="button"
>
{t.label}
{t.count !== undefined && <span className="text-muted-foreground text-xs tabular-nums">{t.count}</span>}
{tab === t.id && (
<motion.span className="absolute inset-x-1 -bottom-px h-0.5 rounded-full bg-foreground" layoutId="tab" transition={spring} />
)}
</button>
))}
</div>
</LayoutGroup>
<section className="flex flex-col gap-3">
<div className="flex items-baseline justify-between gap-3">
<h2 className="font-medium">Progress</h2>
<span className="text-muted-foreground text-xs tabular-nums">
<span className="text-success-foreground">+{d.week}%</span> this week
</span>
</div>
<div className="flex items-center gap-3">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
<motion.div
animate={{ scaleX: d.progress / 100 }}
className={cn("h-full origin-left rounded-full", dot[project.tone])}
initial={{ scaleX: 0 }}
key={project.id}
transition={{ duration: 0.6, ease, delay: 0.1 }}
/>
</div>
<span className="w-10 text-right font-medium tabular-nums">{d.progress}%</span>
</div>
<div className="flex flex-wrap gap-x-5 gap-y-1 text-muted-foreground text-xs tabular-nums">
<span>
<span className="font-medium text-foreground">{closed}</span> done
</span>
<span>
<span className="font-medium text-foreground">{d.open}</span> open
</span>
<span>
Target <span className="font-medium text-foreground">{d.target}</span> · {d.targetIn}
</span>
</div>
</section>
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h2 className="font-medium">Milestones</h2>
<Button className="-me-2" size="xs" variant="ghost">
<PlusIcon />
Add
</Button>
</div>
<ul className="divide-y rounded-xl border bg-card">
{d.milestones.map((m) => {
const done = m.done === 100;
return (
<li className="flex h-11 items-center gap-3 px-3.5" key={m.name}>
<span
aria-label={done ? "Complete" : `${m.done}% complete`}
className={cn(
"size-2.5 shrink-0 rotate-45 rounded-[2px] border",
done ? "border-success bg-success" : m.done > 0 ? cn("border-transparent", dot[project.tone]) : "border-muted-foreground/50",
)}
role="img"
/>
<span className={cn("min-w-0 flex-1 truncate", done && "text-muted-foreground")}>{m.name}</span>
<span className="w-10 text-right text-muted-foreground text-xs tabular-nums">{m.done}%</span>
<span className="w-14 text-right text-muted-foreground text-xs tabular-nums">{m.date}</span>
</li>
);
})}
</ul>
</section>
<section className="flex flex-col gap-3">
<h2 className="font-medium">Latest updates</h2>
<ul className="relative flex flex-col gap-4 before:absolute before:top-3 before:bottom-3 before:left-[9.5px] before:w-px before:bg-border">
{d.updates.map((u) => (
<li className="relative flex gap-3" key={u.target}>
<PersonDot className="bg-background ring-4 ring-background" name={u.who} />
<p className="min-w-0 flex-1 text-pretty text-muted-foreground leading-5">
<span className="font-medium text-foreground">{u.who}</span> {u.what}{" "}
<span className="text-foreground">{u.target}</span>
{u.to && (
<>
{" "}
to <span className="text-foreground">{u.to}</span>
</>
)}
</p>
<span className="text-muted-foreground text-xs tabular-nums leading-5">{u.time} ago</span>
</li>
))}
</ul>
</section>
</div>
<aside aria-label="Project properties" className="hidden w-64 shrink-0 flex-col gap-4 @4xl:flex">
<div className="flex items-center gap-2 font-medium text-muted-foreground text-xs">
<ProgressRing tone={project.tone} value={d.progress} />
<span className="tabular-nums">{d.progress}% complete</span>
</div>
<Properties d={d} project={project} />
<div className="border-t pt-4 text-muted-foreground text-xs leading-5">
<div className="font-medium text-foreground">Next up</div>
{d.targetNote} on <span className="text-foreground tabular-nums">{d.target}</span>.
</div>
</aside>
</div>
);
}
const inbox = [
{ who: "Kai Tanaka", what: "assigned you", target: "Sidebar density pass", where: "Atlas redesign", time: "12m", unread: true },
{ who: "Rosa Lind", what: "mentioned you on", target: "Breadcrumb overflow", where: "Atlas redesign", time: "48m", unread: true },
{ who: "Theo Brandt", what: "asked for review on", target: "Tax rounding for EU", where: "Billing v2", time: "1h", unread: true },
{ who: "Ines Moreau", what: "replied on", target: "Swipe to archive", where: "Mobile beta", time: "3h", unread: true },
{ who: "Kai Tanaka", what: "closed", target: "Failover for eu-west", where: "Infra hardening", time: "Yesterday", unread: false },
];
const myIssues = [
{ id: "ATL-212", title: "Collapse sidebar to an icon rail", project: "atlas", state: "In progress" },
{ id: "ATL-219", title: "Keyboard map for navigation", project: "atlas", state: "Todo" },
{ id: "BIL-142", title: "Prorate seat changes mid-cycle", project: "billing", state: "In progress" },
{ id: "MOB-88", title: "Push for mentions only", project: "mobile", state: "Todo" },
{ id: "GRO-31", title: "Guided first project checklist", project: "growth", state: "In review" },
{ id: "INF-57", title: "Alert on p95 latency, not p50", project: "infra", state: "Todo" },
];
const docs = [
{ title: "Navigation principles", project: "atlas", edited: "Edited 2h ago by Rosa Lind" },
{ title: "Pricing FAQ for support", project: "billing", edited: "Edited yesterday by Theo Brandt" },
{ title: "Mobile beta test plan", project: "mobile", edited: "Edited Mon by Kai Tanaka" },
{ title: "On-call handbook", project: "infra", edited: "Edited Oct 12 by Ines Moreau" },
];
function NavPage({ id, label, icon: Icon, projects, onOpenProject }: { id: string; label: string; icon: LucideIcon; projects: ShellProject[]; onOpenProject: (p: ShellProject) => void }) {
const projectBy = (pid: string) => projects.find((p) => p.id === pid);
const rowCls = "flex w-full cursor-pointer items-center gap-3 rounded-xl px-3 py-2.5 text-left outline-none transition-[background-color,scale] duration-150 ease-out hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]";
const intro: Record<string, string> = {
home: "Your projects at a glance.",
inbox: "Mentions, assignments and reviews waiting on you.",
issues: "Everything assigned to you, across projects.",
projects: "Every project in this workspace.",
docs: "Recently edited docs you can open.",
reports: "Charts and weekly digests for this workspace.",
};
let body: ReactNode;
if (id === "home" || id === "projects") {
body = (
<ul className="grid grid-cols-1 gap-3 @lg:grid-cols-2">
{projects.map((p) => {
const d = details[p.id];
return (
<li key={p.id}>
<button
className="flex w-full cursor-pointer flex-col gap-3 rounded-2xl border bg-card p-4 text-left outline-none transition-[border-color,scale] duration-150 ease-out hover:border-foreground/16 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.98]"
onClick={() => onOpenProject(p)}
type="button"
>
<span className="flex items-center gap-2">
<span className={cn("size-2 rounded-full", dot[p.tone])} />
<span className="font-medium">{p.name}</span>
{d && <Badge className="ms-auto" variant={statusBadge[d.status].variant}>{statusBadge[d.status].label}</Badge>}
</span>
{d && (
<>
<span className="h-1 overflow-hidden rounded-full bg-muted">
<span className={cn("block h-full origin-left rounded-full", dot[p.tone])} style={{ transform: `scaleX(${d.progress / 100})` }} />
</span>
<span className="flex justify-between text-muted-foreground text-xs tabular-nums">
<span>{d.progress}% · {p.open ?? d.open} open</span>
<span>Due {d.target}</span>
</span>
</>
)}
</button>
</li>
);
})}
</ul>
);
} else if (id === "inbox") {
body = (
<ul className="-mx-3 flex flex-col">
{inbox.map((n) => (
<li key={n.target}>
<button className={rowCls} type="button">
<Avatar className="size-7">
<AvatarFallback className={cn("text-[10px]", people[n.who]?.tone)}>{people[n.who]?.initials}</AvatarFallback>
</Avatar>
<span className="min-w-0 flex-1">
<span className="block truncate text-muted-foreground">
<span className="font-medium text-foreground">{n.who}</span> {n.what} <span className="text-foreground">{n.target}</span>
</span>
<span className="block truncate text-muted-foreground text-xs">{n.where}</span>
</span>
<span className="text-muted-foreground text-xs tabular-nums">{n.time}</span>
<span className={cn("size-1.5 shrink-0 rounded-full", n.unread ? "bg-info" : "bg-transparent")} />
</button>
</li>
))}
</ul>
);
} else if (id === "issues") {
body = (
<ul className="-mx-3 flex flex-col">
{myIssues.map((i) => {
const p = projectBy(i.project);
return (
<li key={i.id}>
<button className={rowCls} type="button">
<span className="w-16 shrink-0 font-mono text-muted-foreground text-xs">{i.id}</span>
<span className="min-w-0 flex-1 truncate">{i.title}</span>
{p && (
<span className="hidden items-center gap-1.5 text-muted-foreground text-xs @md:flex">
<span className={cn("size-1.5 rounded-full", dot[p.tone])} />
{p.name}
</span>
)}
<span className="w-20 text-right text-muted-foreground text-xs">{i.state}</span>
</button>
</li>
);
})}
</ul>
);
} else if (id === "docs") {
body = (
<ul className="-mx-3 flex flex-col">
{docs.map((doc) => {
const p = projectBy(doc.project);
return (
<li key={doc.title}>
<button className={rowCls} type="button">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg border bg-card text-muted-foreground">
<FileTextIcon className="size-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{doc.title}</span>
<span className="block truncate text-muted-foreground text-xs">{doc.edited}</span>
</span>
{p && (
<span className="hidden items-center gap-1.5 text-muted-foreground text-xs @md:flex">
<span className={cn("size-1.5 rounded-full", dot[p.tone])} />
{p.name}
</span>
)}
</button>
</li>
);
})}
</ul>
);
} else {
body = (
<div className="flex flex-col items-center rounded-2xl border border-dashed px-6 py-14 text-center">
<span className="flex size-10 items-center justify-center rounded-xl border bg-card text-muted-foreground shadow-xs/5">
<Icon className="size-5" />
</span>
<h2 className="mt-4 font-medium">No {label.toLowerCase()} yet</h2>
<p className="mt-1 max-w-xs text-pretty text-muted-foreground text-xs">
Start one from a project to track progress over time. It will show up here for the whole workspace.
</p>
<Button className="mt-4" size="sm" variant="outline">
<PlusIcon />
New report
</Button>
</div>
);
}
return (
<div className="mx-auto flex w-full max-w-4xl flex-col gap-5 px-4 py-6 @3xl:px-8 @3xl:py-8">
<div>
<h1 className="text-balance font-heading font-semibold text-xl tracking-tight">{label}</h1>
<p className="mt-1 text-muted-foreground">{intro[id] ?? ""}</p>
</div>
{body}
</div>
);
}
export function InsetShell({
workspaces = defaultWorkspaces,
nav = defaultNav,
projects = defaultProjects,
user = defaultUser,
breadcrumbs,
defaultCollapsed = false,
children,
className,
}: InsetShellProps) {
const [collapsed, setCollapsed] = useState(defaultCollapsed);
const [drawer, setDrawer] = useState(false);
const [workspace, setWorkspace] = useState(workspaces[0]?.id ?? "");
const [active, setActive] = useState(`project-${projects[0]?.id ?? ""}`);
const [pageTitle, setPageTitle] = useState(projects[0]?.name ?? "Home");
const drawerRef = useRef<HTMLDivElement>(null);
const ws = workspaces.find((w) => w.id === workspace) ?? workspaces[0];
useEffect(() => {
if (drawer) drawerRef.current?.focus();
}, [drawer]);
const trail = breadcrumbs ?? [ws?.name ?? "Orbit", active.startsWith("project-") ? "Projects" : null, pageTitle].filter(
(x): x is string => Boolean(x),
);
const navigate = (id: string, label: string) => {
setActive(id);
setPageTitle(label);
setDrawer(false);
};
const currentProject = projects.find((p) => `project-${p.id}` === active);
const currentNav = nav.find((n) => n.id === active);
const page = currentProject ? (
<ProjectOverview project={currentProject} />
) : currentNav ? (
<NavPage
icon={currentNav.icon}
id={currentNav.id}
label={currentNav.label}
onOpenProject={(p) => navigate(`project-${p.id}`, p.name)}
projects={projects}
/>
) : null;
const sidebarProps = {
workspaces,
workspace,
onWorkspace: setWorkspace,
nav,
active,
onNavigate: navigate,
projects,
user,
};
return (
<MotionConfig reducedMotion="user">
<TooltipProvider delay={400}>
<div
className={cn(
"@container relative isolate flex h-[760px] w-full overflow-hidden bg-sidebar text-foreground text-sm",
className,
)}
onKeyDown={(e) => {
if (e.key === "Escape" && drawer) setDrawer(false);
}}
>
<motion.aside
animate={{ width: collapsed ? 56 : 248 }}
aria-label="Sidebar"
className="hidden shrink-0 overflow-hidden @3xl:block"
initial={false}
transition={widthSpring}
>
<div className="h-full w-full">
<SidebarBody {...sidebarProps} collapsed={collapsed} />
</div>
</motion.aside>
<AnimatePresence>
{drawer && (
<>
<motion.div
animate={{ opacity: 1 }}
aria-hidden
className="absolute inset-0 z-40 bg-black/32 backdrop-blur-[1px] @3xl:hidden"
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
onClick={() => setDrawer(false)}
/>
<motion.div
animate={{ x: 0 }}
aria-label="Navigation"
aria-modal="true"
className="absolute inset-y-2 left-2 z-50 w-[272px] max-w-[calc(100%-3rem)] overflow-hidden rounded-2xl border bg-sidebar shadow-lg/10 outline-none @3xl:hidden"
exit={{ x: "-110%" }}
initial={{ x: "-110%" }}
ref={drawerRef}
role="dialog"
tabIndex={-1}
transition={spring}
>
<SidebarBody {...sidebarProps} collapsed={false} onClose={() => setDrawer(false)} />
</motion.div>
</>
)}
</AnimatePresence>
<div className="flex min-w-0 flex-1 flex-col @3xl:py-2 @3xl:pe-2">
<main className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background @3xl:rounded-2xl @3xl:border @3xl:shadow-xs/5">
<header className="flex h-13 shrink-0 items-center gap-2 border-b px-3 @3xl:px-4">
<Button
aria-label="Open navigation"
className="@3xl:hidden"
onClick={() => setDrawer(true)}
size="icon-sm"
variant="ghost"
>
<MenuIcon />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-expanded={!collapsed}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
className="hidden @3xl:inline-flex"
onClick={() => setCollapsed((c) => !c)}
size="icon-sm"
variant="ghost"
/>
}
>
<PanelLeftIcon />
</TooltipTrigger>
<TooltipPopup>{collapsed ? "Expand sidebar" : "Collapse sidebar"}</TooltipPopup>
</Tooltip>
<span className="h-4 w-px bg-border" />
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="flex-nowrap gap-1.5 sm:gap-1.5">
{trail.map((crumb, i) => {
const last = i === trail.length - 1;
return (
<motion.li className={cn("flex min-w-0 items-center gap-1.5", !last && "hidden @md:flex")} key={`${i}-${crumb}`} layout="position">
{last ? (
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
initial={{ opacity: 0, y: 6 }}
key={crumb}
transition={spring}
>
<BreadcrumbPage className="truncate font-medium">{crumb}</BreadcrumbPage>
</motion.span>
</AnimatePresence>
) : (
<>
<BreadcrumbLink className="truncate" href="#" onClick={(e) => e.preventDefault()}>
{crumb}
</BreadcrumbLink>
<ChevronRightIcon aria-hidden className="size-3.5 shrink-0 text-muted-foreground/60" />
</>
)}
</motion.li>
);
})}
</BreadcrumbList>
</Breadcrumb>
<div className="flex items-center gap-1.5">
<div className="me-1 hidden -space-x-1 @lg:flex">
{[
["KT", "bg-info/16 text-info-foreground"],
["RL", "bg-orange-500/16 text-orange-600 dark:text-orange-400"],
["TB", "bg-violet-500/20 text-violet-600 dark:text-violet-400"],
].map(([i, t]) => (
<Avatar className="size-6 bg-background ring-2 ring-background" key={i}>
<AvatarFallback className={cn("text-[10px]", t)}>{i}</AvatarFallback>
</Avatar>
))}
</div>
<Button className="hidden @md:inline-flex" size="sm" variant="outline">
<Share2Icon />
Share
</Button>
<Button aria-label="Notifications" className="relative" size="icon-sm" variant="ghost">
<BellIcon />
<span className="absolute top-1.5 right-1.5 size-1.5 rounded-full bg-info" />
</Button>
</div>
</header>
<div className="min-h-0 flex-1 overflow-y-auto" key={children ? undefined : active}>
{children ?? (
<motion.div animate={{ opacity: 1 }} initial={{ opacity: 0 }} transition={{ duration: 0.15, ease: "easeOut" }}>
{page}
</motion.div>
)}
</div>
</main>
</div>
</div>
</TooltipProvider>
</MotionConfig>
);
}