Inbox
block communicationThree-pane mail surface. Left rail: Compose button, folders (Inbox/Starred/Sent/Drafts/Spam/Trash) with unread counts, colour-coded labels with totals. Middle: searchable message list with sender, subject, preview, labels, attachment glyphs, star toggle, smart timestamps. Right: reader pane with subject, from/to, body, attachment chip, reply/forward/archive actions. Stateful demo — swap `mails` for your data source.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/inbox.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/inbox.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/inbox.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/inbox.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/inbox
Examples
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Folder interface Folder {
id: FolderId
label: string
icon: React.ComponentType<{ className?: string }>
} LabelTag interface LabelTag {
id: string
label: string
color: string
} Mail interface Mail {
id: string
folder: FolderId
from: { name: string; email: string; initials: string }
to: string
subject: string
preview: string
body: string
receivedAt: Date
read: boolean
starred: boolean
hasAttachment: boolean
labels: string[]
} npm dependencies
Files (1)
-
components/blocks/Inbox.tsx 28.3 kB
'use client' import * as React from 'react' import { Archive, ArrowLeft, ArrowRight, AtSign, Forward, Inbox as InboxIcon, MoreVertical, Paperclip, Pencil, Reply, ReplyAll, Search, Send, Smile, Star, Tag, Trash2, AlertOctagon, FileText, Bookmark, } from 'lucide-react' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' type ReplyMode = 'reply' | 'reply-all' | 'forward' type FolderId = 'inbox' | 'starred' | 'sent' | 'drafts' | 'spam' | 'trash' interface Folder { id: FolderId label: string icon: React.ComponentType<{ className?: string }> } interface LabelTag { id: string label: string color: string } interface Mail { id: string folder: FolderId from: { name: string; email: string; initials: string } to: string subject: string preview: string body: string receivedAt: Date read: boolean starred: boolean hasAttachment: boolean labels: string[] } const folders: Folder[] = [ { id: 'inbox', label: 'Inbox', icon: InboxIcon }, { id: 'starred', label: 'Starred', icon: Star }, { id: 'sent', label: 'Sent', icon: Send }, { id: 'drafts', label: 'Drafts', icon: FileText }, { id: 'spam', label: 'Spam', icon: AlertOctagon }, { id: 'trash', label: 'Trash', icon: Trash2 }, ] const labels: LabelTag[] = [ { id: 'work', label: 'Work', color: 'bg-primary' }, { id: 'personal', label: 'Personal', color: 'bg-success' }, { id: 'finance', label: 'Finance', color: 'bg-info' }, { id: 'travel', label: 'Travel', color: 'bg-warning' }, ] const now = new Date() const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000) const defaultMails: Mail[] = [ { id: 'e1', folder: 'inbox', from: { name: 'Sarah Connor', email: '[email protected]', initials: 'SC' }, to: '[email protected]', subject: 'Q2 OKR draft — your sign-off needed', preview: 'Pulled together the Q2 OKR draft from the leadership offsite. I have flagged the two areas where we need…', body: `Hi,\n\nPulled together the Q2 OKR draft from the leadership offsite. I have flagged the two areas where we need your call before we lock it:\n\n• Hiring plan: 4 vs 6 ENG roles (page 3)\n• EU rollout timing: April vs June (page 7)\n\nGoing to share with the rest of the leadership team Wednesday EOD, so anything from you by Tuesday is great.\n\nThanks,\nSarah`, receivedAt: new Date(now.getTime() - 18 * 60 * 1000), read: false, starred: true, hasAttachment: true, labels: ['work'], }, { id: 'e2', folder: 'inbox', from: { name: 'Marcus Rivera', email: '[email protected]', initials: 'MR' }, to: '[email protected]', subject: 'Re: Dashboard review tomorrow', preview: 'Yes! Pushing to 10:30 so we can join from the standup. I will own the slides and pull the latest kanban metrics.', body: `Yes! Pushing to 10:30 so we can join from the standup. I will own the slides and pull the latest kanban metrics.\n\nFew things I want to cover:\n - Throughput delta vs last sprint\n - The two stories we de-scoped\n - Q2 capacity ask\n\nSee you tomorrow,\nMarcus`, receivedAt: new Date(now.getTime() - 95 * 60 * 1000), read: false, starred: false, hasAttachment: false, labels: ['work'], }, { id: 'e3', folder: 'inbox', from: { name: 'Stripe', email: '[email protected]', initials: 'St' }, to: '[email protected]', subject: 'Your invoice from Acme Inc. — $1,290.00', preview: 'Thanks for using Stripe. Your invoice for the period Mar 1 – Mar 31 is attached. Total due: $1,290.00…', body: 'Thanks for using Stripe. Your invoice for the period Mar 1 – Mar 31 is attached. Total due: $1,290.00.', receivedAt: new Date(now.getTime() - 4 * 60 * 60 * 1000), read: true, starred: false, hasAttachment: true, labels: ['finance'], }, { id: 'e4', folder: 'inbox', from: { name: 'Linear', email: '[email protected]', initials: 'Li' }, to: '[email protected]', subject: 'Weekly summary: 14 issues closed, 22 opened', preview: 'Good news — the team closed 14 issues this week, up 3 from last week. Top contributors: Marcus, Priya, Devon.', body: 'Good news — the team closed 14 issues this week, up 3 from last week. Top contributors: Marcus, Priya, Devon.', receivedAt: new Date(now.getTime() - 9 * 60 * 60 * 1000), read: true, starred: false, hasAttachment: false, labels: ['work'], }, { id: 'e5', folder: 'inbox', from: { name: 'Priya Shah', email: '[email protected]', initials: 'PS' }, to: '[email protected]', subject: 'Color tokens are live in staging', preview: 'Pushed the new OKLCH token set to staging. All chart colors, success/warning/info, sidebar variants. Take a look…', body: 'Pushed the new OKLCH token set to staging. All chart colors, success/warning/info, sidebar variants. Take a look when you have a sec.', receivedAt: yesterday, read: true, starred: true, hasAttachment: false, labels: ['work'], }, { id: 'e6', folder: 'inbox', from: { name: 'United Airlines', email: '[email protected]', initials: 'UA' }, to: '[email protected]', subject: 'Your itinerary: SFO → LHR, departing Apr 12', preview: 'Your reservation is confirmed. Check-in opens 24h before departure. Booking reference: 7HQ4P2.', body: 'Your reservation is confirmed. Check-in opens 24h before departure. Booking reference: 7HQ4P2.', receivedAt: new Date(now.getTime() - 38 * 60 * 60 * 1000), read: true, starred: false, hasAttachment: true, labels: ['travel'], }, { id: 'e7', folder: 'inbox', from: { name: 'Mom', email: '[email protected]', initials: 'M' }, to: '[email protected]', subject: 'Sunday lunch?', preview: 'Are you free for lunch this Sunday? Dad is making his lasagne. Let me know!', body: 'Are you free for lunch this Sunday? Dad is making his lasagne. Let me know!', receivedAt: new Date(now.getTime() - 55 * 60 * 60 * 1000), read: true, starred: false, hasAttachment: false, labels: ['personal'], }, ] export interface InboxProps { messages?: Mail[] } function formatMailTime(d: Date): string { if (d >= todayStart) return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) if (d.toDateString() === yesterday.toDateString()) return 'Yesterday' const diffDays = Math.floor((now.getTime() - d.getTime()) / (24 * 60 * 60 * 1000)) if (diffDays < 7) return d.toLocaleDateString(undefined, { weekday: 'short' }) return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) } function formatFullTime(d: Date): string { return d.toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }) } function labelMeta(id: string): LabelTag | undefined { return labels.find((l) => l.id === id) } export function Inbox({ messages = defaultMails }: InboxProps) { const [mails, setMails] = React.useState<Mail[]>(messages) const [activeFolder, setActiveFolder] = React.useState<FolderId>('inbox') const [activeLabel, setActiveLabel] = React.useState<string | null>(null) const [activeId, setActiveId] = React.useState<string>('e1') const [search, setSearch] = React.useState('') const [replyMode, setReplyMode] = React.useState<ReplyMode>('reply') const [replyDraft, setReplyDraft] = React.useState('') const [justSent, setJustSent] = React.useState(false) function unreadCount(id: FolderId): number { if (id === 'starred') return mails.filter((m) => m.starred && !m.read && m.folder !== 'trash').length return mails.filter((m) => m.folder === id && !m.read).length } const labelCounts = React.useMemo( () => labels.reduce<Record<string, number>>((acc, l) => { acc[l.id] = mails.filter((m) => m.labels.includes(l.id)).length return acc }, {}), [mails], ) const visibleMails = React.useMemo(() => { const q = search.trim().toLowerCase() let list = mails.filter((m) => { if (activeLabel) return m.labels.includes(activeLabel) if (activeFolder === 'starred') return m.starred && m.folder !== 'trash' return m.folder === activeFolder }) if (q) { list = list.filter( (m) => m.subject.toLowerCase().includes(q) || m.from.name.toLowerCase().includes(q) || m.preview.toLowerCase().includes(q), ) } return [...list].sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime()) }, [mails, search, activeLabel, activeFolder]) const activeMail = React.useMemo(() => mails.find((m) => m.id === activeId) ?? null, [mails, activeId]) function selectMail(id: string) { setActiveId(id) setMails((prev) => prev.map((m) => (m.id === id ? { ...m, read: true } : m))) } function selectFolder(id: FolderId) { setActiveFolder(id) setActiveLabel(null) const list = [...mails] .filter((m) => (id === 'starred' ? m.starred && m.folder !== 'trash' : m.folder === id)) .sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime()) const first = list[0] if (first) setActiveId(first.id) } function selectLabel(id: string) { setActiveLabel(id) const list = [...mails] .filter((m) => m.labels.includes(id)) .sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime()) const first = list[0] if (first) setActiveId(first.id) } function toggleStar(id: string, e?: React.MouseEvent) { e?.stopPropagation() setMails((prev) => prev.map((m) => (m.id === id ? { ...m, starred: !m.starred } : m))) } function archive(id: string) { setMails((prev) => { const next = prev.filter((m) => m.id !== id) const visible = [...next] .filter((m) => { if (activeLabel) return m.labels.includes(activeLabel) if (activeFolder === 'starred') return m.starred && m.folder !== 'trash' return m.folder === activeFolder }) .sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime()) const nextActive = visible[0] if (nextActive) setActiveId(nextActive.id) return next }) } const replyRecipient = React.useMemo(() => { const m = activeMail if (!m) return '' if (replyMode === 'forward') return '' return m.from.name + ' <' + m.from.email + '>' }, [activeMail, replyMode]) const replyPlaceholder = replyMode === 'forward' ? 'Add a note before forwarding' : 'Write a reply' const sendDisabled = !replyDraft.trim() || (replyMode === 'forward' && false) function changeReplyMode(mode: ReplyMode) { setReplyMode(mode) setJustSent(false) } function sendReply() { const body = replyDraft.trim() if (!body) return setReplyDraft('') setJustSent(true) window.setTimeout(() => { setJustSent(false) }, 1800) } function onReplyKey(e: React.KeyboardEvent) { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault() sendReply() } } React.useEffect(() => { setReplyDraft('') setReplyMode('reply') setJustSent(false) }, [activeId]) return ( <div className="bg-card text-card-foreground grid h-[680px] w-full grid-cols-[220px_340px_1fr] overflow-hidden rounded-xl border shadow-sm"> <aside className="bg-muted/30 flex min-w-0 flex-col border-r"> <div className="flex h-14 shrink-0 items-center border-b px-3"> <Button className="h-9 w-full justify-start gap-2 rounded-lg" size="sm"> <Pencil className="size-4" /> Compose </Button> </div> <nav className="flex-1 space-y-1 overflow-y-auto px-3 pt-3 pb-3"> <p className="text-muted-foreground px-2 pb-1 text-[10px] font-semibold tracking-widest uppercase">Folders</p> {folders.map((f) => ( <button key={f.id} className={cn( 'flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors', activeFolder === f.id && !activeLabel ? 'bg-primary/10 text-primary font-medium' : 'hover:bg-muted/60 text-foreground/80', )} onClick={() => selectFolder(f.id)} > <span className="flex min-w-0 items-center gap-2"> <f.icon className="size-4 shrink-0" /> <span className="truncate">{f.label}</span> </span> {unreadCount(f.id) > 0 && ( <span className="text-muted-foreground shrink-0 text-[10px] tabular-nums">{unreadCount(f.id)}</span> )} </button> ))} <p className="text-muted-foreground mt-4 px-2 pb-1 text-[10px] font-semibold tracking-widest uppercase"> Labels </p> {labels.map((l) => ( <button key={l.id} className={cn( 'flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors', activeLabel === l.id ? 'bg-primary/10 text-primary font-medium' : 'hover:bg-muted/60 text-foreground/80', )} onClick={() => selectLabel(l.id)} > <span className="flex min-w-0 items-center gap-2"> <span className={cn('size-2.5 shrink-0 rounded-full', l.color)} /> <span className="truncate">{l.label}</span> </span> <span className="text-muted-foreground shrink-0 text-[10px] tabular-nums">{labelCounts[l.id] ?? 0}</span> </button> ))} </nav> </aside> <section className="bg-background flex min-w-0 flex-col border-r"> <div className="flex h-14 shrink-0 items-center gap-2 border-b px-3"> <div className="relative flex-1"> <Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search mail" className="h-8 rounded-lg pl-8 text-xs" /> </div> <Button variant="ghost" size="icon" className="size-8" aria-label="Filter"> <Tag className="size-4" /> </Button> </div> <div className="flex items-center justify-between px-4 pt-3 pb-2"> <h2 className="text-sm font-semibold tracking-tight capitalize"> {activeLabel ? labelMeta(activeLabel)?.label : activeFolder} </h2> <span className="text-muted-foreground text-[10px] tabular-nums"> {visibleMails.length} message{visibleMails.length === 1 ? '' : 's'} </span> </div> <div className="list-scroll flex-1 overflow-y-auto pb-3"> {visibleMails.map((m) => ( <button key={m.id} className={cn( 'group relative w-full px-4 py-3 text-left transition-colors', m.id === activeId ? 'bg-primary/5' : 'hover:bg-muted/60', )} onClick={() => selectMail(m.id)} > {m.id === activeId && ( <span className="bg-primary absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full" /> )} <div className="flex items-start gap-3"> <Avatar className="mt-0.5 size-8 shrink-0"> <AvatarFallback className="text-[11px]">{m.from.initials}</AvatarFallback> </Avatar> <div className="min-w-0 flex-1"> <div className="flex items-center justify-between gap-2"> <p className={cn('truncate text-[13px]', m.read ? 'font-medium' : 'font-semibold')}> {m.from.name} </p> <span className="text-muted-foreground shrink-0 text-[10px] tabular-nums"> {formatMailTime(m.receivedAt)} </span> </div> <p className={cn('mt-0.5 truncate text-xs', m.read ? 'text-foreground/80' : 'font-semibold')}> {m.subject} </p> <p className="text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed">{m.preview}</p> <div className="mt-1.5 flex items-center gap-1.5"> {m.labels.map((lid) => ( <span key={lid} className="text-muted-foreground bg-muted inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px]" > <span className={cn('size-1.5 rounded-full', labelMeta(lid)?.color)} /> {labelMeta(lid)?.label} </span> ))} {m.hasAttachment && <Paperclip className="text-muted-foreground size-3" />} {!m.read && <span className="bg-primary ml-auto size-1.5 shrink-0 rounded-full" />} </div> </div> <button className="text-muted-foreground hover:text-warning shrink-0 transition-colors" title={m.starred ? 'Unstar' : 'Star'} onClick={(e) => toggleStar(m.id, e)} > <Star className={cn('size-3.5', m.starred ? 'fill-warning text-warning' : '')} /> </button> </div> </button> ))} {visibleMails.length === 0 && ( <div className="flex flex-col items-center justify-center px-6 py-16 text-center"> <div className="bg-muted mb-3 rounded-full p-3"> <InboxIcon className="text-muted-foreground size-5" /> </div> <p className="text-sm font-medium">Nothing here</p> <p className="text-muted-foreground mt-0.5 text-xs">No messages match the current filter.</p> </div> )} </div> </section> <section className="bg-background flex min-w-0 flex-col"> {activeMail ? ( <> <div className="flex h-14 shrink-0 items-center justify-between gap-2 border-b px-4"> <div className="flex items-center gap-1"> <Button variant="ghost" size="icon" className="size-8" aria-label="Back"> <ArrowLeft className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="Archive" onClick={() => archive(activeMail.id)} > <Archive className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="Delete"> <Trash2 className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="Save"> <Bookmark className="size-4" /> </Button> </div> <div className="flex items-center gap-1"> <Button variant="ghost" size="icon" className="size-8" aria-label="Reply"> <Reply className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="Reply all"> <ReplyAll className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="Forward"> <Forward className="size-4" /> </Button> <Button variant="ghost" size="icon" className="size-8" aria-label="More options"> <MoreVertical className="size-4" /> </Button> </div> </div> <div className="reader-scroll flex-1 overflow-y-auto"> <div className="px-6 pt-5 pb-3"> <h1 className="text-foreground text-xl leading-tight font-semibold tracking-tight"> {activeMail.subject} </h1> <div className="mt-2 flex flex-wrap items-center gap-2"> {activeMail.labels.map((lid) => ( <span key={lid} className="text-foreground/70 bg-muted inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px]" > <span className={cn('size-1.5 rounded-full', labelMeta(lid)?.color)} /> {labelMeta(lid)?.label} </span> ))} </div> </div> <div className="flex items-start gap-3 px-6 pb-4"> <Avatar className="size-10"> <AvatarFallback>{activeMail.from.initials}</AvatarFallback> </Avatar> <div className="min-w-0 flex-1"> <div className="flex flex-wrap items-baseline gap-x-2"> <p className="text-sm font-semibold">{activeMail.from.name}</p> <p className="text-muted-foreground text-xs"><{activeMail.from.email}></p> </div> <p className="text-muted-foreground text-xs">to {activeMail.to}</p> </div> <div className="text-muted-foreground flex shrink-0 items-center gap-2 text-xs"> <span>{formatFullTime(activeMail.receivedAt)}</span> <button className="hover:text-warning transition-colors" title={activeMail.starred ? 'Unstar' : 'Star'} onClick={() => toggleStar(activeMail.id)} > <Star className={cn('size-4', activeMail.starred ? 'fill-warning text-warning' : '')} /> </button> </div> </div> <article className="text-foreground/90 px-6 pb-6 text-sm leading-relaxed whitespace-pre-wrap"> {activeMail.body} </article> {activeMail.hasAttachment && ( <div className="px-6 pb-6"> <div className="bg-muted/40 flex items-center gap-3 rounded-lg border px-3 py-2.5"> <div className="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> <FileText className="size-4" /> </div> <div className="min-w-0 flex-1"> <p className="truncate text-xs font-medium">attachment.pdf</p> <p className="text-muted-foreground text-[10px]">142 KB · PDF</p> </div> <Button variant="ghost" size="sm" className="h-7 text-xs"> Download </Button> </div> </div> )} </div> <div className="bg-muted/20 border-t px-4 pt-3 pb-3"> <div className="mb-2 flex items-center gap-1"> <button className={cn( 'inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-[11px] font-medium transition-colors', replyMode === 'reply' ? 'bg-background text-foreground border-input/60 border shadow-sm' : 'text-muted-foreground hover:bg-background/60 hover:text-foreground', )} onClick={() => changeReplyMode('reply')} > <Reply className="size-3.5" /> Reply </button> <button className={cn( 'inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-[11px] font-medium transition-colors', replyMode === 'reply-all' ? 'bg-background text-foreground border-input/60 border shadow-sm' : 'text-muted-foreground hover:bg-background/60 hover:text-foreground', )} onClick={() => changeReplyMode('reply-all')} > <ReplyAll className="size-3.5" /> Reply all </button> <button className={cn( 'inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-[11px] font-medium transition-colors', replyMode === 'forward' ? 'bg-background text-foreground border-input/60 border shadow-sm' : 'text-muted-foreground hover:bg-background/60 hover:text-foreground', )} onClick={() => changeReplyMode('forward')} > <Forward className="size-3.5" /> Forward </button> </div> <div className="bg-background border-input/60 focus-within:border-primary/40 focus-within:ring-primary/10 group flex flex-col overflow-hidden rounded-2xl border shadow-sm transition-all duration-150 focus-within:shadow-md focus-within:ring-4"> {replyRecipient && ( <p className="border-input/40 text-muted-foreground border-b px-4 pt-2.5 pb-2 text-[11px]"> <span className="text-foreground/70 font-medium">To:</span> {replyRecipient} </p> )} <textarea value={replyDraft} onChange={(e) => setReplyDraft(e.target.value)} placeholder={replyPlaceholder} rows={1} className="placeholder:text-muted-foreground/70 max-h-40 min-h-[44px] resize-none border-0 bg-transparent px-4 pt-3 pb-1 text-sm leading-relaxed shadow-none outline-none focus-visible:ring-0" onKeyDown={onReplyKey} /> <div className="flex items-center justify-between gap-2 px-2 pb-2"> <div className="flex items-center gap-0.5"> <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground size-8" aria-label="Attach file" > <Paperclip className="size-4" /> </Button> <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground size-8" aria-label="Emoji" > <Smile className="size-4" /> </Button> <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground size-8" aria-label="Mention" > <AtSign className="size-4" /> </Button> </div> <div className="flex items-center gap-2"> <kbd className="bg-muted text-muted-foreground hidden h-5 items-center gap-0.5 rounded border px-1.5 font-mono text-[10px] sm:inline-flex"> <span>⌘</span> <span>↵</span> </kbd> <Button className="h-8 gap-1.5 rounded-lg px-3 text-xs font-medium" disabled={sendDisabled} aria-label="Send" onClick={sendReply} > Send <ArrowRight className="size-3.5" /> </Button> </div> </div> </div> {justSent && <p className="text-success mt-2 text-center text-[11px] font-medium">Reply sent</p>} </div> </> ) : ( <div className="flex flex-1 items-center justify-center"> <div className="text-center"> <div className="bg-muted mx-auto mb-3 size-12 rounded-full p-3"> <InboxIcon className="text-muted-foreground size-6" /> </div> <p className="text-sm font-medium">No message selected</p> <p className="text-muted-foreground mt-1 text-xs">Pick a message from the list to read it here.</p> </div> </div> )} </section> </div> ) }
Raw manifest: https://react.uipkge.dev/r/react/inbox.json