Launch retro · Oct 2
Right-click a block, or focus it and press Shift+F10, to transform, insert, or reorder.
4 blocks
Free, copy-and-go Context Menu components built on the SevenUI Context Menu primitive.Read the primitive docs.
TSX"use client"; import { ClipboardPaste, Copy, CopyPlus, Scissors, Trash2 } from "lucide-react"; import type * as React from "react"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } const clipboardActions = [ { label: "Cut", icon: Scissors, shortcut: "⌘X" }, { label: "Copy", icon: Copy, shortcut: "⌘C" }, { label: "Paste", icon: ClipboardPaste, shortcut: "⌘V" }, ]; export default function ContextMenu01() { return ( <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label="Hero banner layer. Right-click or press Shift+F10 for actions." className="flex aspect-video w-full max-w-sm flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-border bg-muted/40 p-6 text-center outline-none focus-visible:ring-3 focus-visible:ring-ring/50" > <span className="text-sm font-medium">Hero banner</span> <span className="text-xs text-muted-foreground"> Right-click the layer or press Shift+F10 </span> </ContextMenuTrigger> <ContextMenuContent className="w-52"> <ContextMenuGroup> {clipboardActions.map(({ label, icon: Icon, shortcut }) => ( <ContextMenuItem key={label}> <Icon aria-hidden="true" /> {label} <ContextMenuShortcut>{shortcut}</ContextMenuShortcut> </ContextMenuItem> ))} <ContextMenuItem> <CopyPlus aria-hidden="true" /> Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut> </ContextMenuItem> </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuItem variant="destructive"> <Trash2 aria-hidden="true" /> Delete layer <ContextMenuShortcut>⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> ); }
npx shadcn@latest add @sevenui/component/context-menu-01pnpm dlx shadcn@latest add @sevenui/component/context-menu-01yarn dlx shadcn@latest add @sevenui/component/context-menu-01bunx --bun shadcn@latest add @sevenui/component/context-menu-01Showing pixel grid, rulers, snap to grid
TSX"use client"; import * as React from "react"; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type ViewOption = "grid" | "rulers" | "guides" | "snap"; const options: { key: ViewOption; label: string; shortcut: string }[] = [ { key: "grid", label: "Pixel grid", shortcut: "⌘'" }, { key: "rulers", label: "Rulers", shortcut: "⇧R" }, { key: "guides", label: "Layout guides", shortcut: "⌘G" }, { key: "snap", label: "Snap to grid", shortcut: "⇧S" }, ]; export default function ContextMenu02() { const [view, setView] = React.useState<Record<ViewOption, boolean>>({ grid: true, rulers: true, guides: false, snap: true, }); const active = options.filter((option) => view[option.key]); return ( <div className="flex w-full max-w-md flex-col gap-2"> <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label="Artboard. Right-click or press Shift+F10 for view options." className="relative flex aspect-[4/3] w-full overflow-hidden rounded-xl border border-border bg-card outline-none focus-visible:ring-3 focus-visible:ring-ring/50" > {view.rulers && ( <> <div aria-hidden="true" className="absolute inset-x-0 top-0 h-4 border-b border-border bg-[repeating-linear-gradient(90deg,var(--color-border)_0_1px,transparent_1px_12px)] bg-muted" /> <div aria-hidden="true" className="absolute inset-y-0 left-0 w-4 border-r border-border bg-[repeating-linear-gradient(0deg,var(--color-border)_0_1px,transparent_1px_12px)] bg-muted" /> </> )} <div className={ view.rulers ? "absolute inset-0 top-4 left-4" : "absolute inset-0" } > {view.grid && ( <div aria-hidden="true" className="absolute inset-0 bg-[radial-gradient(var(--color-border)_1px,transparent_1px)] bg-size-[14px_14px]" /> )} {view.guides && ( <div aria-hidden="true" className="absolute inset-y-0 left-1/2 w-px bg-chart-1/60" /> )} <div className="absolute top-1/2 left-1/2 flex h-16 w-28 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border border-border bg-background text-xs font-medium shadow-sm"> Sign-up card </div> </div> </ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuGroup> <ContextMenuLabel>View</ContextMenuLabel> {options.map((option) => ( <ContextMenuCheckboxItem key={option.key} checked={view[option.key]} closeOnClick={false} onCheckedChange={(checked) => setView((prev) => ({ ...prev, [option.key]: checked })) } > {option.label} <ContextMenuShortcut className="mr-4"> {option.shortcut} </ContextMenuShortcut> </ContextMenuCheckboxItem> ))} </ContextMenuGroup> <ContextMenuSeparator /> <p className="px-1.5 py-1 text-xs text-muted-foreground"> Changes apply instantly; the menu stays open. </p> </ContextMenuContent> </ContextMenu> <p className="text-xs text-muted-foreground" aria-live="polite"> {active.length > 0 ? `Showing ${active.map((option) => option.label.toLowerCase()).join(", ")}` : "All view aids hidden"} </p> </div> ); }
npx shadcn@latest add @sevenui/component/context-menu-02pnpm dlx shadcn@latest add @sevenui/component/context-menu-02yarn dlx shadcn@latest add @sevenui/component/context-menu-02bunx --bun shadcn@latest add @sevenui/component/context-menu-02Audit onboarding emails for the Q4 pricing change
Right-click to relabel · Due Oct 14
TSX"use client"; import { SignalHigh, SignalLow, SignalMedium } from "lucide-react"; import * as React from "react"; import { ContextMenu, ContextMenuContent, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } const labels = [ { value: "research", name: "Research", swatch: "bg-chart-1" }, { value: "design", name: "Design", swatch: "bg-chart-2" }, { value: "engineering", name: "Engineering", swatch: "bg-chart-3" }, { value: "marketing", name: "Marketing", swatch: "bg-chart-4" }, { value: "ops", name: "Operations", swatch: "bg-chart-5" }, ]; const priorities = [ { value: "high", name: "High", icon: SignalHigh }, { value: "medium", name: "Medium", icon: SignalMedium }, { value: "low", name: "Low", icon: SignalLow }, ]; export default function ContextMenu03() { const [label, setLabel] = React.useState("design"); const [priority, setPriority] = React.useState("medium"); const currentLabel = labels.find((item) => item.value === label) ?? labels[0]; const currentPriority = priorities.find((item) => item.value === priority) ?? priorities[1]; const PriorityIcon = currentPriority.icon; return ( <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label="Task card. Right-click or press Shift+F10 to change label and priority." className="flex w-full max-w-xs flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground shadow-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/50" > <div className="flex items-center justify-between gap-2 text-xs"> <span className="inline-flex items-center gap-1.5 rounded-full border border-border px-2 py-0.5 font-medium"> <span aria-hidden="true" className={`size-2 rounded-full ${currentLabel.swatch}`} /> {currentLabel.name} </span> <span className="inline-flex items-center gap-1 text-muted-foreground"> <PriorityIcon aria-hidden="true" className="size-3.5" /> {currentPriority.name} </span> </div> <p className="text-sm leading-snug font-medium"> Audit onboarding emails for the Q4 pricing change </p> <p className="text-xs text-muted-foreground"> Right-click to relabel · Due Oct 14 </p> </ContextMenuTrigger> <ContextMenuContent className="w-52"> <ContextMenuRadioGroup value={label} onValueChange={setLabel}> <ContextMenuLabel>Label</ContextMenuLabel> {labels.map((item) => ( <ContextMenuRadioItem key={item.value} value={item.value}> <span aria-hidden="true" className={`size-2.5 rounded-full ring-2 ring-background ${item.swatch}`} /> {item.name} </ContextMenuRadioItem> ))} </ContextMenuRadioGroup> <ContextMenuSeparator /> <ContextMenuRadioGroup value={priority} onValueChange={setPriority}> <ContextMenuLabel>Priority</ContextMenuLabel> {priorities.map(({ value, name, icon: Icon }) => ( <ContextMenuRadioItem key={value} value={value}> <Icon aria-hidden="true" className="text-muted-foreground" /> {name} </ContextMenuRadioItem> ))} </ContextMenuRadioGroup> </ContextMenuContent> </ContextMenu> ); }
npx shadcn@latest add @sevenui/component/context-menu-03pnpm dlx shadcn@latest add @sevenui/component/context-menu-03yarn dlx shadcn@latest add @sevenui/component/context-menu-03bunx --bun shadcn@latest add @sevenui/component/context-menu-032025 hiring plan
Shared by Priya Nair · Can view
TSX"use client"; import { Download, Eye, Link, Lock, Pencil, Trash2, UserPlus, } from "lucide-react"; import * as React from "react"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } const editActions = [ { label: "Edit document", icon: Pencil, shortcut: "E" }, { label: "Invite collaborators", icon: UserPlus, shortcut: "⌘I" }, ]; export default function ContextMenu04() { const [readOnly, setReadOnly] = React.useState(true); const switchId = React.useId(); return ( <div className="flex w-full max-w-sm flex-col gap-4"> <div className="flex items-center justify-between gap-3"> <Label htmlFor={switchId}>View-only access</Label> <Switch id={switchId} checked={readOnly} onCheckedChange={setReadOnly} /> </div> <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label="2025 hiring plan document. Right-click or press Shift+F10 for actions." className="flex items-center gap-3 rounded-lg border border-border bg-card p-3 outline-none focus-visible:ring-3 focus-visible:ring-ring/50" > <div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"> {readOnly ? ( <Lock aria-hidden="true" className="size-4" /> ) : ( <Pencil aria-hidden="true" className="size-4" /> )} </div> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium">2025 hiring plan</p> <p className="truncate text-xs text-muted-foreground"> Shared by Priya Nair · {readOnly ? "Can view" : "Can edit"} </p> </div> </ContextMenuTrigger> <ContextMenuContent className="w-60"> {readOnly && ( <> <p className="flex items-start gap-1.5 px-1.5 py-1 text-xs text-muted-foreground"> <Eye aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" /> You have view-only access. Ask Priya to change it. </p> <ContextMenuSeparator /> </> )} <ContextMenuGroup> <ContextMenuItem> <Link aria-hidden="true" /> Copy link <ContextMenuShortcut>⌘L</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem> <Download aria-hidden="true" /> Download as PDF </ContextMenuItem> </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuGroup> {editActions.map(({ label, icon: Icon, shortcut }) => ( <ContextMenuItem key={label} disabled={readOnly}> <Icon aria-hidden="true" /> {label} {readOnly ? ( <Lock aria-hidden="true" className="ml-auto size-3.5 text-muted-foreground" /> ) : ( <ContextMenuShortcut>{shortcut}</ContextMenuShortcut> )} </ContextMenuItem> ))} <ContextMenuItem variant="destructive" disabled={readOnly}> <Trash2 aria-hidden="true" /> Move to trash {readOnly && ( <Lock aria-hidden="true" className="ml-auto size-3.5 text-muted-foreground" /> )} </ContextMenuItem> </ContextMenuGroup> {readOnly && ( <> <ContextMenuSeparator /> <ContextMenuItem> <UserPlus aria-hidden="true" /> Request edit access </ContextMenuItem> </> )} </ContextMenuContent> </ContextMenu> </div> ); }
npx shadcn@latest add @sevenui/component/context-menu-04pnpm dlx shadcn@latest add @sevenui/component/context-menu-04yarn dlx shadcn@latest add @sevenui/component/context-menu-04bunx --bun shadcn@latest add @sevenui/component/context-menu-043 songs · 0 queued
Right-click a song, or focus it and press Shift+F10.
TSX"use client"; import { Disc3, Heart, ListEnd, ListMusic, ListPlus, ListStart, MicVocal, Play, Plus, RotateCcw, Trash2, } from "lucide-react"; import * as React from "react"; import { cn } from "cn"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Track = { id: string; title: string; artist: string; album: string; duration: string; liked: boolean; playlists: string[]; }; const playlists = ["Deep work", "Sunday drive", "Run club 5K"]; const initialTracks: Track[] = [ { id: "t1", title: "Glass Harbor", artist: "Lune Avenue", album: "Low Tide Lights", duration: "3:42", liked: true, playlists: ["Deep work"], }, { id: "t2", title: "Paper Satellites", artist: "The Quiet Arcade", album: "Orbit Songs", duration: "4:05", liked: false, playlists: [], }, { id: "t3", title: "Northbound", artist: "Mara Solis", album: "Northbound", duration: "2:58", liked: false, playlists: ["Sunday drive"], }, ]; export default function ContextMenu05() { const [tracks, setTracks] = React.useState(initialTracks); const [playingId, setPlayingId] = React.useState("t1"); const [queue, setQueue] = React.useState<string[]>([]); const [status, setStatus] = React.useState(""); function patch(id: string, next: Partial<Track>) { setTracks((current) => current.map((track) => (track.id === id ? { ...track, ...next } : track)), ); } function enqueue(track: Track, position: "next" | "last") { setQueue((current) => { const rest = current.filter((id) => id !== track.id); return position === "next" ? [track.id, ...rest] : [...rest, track.id]; }); setStatus( position === "next" ? `${track.title} plays next.` : `${track.title} added to the end of the queue.`, ); } return ( <section aria-labelledby="context-menu-05-title" className="w-full max-w-md rounded-xl border bg-card text-card-foreground" > <header className="flex items-center gap-3 border-b p-4"> <div className="flex size-12 shrink-0 items-center justify-center rounded-md bg-muted"> <ListMusic aria-hidden="true" className="size-5 text-muted-foreground" /> </div> <div className="min-w-0"> <h3 id="context-menu-05-title" className="truncate text-sm font-semibold" > Late night coding </h3> <p className="text-xs text-muted-foreground tabular-nums"> {tracks.length} songs · {queue.length} queued </p> </div> </header> {tracks.length === 0 ? ( <div className="flex flex-col items-center gap-3 px-4 py-10 text-center"> <p className="text-sm text-muted-foreground"> This playlist is empty. Add songs from search or your library. </p> <Button variant="outline" size="sm" onClick={() => { setTracks(initialTracks); setPlayingId("t1"); setStatus("Playlist restored."); }} > <RotateCcw aria-hidden="true" /> Restore songs </Button> </div> ) : ( <ol className="p-1.5"> {tracks.map((track, index) => { const playing = track.id === playingId; return ( <li key={track.id}> <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`${track.title} by ${track.artist}, ${track.duration}${playing ? ", now playing" : ""}${track.liked ? ", liked" : ""}`} className="flex items-center gap-3 rounded-md px-2.5 py-2 outline-none transition-colors hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:bg-muted" > <span className="w-4 shrink-0 text-center text-xs text-muted-foreground tabular-nums"> {playing ? ( <Play aria-hidden="true" className="size-3.5 fill-current text-primary" /> ) : ( index + 1 )} </span> <img src="/placeholder.svg" alt="" className="size-9 shrink-0 rounded-sm bg-muted object-cover" /> <span className="flex min-w-0 flex-1 flex-col"> <span className={cn( "truncate text-sm font-medium", playing && "text-primary", )} > {track.title} </span> <span className="truncate text-xs text-muted-foreground"> {track.artist} </span> </span> {track.liked ? ( <Heart aria-hidden="true" className="size-3.5 shrink-0 fill-current text-primary" /> ) : null} <span className="shrink-0 text-xs text-muted-foreground tabular-nums"> {track.duration} </span> </ContextMenuTrigger> <ContextMenuContent className="w-60"> <div className="flex items-center gap-2.5 px-1.5 py-1.5"> <img src="/placeholder.svg" alt="" className="size-10 shrink-0 rounded-sm bg-muted object-cover" /> <span className="flex min-w-0 flex-col"> <span className="truncate text-sm font-medium"> {track.title} </span> <span className="truncate text-xs text-muted-foreground"> {track.artist} · {track.album} </span> </span> </div> <ContextMenuSeparator /> <ContextMenuItem disabled={playing} onClick={() => { setPlayingId(track.id); setStatus(`Now playing ${track.title}.`); }} > <Play aria-hidden="true" /> {playing ? "Now playing" : "Play"} </ContextMenuItem> <ContextMenuItem disabled={playing} onClick={() => enqueue(track, "next")} > <ListStart aria-hidden="true" /> Play next </ContextMenuItem> <ContextMenuItem disabled={playing} onClick={() => enqueue(track, "last")} > <ListEnd aria-hidden="true" /> Add to queue </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuCheckboxItem checked={track.liked} onCheckedChange={(checked) => { patch(track.id, { liked: checked }); setStatus( checked ? `${track.title} saved to Liked songs.` : `${track.title} removed from Liked songs.`, ); }} > <Heart aria-hidden="true" /> Save to Liked songs </ContextMenuCheckboxItem> <ContextMenuSub> <ContextMenuSubTrigger> <ListPlus aria-hidden="true" /> Add to playlist </ContextMenuSubTrigger> <ContextMenuSubContent className="w-48"> <ContextMenuItem onClick={() => setStatus( `New playlist started with ${track.title}.`, ) } > <Plus aria-hidden="true" /> New playlist </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuGroup> <ContextMenuLabel>Your playlists</ContextMenuLabel> {playlists.map((playlist) => ( <ContextMenuCheckboxItem key={playlist} closeOnClick={false} checked={track.playlists.includes(playlist)} onCheckedChange={(checked) => patch(track.id, { playlists: checked ? [...track.playlists, playlist] : track.playlists.filter( (name) => name !== playlist, ), }) } > {playlist} </ContextMenuCheckboxItem> ))} </ContextMenuGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSeparator /> <ContextMenuItem onClick={() => setStatus(`Opened ${track.artist}.`)} > <MicVocal aria-hidden="true" /> Go to artist </ContextMenuItem> <ContextMenuItem onClick={() => setStatus(`Opened ${track.album}.`)} > <Disc3 aria-hidden="true" /> Go to album </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem variant="destructive" onClick={() => { setTracks((current) => current.filter((item) => item.id !== track.id), ); setQueue((current) => current.filter((id) => id !== track.id), ); setStatus(`${track.title} removed from this playlist.`); }} > <Trash2 aria-hidden="true" /> Remove from this playlist </ContextMenuItem> </ContextMenuContent> </ContextMenu> </li> ); })} </ol> )} <p aria-live="polite" className="min-h-10 border-t px-4 py-2.5 text-xs text-muted-foreground" > {status || "Right-click a song, or focus it and press Shift+F10."} </p> </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-05pnpm dlx shadcn@latest add @sevenui/component/context-menu-05yarn dlx shadcn@latest add @sevenui/component/context-menu-05bunx --bun shadcn@latest add @sevenui/component/context-menu-05Right-click a block, or focus it and press Shift+F10, to transform, insert, or reorder.
4 blocks
TSX"use client"; import { ArrowDown, ArrowUp, CopyPlus, FileVideo, Heading2, ImageIcon, ListTodo, Paperclip, Pilcrow, Plus, Quote, Repeat2, Trash2, } from "lucide-react"; import * as React from "react"; import { cn } from "cn"; import { Checkbox } from "@/components/ui/checkbox"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type TextType = "text" | "heading" | "quote" | "todo"; type MediaType = "image" | "video" | "file"; type Block = { id: string; type: TextType | MediaType; text: string; done?: boolean; }; const textTypes: { value: TextType; label: string; icon: typeof Pilcrow }[] = [ { value: "text", label: "Text", icon: Pilcrow }, { value: "heading", label: "Heading", icon: Heading2 }, { value: "quote", label: "Quote", icon: Quote }, { value: "todo", label: "To-do", icon: ListTodo }, ]; const mediaTypes: { value: MediaType; label: string; icon: typeof Pilcrow }[] = [ { value: "image", label: "Image", icon: ImageIcon }, { value: "video", label: "Video embed", icon: FileVideo }, { value: "file", label: "File attachment", icon: Paperclip }, ]; const initialBlocks: Block[] = [ { id: "b1", type: "heading", text: "What went well" }, { id: "b2", type: "text", text: "Checkout conversion held at 3.8% through the launch-day traffic spike.", }, { id: "b3", type: "quote", text: "The status page update went out before the first support ticket.", }, { id: "b4", type: "todo", text: "Write the incident timeline for the billing outage", done: false, }, ]; const placeholder: Record<Block["type"], string> = { text: "New paragraph", heading: "New section", quote: "Add a quote", todo: "New task", image: "Image · drop a file or paste a link", video: "Video · paste a YouTube or Loom link", file: "Attachment · up to 25 MB", }; function labelOf(type: Block["type"]) { return ( [...textTypes, ...mediaTypes].find((item) => item.value === type)?.label ?? type ); } export default function ContextMenu06() { const [blocks, setBlocks] = React.useState(initialBlocks); const [status, setStatus] = React.useState(""); const counter = React.useRef(0); const hintId = React.useId(); function insertAfter(index: number, type: Block["type"]) { counter.current += 1; const block: Block = { id: `new-${counter.current}`, type, text: placeholder[type], done: type === "todo" ? false : undefined, }; setBlocks((current) => [ ...current.slice(0, index + 1), block, ...current.slice(index + 1), ]); setStatus(`${labelOf(type)} block inserted.`); } function move(index: number, offset: -1 | 1) { setBlocks((current) => { const next = [...current]; const [block] = next.splice(index, 1); next.splice(index + offset, 0, block); return next; }); setStatus(offset < 0 ? "Block moved up." : "Block moved down."); } function patch(id: string, next: Partial<Block>) { setBlocks((current) => current.map((block) => (block.id === id ? { ...block, ...next } : block)), ); } return ( <article className="flex w-full max-w-md flex-col gap-3 rounded-xl border bg-card p-5 text-card-foreground"> <header> <h3 className="text-base font-semibold">Launch retro · Oct 2</h3> <p id={hintId} className="text-xs text-muted-foreground"> Right-click a block, or focus it and press Shift+F10, to transform, insert, or reorder. </p> </header> <div className="flex flex-col gap-1"> {blocks.map((block, index) => { const media = mediaTypes.find((item) => item.value === block.type); const isMedia = media !== undefined; const MediaIcon = media?.icon ?? Paperclip; return ( <ContextMenu key={block.id}> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-describedby={hintId} className={cn( "-mx-2 rounded-md px-2 py-1 outline-none transition-colors hover:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:bg-muted", block.type === "heading" && "pt-2 text-base font-semibold", block.type === "text" && "text-sm leading-relaxed", block.type === "quote" && "border-l-2 border-border text-sm text-muted-foreground italic", )} > {block.type === "todo" ? ( <span className="flex items-start gap-2 text-sm"> <Checkbox aria-label={block.text} checked={block.done} onCheckedChange={(checked) => patch(block.id, { done: checked }) } className="mt-0.5" /> <span className={cn( block.done && "text-muted-foreground line-through", )} > {block.text} </span> </span> ) : isMedia ? ( <span className="flex items-center gap-2 rounded-md border border-dashed bg-muted/40 px-3 py-4 text-xs text-muted-foreground"> <MediaIcon aria-hidden="true" className="size-4 shrink-0" /> {block.text} </span> ) : ( block.text )} </ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuSub> <ContextMenuSubTrigger disabled={isMedia}> <Repeat2 aria-hidden="true" /> Turn into </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> <ContextMenuRadioGroup value={block.type} onValueChange={(value) => { const type = value as TextType; patch(block.id, { type, done: type === "todo" ? false : undefined, }); setStatus(`Block turned into ${labelOf(type)}.`); }} > {textTypes.map(({ value, label, icon: Icon }) => ( <ContextMenuRadioItem key={value} value={value}> <Icon aria-hidden="true" /> {label} </ContextMenuRadioItem> ))} </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuSub> <ContextMenuSubTrigger> <Plus aria-hidden="true" /> Insert below </ContextMenuSubTrigger> <ContextMenuSubContent className="w-48"> <ContextMenuGroup> <ContextMenuLabel>Basic blocks</ContextMenuLabel> {textTypes.map(({ value, label, icon: Icon }) => ( <ContextMenuItem key={value} onClick={() => insertAfter(index, value)} > <Icon aria-hidden="true" /> {label} </ContextMenuItem> ))} </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger> <ImageIcon aria-hidden="true" /> Media </ContextMenuSubTrigger> <ContextMenuSubContent className="w-48"> {mediaTypes.map(({ value, label, icon: Icon }) => ( <ContextMenuItem key={value} onClick={() => insertAfter(index, value)} > <Icon aria-hidden="true" /> {label} </ContextMenuItem> ))} </ContextMenuSubContent> </ContextMenuSub> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuItem onClick={() => { counter.current += 1; const copy = { ...block, id: `copy-${counter.current}` }; setBlocks((current) => [ ...current.slice(0, index + 1), copy, ...current.slice(index + 1), ]); setStatus("Block duplicated."); }} > <CopyPlus aria-hidden="true" /> Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem disabled={index === 0} onClick={() => move(index, -1)} > <ArrowUp aria-hidden="true" /> Move up <ContextMenuShortcut>⌥⇧↑</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem disabled={index === blocks.length - 1} onClick={() => move(index, 1)} > <ArrowDown aria-hidden="true" /> Move down <ContextMenuShortcut>⌥⇧↓</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem variant="destructive" disabled={blocks.length === 1} onClick={() => { setBlocks((current) => current.filter((item) => item.id !== block.id), ); setStatus(`${labelOf(block.type)} block deleted.`); }} > <Trash2 aria-hidden="true" /> Delete block <ContextMenuShortcut>⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> ); })} </div> <p aria-live="polite" className="border-t pt-3 text-xs text-muted-foreground tabular-nums" > {status || `${blocks.length} blocks`} </p> </article> ); }
npx shadcn@latest add @sevenui/component/context-menu-06pnpm dlx shadcn@latest add @sevenui/component/context-menu-06yarn dlx shadcn@latest add @sevenui/component/context-menu-06bunx --bun shadcn@latest add @sevenui/component/context-menu-06acme-web
main · last deploy 3 hours ago
Actions keep the menu open and report progress inline.
TSX"use client"; import { AlertCircle, Check, CloudUpload, RefreshCw, Rocket, } from "lucide-react"; import * as React from "react"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Spinner } from "@/components/ui/spinner"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Status = "idle" | "loading" | "success" | "error"; type ActionKey = "sync" | "backup" | "publish"; const actions: { key: ActionKey; label: string; icon: typeof RefreshCw; outcome: "success" | "error"; success: string; error: string; }[] = [ { key: "sync", label: "Sync with GitHub", icon: RefreshCw, outcome: "success", success: "Synced 12 commits", error: "", }, { key: "backup", label: "Back up database", icon: CloudUpload, outcome: "success", success: "Snapshot saved", error: "", }, { key: "publish", label: "Deploy to production", icon: Rocket, outcome: "error", success: "Deployed in 48s", error: "Build failed: 2 type errors", }, ]; const initialStatus: Record<ActionKey, Status> = { sync: "idle", backup: "idle", publish: "idle", }; export default function ContextMenu07() { const [open, setOpen] = React.useState(false); const [status, setStatus] = React.useState(initialStatus); const [announcement, setAnnouncement] = React.useState(""); const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]); React.useEffect(() => { const pending = timers.current; return () => pending.forEach(clearTimeout); }, []); function run(action: (typeof actions)[number]) { if (status[action.key] === "loading") return; // The first deploy fails on purpose; a retry succeeds. const outcome = status[action.key] === "error" ? "success" : action.outcome; setStatus((prev) => ({ ...prev, [action.key]: "loading" })); setAnnouncement(`${action.label} started`); timers.current.push( setTimeout(() => { setStatus((prev) => ({ ...prev, [action.key]: outcome })); setAnnouncement(outcome === "success" ? action.success : action.error); }, 1400), ); } function handleOpenChange(next: boolean) { setOpen(next); if (next) { // Drop results still pending from the last visit so they cannot land // on the freshly reset menu. timers.current.forEach(clearTimeout); timers.current.length = 0; setStatus(initialStatus); } } const busy = Object.values(status).some((value) => value === "loading"); return ( <div className="flex w-full max-w-sm flex-col gap-2"> <ContextMenu open={open} onOpenChange={handleOpenChange}> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label="acme-web project. Right-click or press Shift+F10 for project actions." className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card p-4 outline-none focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:border-ring" > <div className="min-w-0"> <p className="truncate text-sm font-medium">acme-web</p> <p className="truncate text-xs text-muted-foreground"> main · last deploy 3 hours ago </p> </div> <span className="shrink-0 text-xs text-muted-foreground"> {open ? (busy ? "Working…" : "Menu open") : "Right-click"} </span> </ContextMenuTrigger> <ContextMenuContent className="w-64"> <ContextMenuGroup> <ContextMenuLabel>Project actions</ContextMenuLabel> {actions.map((action) => { const state = status[action.key]; const Icon = action.icon; return ( <ContextMenuItem key={action.key} closeOnClick={false} label={action.label} aria-busy={state === "loading"} onClick={() => run(action)} className="items-start py-1.5" > <span className="mt-0.5 flex size-4 items-center justify-center"> {state === "loading" ? ( <Spinner aria-hidden="true" /> ) : state === "success" ? ( <Check aria-hidden="true" className="text-success" /> ) : state === "error" ? ( <AlertCircle aria-hidden="true" className="text-destructive" /> ) : ( <Icon aria-hidden="true" /> )} </span> <span className="flex min-w-0 flex-1 flex-col"> <span>{action.label}</span> {state === "loading" && ( <span className="text-xs text-muted-foreground"> Running… </span> )} {state === "success" && ( <span className="text-xs text-muted-foreground"> {action.success} </span> )} {state === "error" && ( <span className="text-xs text-destructive"> {action.error} · Click to retry </span> )} </span> </ContextMenuItem> ); })} </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuItem onClick={() => setOpen(false)}> Done <ContextMenuShortcut>Esc</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> <p className="sr-only" aria-live="polite"> {announcement} </p> <p className="text-xs text-muted-foreground"> Actions keep the menu open and report progress inline. </p> </div> ); }
npx shadcn@latest add @sevenui/component/context-menu-07pnpm dlx shadcn@latest add @sevenui/component/context-menu-07yarn dlx shadcn@latest add @sevenui/component/context-menu-07bunx --bun shadcn@latest add @sevenui/component/context-menu-07Right-click a member to change their role or access.
Maya Chen
maya@northwind.io
Jonas Weber
jonas@northwind.io
Priya Raman
priya@northwind.io
Leo Martins
leo@contractor.dev
TSX"use client"; import { Copy, Mail, ShieldCheck, UserMinus } from "lucide-react"; import * as React from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Role = "Owner" | "Admin" | "Member" | "Viewer"; type Member = { id: string; name: string; email: string; initials: string; role: Role; pending: boolean; }; const roles: { value: Role; description: string }[] = [ { value: "Admin", description: "Billing and members" }, { value: "Member", description: "Create and edit projects" }, { value: "Viewer", description: "Read-only access" }, ]; const initialMembers: Member[] = [ { id: "maya", name: "Maya Chen", email: "maya@northwind.io", initials: "MC", role: "Owner", pending: false, }, { id: "jonas", name: "Jonas Weber", email: "jonas@northwind.io", initials: "JW", role: "Admin", pending: false, }, { id: "priya", name: "Priya Raman", email: "priya@northwind.io", initials: "PR", role: "Member", pending: false, }, { id: "leo", name: "Leo Martins", email: "leo@contractor.dev", initials: "LM", role: "Viewer", pending: true, }, ]; export default function ContextMenu08() { const [members, setMembers] = React.useState(initialMembers); const [notice, setNotice] = React.useState(""); function setRole(id: string, role: Role) { setMembers((current) => current.map((member) => member.id === id ? { ...member, role } : member, ), ); } return ( <section aria-labelledby="context-menu-08-title" className="w-full max-w-md rounded-xl border bg-card text-card-foreground" > <header className="border-b px-4 py-3"> <h3 id="context-menu-08-title" className="text-sm font-semibold"> Team members </h3> <p className="text-xs text-muted-foreground"> Right-click a member to change their role or access. </p> </header> <ul className="divide-y"> {members.map((member) => { const isOwner = member.role === "Owner"; return ( <li key={member.id}> <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`${member.name}, ${member.role}${member.pending ? ", invitation pending" : ""}`} className="flex items-center gap-3 px-4 py-3 outline-none transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset data-popup-open:bg-muted/60" > <Avatar> <AvatarFallback>{member.initials}</AvatarFallback> </Avatar> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium"> {member.name} </p> <p className="truncate text-xs text-muted-foreground"> {member.email} </p> </div> {member.pending ? ( <Badge variant="outline" className="hidden sm:inline-flex"> Invited </Badge> ) : null} <Badge variant={isOwner ? "default" : "secondary"}> {member.role} </Badge> </ContextMenuTrigger> <ContextMenuContent className="w-60"> <ContextMenuGroup> <ContextMenuLabel className="truncate"> {member.name} </ContextMenuLabel> <ContextMenuSub> <ContextMenuSubTrigger disabled={isOwner}> <ShieldCheck aria-hidden="true" /> Change role </ContextMenuSubTrigger> <ContextMenuSubContent className="w-56"> <ContextMenuRadioGroup value={member.role} onValueChange={(value) => { setRole(member.id, value as Role); setNotice(`${member.name} is now ${value}.`); }} > {roles.map((role) => ( <ContextMenuRadioItem key={role.value} value={role.value} > <span className="flex flex-col"> <span>{role.value}</span> <span className="text-xs text-muted-foreground"> {role.description} </span> </span> </ContextMenuRadioItem> ))} </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuItem onClick={() => { void navigator.clipboard ?.writeText(member.email) .catch(() => {}); setNotice(`Copied ${member.email} to clipboard.`); }} > <Copy aria-hidden="true" /> Copy email </ContextMenuItem> {member.pending ? ( <ContextMenuItem onClick={() => setNotice(`Invitation resent to ${member.email}.`) } > <Mail aria-hidden="true" /> Resend invitation </ContextMenuItem> ) : null} </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuItem variant="destructive" disabled={isOwner} onClick={() => { setMembers((current) => current.filter((item) => item.id !== member.id), ); setNotice(`${member.name} was removed from the team.`); }} > <UserMinus aria-hidden="true" /> {member.pending ? "Revoke invitation" : "Remove from team"} </ContextMenuItem> </ContextMenuContent> </ContextMenu> </li> ); })} </ul> <footer aria-live="polite" className="min-h-10 border-t px-4 py-2.5 text-xs text-muted-foreground" > {notice || "Owners can't be demoted or removed."} </footer> </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-08pnpm dlx shadcn@latest add @sevenui/component/context-menu-08yarn dlx shadcn@latest add @sevenui/component/context-menu-08bunx --bun shadcn@latest add @sevenui/component/context-menu-08Amara mentioned you in Pricing page copy
“Can you confirm the annual discount is still 20%?”
Review requested on PR 482
fix(billing): prorate seat changes mid-cycle
Theo replied in Onboarding checklist
“Shipped the empty state, screenshots in thread.”
TSX"use client"; import { Archive, AtSign, BellOff, Clock, GitPullRequest, MailCheck, MailOpen, MessageSquare, RotateCcw, } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Notice = { id: string; icon: typeof AtSign; title: string; detail: string; time: string; thread: string; unread: boolean; snoozedUntil?: string; }; const snoozeOptions = [ "In 1 hour", "This evening", "Tomorrow morning", "Next week", ]; const initialNotices: Notice[] = [ { id: "n1", icon: AtSign, title: "Amara mentioned you in Pricing page copy", detail: "“Can you confirm the annual discount is still 20%?”", time: "4m", thread: "Pricing page copy", unread: true, }, { id: "n2", icon: GitPullRequest, title: "Review requested on PR 482", detail: "fix(billing): prorate seat changes mid-cycle", time: "32m", thread: "PR 482", unread: true, }, { id: "n3", icon: MessageSquare, title: "Theo replied in Onboarding checklist", detail: "“Shipped the empty state, screenshots in thread.”", time: "2h", thread: "Onboarding checklist", unread: false, }, ]; export default function ContextMenu09() { const [notices, setNotices] = React.useState(initialNotices); const [muted, setMuted] = React.useState<string[]>([]); const unreadCount = notices.filter( (notice) => notice.unread && !notice.snoozedUntil, ).length; function patch(id: string, next: Partial<Notice>) { setNotices((current) => current.map((notice) => notice.id === id ? { ...notice, ...next } : notice, ), ); } return ( <section aria-labelledby="context-menu-09-title" className="w-full max-w-sm overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-sm" > <header className="flex items-center justify-between gap-2 border-b px-4 py-3"> <h3 id="context-menu-09-title" className="text-sm font-semibold"> Notifications <span className="ml-2 text-xs font-normal whitespace-nowrap text-muted-foreground tabular-nums"> {unreadCount} unread </span> </h3> <Button variant="ghost" size="sm" disabled={unreadCount === 0} onClick={() => setNotices((current) => current.map((notice) => ({ ...notice, unread: false })), ) } > <MailCheck aria-hidden="true" /> Mark all read </Button> </header> {notices.length === 0 ? ( <div className="flex flex-col items-center gap-3 px-4 py-10 text-center"> <p className="text-sm text-muted-foreground">You're all caught up.</p> <Button variant="outline" size="sm" onClick={() => { setNotices(initialNotices); setMuted([]); }} > <RotateCcw aria-hidden="true" /> Restore archived </Button> </div> ) : ( <ul className="divide-y"> {notices.map((notice) => { const Icon = notice.icon; const isMuted = muted.includes(notice.thread); return ( <li key={notice.id}> <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`${notice.unread ? "Unread: " : ""}${notice.title}, ${notice.time} ago`} className="flex gap-3 px-4 py-3 outline-none transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset data-popup-open:bg-muted/60" > <span className="relative mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-muted"> <Icon aria-hidden="true" className="size-4 text-muted-foreground" /> {notice.unread && !notice.snoozedUntil ? ( <span aria-hidden="true" className="absolute -top-0.5 -right-0.5 size-2.5 rounded-full bg-primary ring-2 ring-popover" /> ) : null} </span> <div className="min-w-0 flex-1"> <p className={ notice.unread ? "text-sm font-medium" : "text-sm text-muted-foreground" } > {notice.title} </p> <p className="mt-0.5 truncate text-xs text-muted-foreground"> {notice.detail} </p> {notice.snoozedUntil || isMuted ? ( <p className="mt-1.5 flex flex-wrap gap-x-3 text-xs text-muted-foreground"> {notice.snoozedUntil ? ( <span className="inline-flex items-center gap-1"> <Clock aria-hidden="true" className="size-3" /> Snoozed · {notice.snoozedUntil} </span> ) : null} {isMuted ? ( <span className="inline-flex items-center gap-1"> <BellOff aria-hidden="true" className="size-3" /> Thread muted </span> ) : null} </p> ) : null} </div> <span className="shrink-0 text-xs text-muted-foreground tabular-nums"> {notice.time} </span> </ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuItem onClick={() => patch(notice.id, { unread: !notice.unread }) } > {notice.unread ? ( <MailCheck aria-hidden="true" /> ) : ( <MailOpen aria-hidden="true" /> )} {notice.unread ? "Mark as read" : "Mark as unread"} <ContextMenuShortcut>U</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSub> <ContextMenuSubTrigger> <Clock aria-hidden="true" /> Snooze </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> {snoozeOptions.map((option) => ( <ContextMenuItem key={option} onClick={() => patch(notice.id, { snoozedUntil: option }) } > {option} </ContextMenuItem> ))} {notice.snoozedUntil ? ( <> <ContextMenuSeparator /> <ContextMenuItem onClick={() => patch(notice.id, { snoozedUntil: undefined }) } > Unsnooze </ContextMenuItem> </> ) : null} </ContextMenuSubContent> </ContextMenuSub> <ContextMenuCheckboxItem checked={isMuted} onCheckedChange={(checked) => setMuted((current) => checked ? [...current, notice.thread] : current.filter((t) => t !== notice.thread), ) } > <BellOff aria-hidden="true" /> Mute this thread </ContextMenuCheckboxItem> <ContextMenuSeparator /> <ContextMenuItem onClick={() => setNotices((current) => current.filter((item) => item.id !== notice.id), ) } > <Archive aria-hidden="true" /> Archive <ContextMenuShortcut>E</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> </li> ); })} </ul> )} </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-09pnpm dlx shadcn@latest add @sevenui/component/context-menu-09yarn dlx shadcn@latest add @sevenui/component/context-menu-09bunx --bun shadcn@latest add @sevenui/component/context-menu-093 events today
TSX"use client"; import { CalendarArrowUp, CopyPlus, Link2, Palette, RotateCcw, Trash2, Video, } from "lucide-react"; import * as React from "react"; import { cn } from "cn"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Rsvp = "going" | "maybe" | "declined"; type Tone = "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5"; type CalendarEvent = { id: string; title: string; start: number; end: number; rsvp: Rsvp; tone: Tone; video: boolean; }; const START_HOUR = 9; const HOURS = [9, 10, 11, 12, 13]; const ROW = 48; const tones: { value: Tone; label: string; swatch: string; block: string }[] = [ { value: "chart-1", label: "Focus", swatch: "bg-chart-1", block: "border-chart-1/40 bg-chart-1/15", }, { value: "chart-2", label: "Meetings", swatch: "bg-chart-2", block: "border-chart-2/40 bg-chart-2/15", }, { value: "chart-3", label: "Customers", swatch: "bg-chart-3", block: "border-chart-3/40 bg-chart-3/15", }, { value: "chart-4", label: "Hiring", swatch: "bg-chart-4", block: "border-chart-4/40 bg-chart-4/15", }, { value: "chart-5", label: "Personal", swatch: "bg-chart-5", block: "border-chart-5/40 bg-chart-5/15", }, ]; const initialEvents: CalendarEvent[] = [ { id: "standup", title: "Design standup", start: 9, end: 9.5, rsvp: "going", tone: "chart-2", video: true, }, { id: "acme", title: "Acme onboarding call", start: 10, end: 11, rsvp: "maybe", tone: "chart-3", video: true, }, { id: "focus", title: "Write Q4 roadmap", start: 11.5, end: 13, rsvp: "going", tone: "chart-1", video: false, }, ]; function formatHour(value: number) { const hour = Math.floor(value); const minutes = value % 1 === 0 ? "00" : "30"; const display = hour > 12 ? hour - 12 : hour; return `${display}:${minutes}${hour >= 12 ? " PM" : " AM"}`; } export default function ContextMenu10() { const [events, setEvents] = React.useState(initialEvents); const [status, setStatus] = React.useState(""); function patch(id: string, next: Partial<CalendarEvent>) { setEvents((current) => current.map((event) => (event.id === id ? { ...event, ...next } : event)), ); } return ( <section aria-labelledby="context-menu-10-title" className="w-full max-w-sm rounded-xl border bg-card text-card-foreground" > <header className="flex items-baseline justify-between border-b px-4 py-3"> <h3 id="context-menu-10-title" className="text-sm font-semibold"> Thursday, Oct 8 </h3> <span className="text-xs text-muted-foreground"> Right-click an event </span> </header> <div className="relative px-4 py-3"> <ol aria-hidden="true"> {HOURS.map((hour) => ( <li key={hour} style={{ height: ROW }} className="flex gap-3 border-t border-dashed first:border-t-0" > <span className="-mt-2 w-14 shrink-0 bg-card text-[11px] whitespace-nowrap text-muted-foreground tabular-nums"> {formatHour(hour)} </span> </li> ))} </ol> <ul className="absolute inset-y-3 right-4 left-21"> {events.map((event) => { const tone = tones.find((item) => item.value === event.tone); return ( <li key={event.id} className="absolute inset-x-0 px-0.5 py-px" style={{ top: (event.start - START_HOUR) * ROW, height: (event.end - event.start) * ROW, }} > <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`${event.title}, ${formatHour(event.start)} to ${formatHour(event.end)}, RSVP ${event.rsvp}`} className={cn( "flex h-full flex-col overflow-hidden rounded-md border px-2 py-1 text-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:ring-2 data-popup-open:ring-ring/40", tone?.block, event.rsvp === "declined" && "opacity-50", )} > <span className={cn( "flex min-w-0 shrink-0 items-center gap-1 font-medium", event.rsvp === "declined" && "line-through", )} > {event.video ? ( <Video aria-hidden="true" className="size-3 shrink-0" /> ) : null} <span className="truncate">{event.title}</span> </span> {event.end - event.start >= 1 ? ( <span className="truncate text-muted-foreground tabular-nums"> {formatHour(event.start)} – {formatHour(event.end)} {event.rsvp === "maybe" ? " · Maybe" : ""} </span> ) : null} </ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuGroup> <ContextMenuLabel>Going?</ContextMenuLabel> <ContextMenuRadioGroup value={event.rsvp} onValueChange={(value) => patch(event.id, { rsvp: value as Rsvp }) } > <ContextMenuRadioItem value="going"> Yes </ContextMenuRadioItem> <ContextMenuRadioItem value="maybe"> Maybe </ContextMenuRadioItem> <ContextMenuRadioItem value="declined"> No </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger> <Palette aria-hidden="true" /> Calendar color </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> <ContextMenuRadioGroup value={event.tone} onValueChange={(value) => patch(event.id, { tone: value as Tone }) } > {tones.map((item) => ( <ContextMenuRadioItem key={item.value} value={item.value} > <span aria-hidden="true" className={cn( "size-2.5 rounded-full", item.swatch, )} /> {item.label} </ContextMenuRadioItem> ))} </ContextMenuRadioGroup> </ContextMenuSubContent> </ContextMenuSub> {event.video ? ( <ContextMenuItem onClick={() => setStatus(`Meeting link for ${event.title} copied.`) } > <Link2 aria-hidden="true" /> Copy meeting link </ContextMenuItem> ) : null} <ContextMenuItem onClick={() => setStatus(`${event.title} duplicated to Friday.`) } > <CopyPlus aria-hidden="true" /> Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem onClick={() => { setEvents((current) => current.filter((item) => item.id !== event.id), ); setStatus(`${event.title} moved to Friday, Oct 9.`); }} > <CalendarArrowUp aria-hidden="true" /> Move to tomorrow </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuItem variant="destructive" onClick={() => { setEvents((current) => current.filter((item) => item.id !== event.id), ); setStatus( `${event.title} deleted. Guests were notified.`, ); }} > <Trash2 aria-hidden="true" /> Delete event </ContextMenuItem> </ContextMenuContent> </ContextMenu> </li> ); })} </ul> </div> <div className="flex min-h-10 items-center justify-between gap-2 border-t px-4 py-1.5"> <p aria-live="polite" className="text-xs text-muted-foreground"> {status || `${events.length} events today`} </p> {events.length === 0 ? ( <Button variant="ghost" size="xs" onClick={() => { setEvents(initialEvents); setStatus("Thursday's events restored."); }} > <RotateCcw aria-hidden="true" /> Restore </Button> ) : null} </div> </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-10pnpm dlx shadcn@latest add @sevenui/component/context-menu-10yarn dlx shadcn@latest add @sevenui/component/context-menu-10bunx --bun shadcn@latest add @sevenui/component/context-menu-10Right-click a message to reply, react, or pin it.
TSX"use client"; import { CheckCircle2, Copy, Eye, Heart, Pin, Reply, SendHorizontal, SmilePlus, ThumbsUp, Trash2, X, } from "lucide-react"; import * as React from "react"; import { cn } from "cn"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Input } from "@/components/ui/input"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type ReactionKey = "thanks" | "love" | "seen"; type ChatMessage = { id: string; author: "agent" | "customer"; name: string; text: string; time: string; pinned: boolean; reactions: ReactionKey[]; replyTo?: string; }; const reactions: { key: ReactionKey; label: string; icon: typeof Heart }[] = [ { key: "thanks", label: "Thanks", icon: ThumbsUp }, { key: "love", label: "Love it", icon: Heart }, { key: "seen", label: "Seen", icon: Eye }, ]; const initialMessages: ChatMessage[] = [ { id: "m1", author: "customer", name: "Daniel Ortiz", text: "Our invoice for September shows 14 seats but we removed two people on the 12th.", time: "10:42", pinned: false, reactions: [], }, { id: "m2", author: "agent", name: "You", text: "Thanks Daniel. Seat removals are prorated on the next invoice, so you'll see a $38.00 credit in October.", time: "10:44", pinned: false, reactions: [], }, { id: "m3", author: "customer", name: "Daniel Ortiz", text: "Got it. Can the credit be applied to this invoice instead?", time: "10:46", pinned: false, reactions: [], }, ]; export default function ContextMenu11() { const [messages, setMessages] = React.useState(initialMessages); const [replyTo, setReplyTo] = React.useState<ChatMessage | null>(null); const [draft, setDraft] = React.useState(""); const [resolvedId, setResolvedId] = React.useState<string | null>(null); const [copiedId, setCopiedId] = React.useState<string | null>(null); const inputRef = React.useRef<HTMLInputElement>(null); // Set by "Reply" so the closing menu hands focus to the composer instead // of returning it to the message bubble. const focusComposer = React.useRef(false); const copyTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined); React.useEffect(() => () => clearTimeout(copyTimer.current), []); function copy(message: ChatMessage) { void navigator.clipboard?.writeText(message.text).catch(() => {}); setCopiedId(message.id); clearTimeout(copyTimer.current); copyTimer.current = setTimeout(() => setCopiedId(null), 1500); } function patch(id: string, next: Partial<ChatMessage>) { setMessages((current) => current.map((message) => message.id === id ? { ...message, ...next } : message, ), ); } function send(event: React.FormEvent<HTMLFormElement>) { event.preventDefault(); const text = draft.trim(); if (!text) return; setMessages((current) => [ ...current, { id: `m${current.length + 1}-${Date.now()}`, author: "agent", name: "You", text, time: "10:48", pinned: false, reactions: [], replyTo: replyTo?.text, }, ]); setDraft(""); setReplyTo(null); } const pinned = messages.find((message) => message.pinned); return ( <section aria-labelledby="context-menu-11-title" className="flex w-full max-w-md flex-col overflow-hidden rounded-xl border bg-card text-card-foreground" > <header className="border-b px-4 py-3"> <h3 id="context-menu-11-title" className="text-sm font-semibold"> Billing question · Daniel Ortiz </h3> <p className="text-xs text-muted-foreground"> Right-click a message to reply, react, or pin it. </p> </header> {pinned ? ( <div className="flex items-start gap-2 border-b bg-muted/50 px-4 py-2 text-xs"> <Pin aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" /> <p className="line-clamp-1 text-muted-foreground"> <span className="font-medium text-foreground">Pinned: </span> {pinned.text} </p> </div> ) : null} <ol className="flex flex-col gap-3 p-4" aria-label="Conversation"> {messages.map((message) => { const own = message.author === "agent"; const resolved = resolvedId === message.id; return ( <li key={message.id} className={cn( "flex flex-col gap-1", own ? "items-end" : "items-start", )} > <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`${message.name} at ${message.time}: ${message.text}`} className={cn( "max-w-[85%] rounded-2xl px-3 py-2 text-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:ring-2 data-popup-open:ring-ring/40", own ? "rounded-br-sm bg-primary text-primary-foreground" : "rounded-bl-sm bg-muted", resolved && "ring-1 ring-success", )} > {message.replyTo ? ( <span className={cn( "mb-1 block truncate border-b pb-1 text-xs", own ? "border-primary-foreground/20 text-primary-foreground/75" : "border-border text-muted-foreground", )} > Replying to “{message.replyTo}” </span> ) : null} {message.text} </ContextMenuTrigger> <ContextMenuContent className="w-52" finalFocus={() => { if (!focusComposer.current) return true; focusComposer.current = false; return inputRef.current; }} > <ContextMenuItem onClick={() => { setReplyTo(message); focusComposer.current = true; }} > <Reply aria-hidden="true" /> Reply <ContextMenuShortcut>R</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSub> <ContextMenuSubTrigger> <SmilePlus aria-hidden="true" /> React </ContextMenuSubTrigger> <ContextMenuSubContent className="w-40"> {reactions.map((reaction) => { const Icon = reaction.icon; return ( <ContextMenuCheckboxItem key={reaction.key} checked={message.reactions.includes(reaction.key)} onCheckedChange={(checked) => patch(message.id, { reactions: checked ? [...message.reactions, reaction.key] : message.reactions.filter( (key) => key !== reaction.key, ), }) } > <Icon aria-hidden="true" /> {reaction.label} </ContextMenuCheckboxItem> ); })} </ContextMenuSubContent> </ContextMenuSub> <ContextMenuItem onClick={() => copy(message)}> <Copy aria-hidden="true" /> Copy text <ContextMenuShortcut>⌘C</ContextMenuShortcut> </ContextMenuItem> <ContextMenuCheckboxItem checked={message.pinned} onCheckedChange={(checked) => setMessages((current) => current.map((item) => ({ ...item, pinned: item.id === message.id ? checked : false, })), ) } > <Pin aria-hidden="true" /> Pin to conversation </ContextMenuCheckboxItem> {own ? ( <ContextMenuCheckboxItem checked={resolved} onCheckedChange={(checked) => setResolvedId(checked ? message.id : null) } > <CheckCircle2 aria-hidden="true" /> Mark as solution </ContextMenuCheckboxItem> ) : null} {own ? ( <> <ContextMenuSeparator /> <ContextMenuItem variant="destructive" onClick={() => setMessages((current) => current.filter((item) => item.id !== message.id), ) } > <Trash2 aria-hidden="true" /> Delete message </ContextMenuItem> </> ) : null} </ContextMenuContent> </ContextMenu> <div className="flex items-center gap-2 px-1 text-[11px] text-muted-foreground"> <span> {message.name} · {message.time} </span> {copiedId === message.id ? ( <span role="status" className="font-medium text-foreground"> Copied </span> ) : null} {resolved ? ( <span className="inline-flex items-center gap-1 font-medium text-foreground"> <CheckCircle2 aria-hidden="true" className="size-3 text-success" /> Solution </span> ) : null} {message.reactions.map((key) => { const reaction = reactions.find((item) => item.key === key); if (!reaction) return null; const Icon = reaction.icon; return ( <span key={key} className="inline-flex items-center rounded-full border bg-background px-1.5 py-0.5" > <Icon aria-label={reaction.label} className="size-3" /> </span> ); })} </div> </li> ); })} </ol> <form onSubmit={send} className="mt-auto border-t p-3"> {replyTo ? ( <div className="mb-2 flex items-center gap-2 rounded-md bg-muted px-2 py-1.5 text-xs"> <Reply aria-hidden="true" className="size-3.5 shrink-0" /> <span className="min-w-0 flex-1 truncate text-muted-foreground"> Replying to {replyTo.name}: {replyTo.text} </span> <Button type="button" variant="ghost" size="icon-xs" aria-label="Cancel reply" onClick={() => setReplyTo(null)} > <X aria-hidden="true" /> </Button> </div> ) : null} <div className="flex gap-2"> <Input ref={inputRef} aria-label="Message" placeholder="Write a reply…" value={draft} onChange={(event) => setDraft(event.target.value)} /> <Button type="submit" size="icon" aria-label="Send message"> <SendHorizontal aria-hidden="true" /> </Button> </div> </form> </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-11pnpm dlx shadcn@latest add @sevenui/component/context-menu-11yarn dlx shadcn@latest add @sevenui/component/context-menu-11bunx --bun shadcn@latest add @sevenui/component/context-menu-11New signups
3,482
Last 7 days
+12.4% vs previous
TSX"use client"; import { Download, EyeOff, FileImage, FileSpreadsheet, LayoutDashboard, RotateCcw, TrendingDown, TrendingUp, } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Range = "7d" | "30d" | "90d"; const series: Record< Range, { label: string; total: string; delta: string; bars: { label: string; value: number; previous: number }[]; } > = { "7d": { label: "Last 7 days", total: "3,482", delta: "+12.4%", bars: [ { label: "Mon", value: 420, previous: 380 }, { label: "Tue", value: 512, previous: 441 }, { label: "Wed", value: 488, previous: 470 }, { label: "Thu", value: 604, previous: 512 }, { label: "Fri", value: 571, previous: 498 }, { label: "Sat", value: 402, previous: 377 }, { label: "Sun", value: 485, previous: 420 }, ], }, "30d": { label: "Last 30 days", total: "14,906", delta: "+6.1%", bars: [ { label: "W1", value: 3310, previous: 3190 }, { label: "W2", value: 3642, previous: 3401 }, { label: "W3", value: 3920, previous: 3588 }, { label: "W4", value: 4034, previous: 3870 }, ], }, "90d": { label: "Last 90 days", total: "41,275", delta: "−2.3%", bars: [ { label: "Jul", value: 14620, previous: 14100 }, { label: "Aug", value: 12749, previous: 14230 }, { label: "Sep", value: 13906, previous: 13920 }, ], }, }; export default function ContextMenu12() { const [range, setRange] = React.useState<Range>("7d"); const [compare, setCompare] = React.useState(true); const [showValues, setShowValues] = React.useState(false); const [hidden, setHidden] = React.useState(false); const [status, setStatus] = React.useState(""); const data = series[range]; const max = Math.max( ...data.bars.flatMap((bar) => [bar.value, bar.previous]), ); const negative = data.delta.startsWith("−"); if (hidden) { return ( <div className="flex w-full max-w-md flex-col items-center gap-3 rounded-xl border border-dashed p-8 text-center"> <LayoutDashboard aria-hidden="true" className="size-5 text-muted-foreground" /> <p className="text-sm text-muted-foreground"> Signups widget removed from Growth overview. </p> <Button variant="outline" size="sm" onClick={() => setHidden(false)}> <RotateCcw aria-hidden="true" /> Undo </Button> </div> ); } return ( <ContextMenu> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} tabIndex={0} aria-label={`New signups widget, ${data.label}: ${data.total}. Open the context menu for widget options.`} className="block w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground outline-none focus-visible:ring-3 focus-visible:ring-ring/50 data-popup-open:border-ring" > <div className="flex items-start justify-between gap-4"> <div> <p className="text-sm text-muted-foreground">New signups</p> <p className="mt-1 text-2xl font-semibold tabular-nums"> {data.total} </p> </div> <div className="text-right"> <p className="text-xs text-muted-foreground">{data.label}</p> <p className="inline-flex items-center gap-1 text-xs font-medium tabular-nums"> {negative ? ( <TrendingDown aria-hidden="true" className="size-3.5 text-destructive" /> ) : ( <TrendingUp aria-hidden="true" className="size-3.5 text-success" /> )} {data.delta} vs previous </p> </div> </div> <div role="img" aria-label={data.bars .map((bar) => `${bar.label}: ${bar.value.toLocaleString("en-US")}`) .join(", ")} className="mt-5 flex h-36 items-end gap-2" > {data.bars.map((bar) => ( <div key={bar.label} className="flex h-full flex-1 flex-col items-center gap-1.5" > <div className="flex w-full flex-1 items-end justify-center gap-0.5"> {compare ? ( <div className="w-full max-w-5 rounded-t-sm bg-muted-foreground/20" style={{ height: `${(bar.previous / max) * 100}%` }} /> ) : null} <div className="relative w-full max-w-5 rounded-t-sm bg-chart-2 transition-[height] duration-300 ease-out" style={{ height: `${(bar.value / max) * 100}%` }} > {showValues ? ( <span className="absolute -top-4 left-1/2 -translate-x-1/2 text-[10px] text-muted-foreground tabular-nums"> {bar.value >= 1000 ? `${(bar.value / 1000).toFixed(1)}k` : bar.value} </span> ) : null} </div> </div> <span className="text-[11px] text-muted-foreground"> {bar.label} </span> </div> ))} </div> <div className="mt-3 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-xs text-muted-foreground"> <span className="flex items-center gap-3"> <span className="inline-flex items-center gap-1.5"> <span aria-hidden="true" className="size-2 shrink-0 rounded-xs bg-chart-2" /> Current </span> {compare ? ( <span className="inline-flex items-center gap-1.5"> <span aria-hidden="true" className="size-2 shrink-0 rounded-xs bg-muted-foreground/20" /> Previous </span> ) : null} </span> <span aria-live="polite">{status || "Right-click for options"}</span> </div> </ContextMenuTrigger> <ContextMenuContent className="w-56"> <ContextMenuGroup> <ContextMenuLabel>Date range</ContextMenuLabel> <ContextMenuRadioGroup value={range} onValueChange={(value) => setRange(value as Range)} > <ContextMenuRadioItem value="7d">Last 7 days</ContextMenuRadioItem> <ContextMenuRadioItem value="30d"> Last 30 days </ContextMenuRadioItem> <ContextMenuRadioItem value="90d"> Last 90 days </ContextMenuRadioItem> </ContextMenuRadioGroup> </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuGroup> <ContextMenuLabel>Display</ContextMenuLabel> <ContextMenuCheckboxItem checked={compare} onCheckedChange={setCompare} > Compare to previous period </ContextMenuCheckboxItem> <ContextMenuCheckboxItem checked={showValues} onCheckedChange={setShowValues} > Show data labels </ContextMenuCheckboxItem> </ContextMenuGroup> <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger> <Download aria-hidden="true" /> Export </ContextMenuSubTrigger> <ContextMenuSubContent className="w-44"> <ContextMenuItem onClick={() => setStatus(`signups-${range}.csv exported`)} > <FileSpreadsheet aria-hidden="true" /> Data as CSV </ContextMenuItem> <ContextMenuItem onClick={() => setStatus(`signups-${range}.png exported`)} > <FileImage aria-hidden="true" /> Chart as PNG </ContextMenuItem> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuItem variant="destructive" onClick={() => { setStatus(""); setHidden(true); }} > <EyeOff aria-hidden="true" /> Remove from dashboard </ContextMenuItem> </ContextMenuContent> </ContextMenu> ); }
npx shadcn@latest add @sevenui/component/context-menu-12pnpm dlx shadcn@latest add @sevenui/component/context-menu-12yarn dlx shadcn@latest add @sevenui/component/context-menu-12bunx --bun shadcn@latest add @sevenui/component/context-menu-12| Order | Customer | Status | Total |
|---|---|---|---|
| SO-10482Hannah Okafor | Hannah Okafor3 items | Unfulfilled | $128.50Unfulfilled |
| SO-10481Kenji Sato | Kenji Sato1 item | Packed | $64.00Packed |
| SO-10479Lucía Fernández | Lucía Fernández5 items | Shipped | $242.90Shipped |
TSX"use client"; import { Ban, Copy, ExternalLink, PackageCheck, Printer, ReceiptText, } from "lucide-react"; import * as React from "react"; import { Badge } from "@/components/ui/badge"; import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type Fulfillment = "unfulfilled" | "packed" | "shipped"; type Payment = "paid" | "refunded" | "partially refunded" | "canceled"; type Order = { id: string; customer: string; items: number; total: number; shipping: number; fulfillment: Fulfillment; payment: Payment; }; const fulfillmentLabel: Record<Fulfillment, string> = { unfulfilled: "Unfulfilled", packed: "Packed", shipped: "Shipped", }; const initialOrders: Order[] = [ { id: "SO-10482", customer: "Hannah Okafor", items: 3, total: 128.5, shipping: 8.5, fulfillment: "unfulfilled", payment: "paid", }, { id: "SO-10481", customer: "Kenji Sato", items: 1, total: 64, shipping: 6, fulfillment: "packed", payment: "paid", }, { id: "SO-10479", customer: "Lucía Fernández", items: 5, total: 242.9, shipping: 0, fulfillment: "shipped", payment: "paid", }, ]; const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }); export default function ContextMenu13() { const [orders, setOrders] = React.useState(initialOrders); const [status, setStatus] = React.useState(""); function patch(id: string, next: Partial<Order>) { setOrders((current) => current.map((order) => (order.id === id ? { ...order, ...next } : order)), ); } return ( <div className="w-full max-w-xl rounded-xl border bg-card text-card-foreground"> <Table> <TableCaption className="mb-3 px-4 text-left text-xs"> <span aria-live="polite"> {status || "Right-click an order for fulfillment and refund actions."} </span> </TableCaption> <TableHeader> <TableRow className="hover:bg-transparent"> <TableHead className="pl-4">Order</TableHead> <TableHead className="hidden sm:table-cell">Customer</TableHead> <TableHead className="hidden sm:table-cell">Status</TableHead> <TableHead className="pr-4 text-right">Total</TableHead> </TableRow> </TableHeader> <TableBody> {orders.map((order) => { const closed = order.payment === "refunded" || order.payment === "canceled"; const statusBadge = closed ? ( <Badge variant="destructive" className="capitalize"> {order.payment} </Badge> ) : ( <Badge variant={ order.fulfillment === "shipped" ? "secondary" : "outline" } > {order.fulfillment === "shipped" ? ( <PackageCheck aria-hidden="true" /> ) : null} {fulfillmentLabel[order.fulfillment]} </Badge> ); return ( <ContextMenu key={order.id}> <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} render={<TableRow />} tabIndex={0} aria-label={`Order ${order.id} from ${order.customer}, ${fulfillmentLabel[order.fulfillment]}, ${order.payment}, ${currency.format(order.total)}`} className="outline-none focus-visible:bg-muted/50 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset data-popup-open:bg-muted" > <TableCell className="pl-4"> <span className="font-medium tabular-nums">{order.id}</span> <span className="block text-xs text-muted-foreground sm:hidden"> {order.customer} </span> </TableCell> <TableCell className="hidden sm:table-cell"> {order.customer} <span className="block text-xs text-muted-foreground"> {order.items} {order.items === 1 ? "item" : "items"} </span> </TableCell> <TableCell className="hidden sm:table-cell"> {statusBadge} </TableCell> <TableCell className="pr-4 text-right tabular-nums"> <span className={ closed ? "text-muted-foreground line-through" : "" } > {currency.format(order.total)} </span> <span className="mt-1 flex justify-end sm:hidden"> {statusBadge} </span> {order.payment === "partially refunded" ? ( <span className="block text-xs text-muted-foreground"> −{currency.format(order.shipping)} shipping </span> ) : null} </TableCell> </ContextMenuTrigger> <ContextMenuContent className="w-60"> <ContextMenuItem onClick={() => setStatus(`Opened order ${order.id}.`)} > <ExternalLink aria-hidden="true" /> View order <ContextMenuShortcut>↵</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem onClick={() => { void navigator.clipboard ?.writeText(order.id) .catch(() => {}); setStatus(`Copied ${order.id} to clipboard.`); }} > <Copy aria-hidden="true" /> Copy order number </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuGroup> <ContextMenuLabel>Fulfillment</ContextMenuLabel> <ContextMenuRadioGroup value={order.fulfillment} onValueChange={(value) => { patch(order.id, { fulfillment: value as Fulfillment }); setStatus( `${order.id} marked as ${fulfillmentLabel[value as Fulfillment].toLowerCase()}.`, ); }} > {(Object.keys(fulfillmentLabel) as Fulfillment[]).map( (key) => ( <ContextMenuRadioItem key={key} value={key} disabled={closed} > {fulfillmentLabel[key]} </ContextMenuRadioItem> ), )} </ContextMenuRadioGroup> </ContextMenuGroup> <ContextMenuItem disabled={closed} onClick={() => setStatus(`Packing slip for ${order.id} sent to printer.`) } > <Printer aria-hidden="true" /> Print packing slip <ContextMenuShortcut>⌘P</ContextMenuShortcut> </ContextMenuItem> <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger disabled={order.payment !== "paid"}> <ReceiptText aria-hidden="true" /> Refund </ContextMenuSubTrigger> <ContextMenuSubContent className="w-56"> <ContextMenuItem onClick={() => { patch(order.id, { payment: "refunded" }); setStatus( `Refunded ${currency.format(order.total)} to ${order.customer}.`, ); }} > Full refund <ContextMenuShortcut className="tracking-normal"> {currency.format(order.total)} </ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem disabled={order.shipping === 0} onClick={() => { patch(order.id, { payment: "partially refunded" }); setStatus( `Refunded ${currency.format(order.shipping)} shipping on ${order.id}.`, ); }} > Shipping only <ContextMenuShortcut className="tracking-normal"> {order.shipping === 0 ? "Free" : currency.format(order.shipping)} </ContextMenuShortcut> </ContextMenuItem> </ContextMenuSubContent> </ContextMenuSub> <ContextMenuItem variant="destructive" disabled={order.fulfillment === "shipped" || closed} onClick={() => { patch(order.id, { payment: "canceled" }); setStatus(`${order.id} canceled and restocked.`); }} > <Ban aria-hidden="true" /> Cancel order </ContextMenuItem> </ContextMenuContent> </ContextMenu> ); })} </TableBody> </Table> </div> ); }
npx shadcn@latest add @sevenui/component/context-menu-13pnpm dlx shadcn@latest add @sevenui/component/context-menu-13yarn dlx shadcn@latest add @sevenui/component/context-menu-13bunx --bun shadcn@latest add @sevenui/component/context-menu-13TSX"use client"; import { ChevronRight, Clipboard, FilePlus, FileCode2, FileText, Folder, FolderOpen, FolderPlus, GitBranch, PanelRight, Pencil, RotateCcw, Trash2, } from "lucide-react"; import * as React from "react"; import { cn } from "cn"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; // macOS browsers never turn Shift+F10 into a contextmenu event (Windows and // Linux do), so the shortcut the hint advertises is forwarded by hand there. function openMenuWithShiftF10(event: React.KeyboardEvent<HTMLElement>) { if (event.key !== "F10" || !event.shiftKey) return; if (!/Mac|iPhone|iPad/.test(navigator.userAgent)) return; event.preventDefault(); const rect = event.currentTarget.getBoundingClientRect(); event.currentTarget.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left + 8, clientY: rect.top + 8, }), ); } type GitStatus = "M" | "U"; type TreeNode = { id: string; name: string; parent: string | null; kind: "folder" | "file"; git?: GitStatus; }; const ROOT = "acme-web"; const initialNodes: TreeNode[] = [ { id: "src", name: "src", parent: null, kind: "folder" }, { id: "components", name: "components", parent: "src", kind: "folder" }, { id: "button", name: "button.tsx", parent: "components", kind: "file", git: "M", }, { id: "dialog", name: "dialog.tsx", parent: "components", kind: "file" }, { id: "lib", name: "lib", parent: "src", kind: "folder" }, { id: "utils", name: "format-date.ts", parent: "lib", kind: "file", git: "U", }, { id: "app", name: "app.tsx", parent: "src", kind: "file", git: "M" }, { id: "pkg", name: "package.json", parent: null, kind: "file" }, { id: "readme", name: "README.md", parent: null, kind: "file" }, ]; const gitLabel: Record<GitStatus, string> = { M: "modified", U: "untracked", }; function childrenOf(nodes: TreeNode[], parent: string | null) { return nodes .filter((node) => node.parent === parent) .sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "folder" ? -1 : 1, ); } function pathOf(nodes: TreeNode[], id: string): string { const node = nodes.find((item) => item.id === id); if (!node) return ""; return node.parent ? `${pathOf(nodes, node.parent)}/${node.name}` : node.name; } export default function ContextMenu14() { const [nodes, setNodes] = React.useState(initialNodes); const [expanded, setExpanded] = React.useState<string[]>([ "src", "components", ]); const [activeId, setActiveId] = React.useState("button"); const [renamingId, setRenamingId] = React.useState<string | null>(null); const [draftName, setDraftName] = React.useState(""); const [status, setStatus] = React.useState(""); const renameRef = React.useRef<HTMLInputElement>(null); const counter = React.useRef(0); // Rename starts only after the menu has fully closed, so focus returning // to the trigger cannot steal it back from the rename input. const pendingRename = React.useRef<TreeNode | null>(null); const renameClaimsFocus = React.useRef(false); React.useEffect(() => { if (!renamingId) return; renameRef.current?.focus(); renameRef.current?.select(); }, [renamingId]); function startRename(node: TreeNode) { setDraftName(node.name); setRenamingId(node.id); } function commitRename() { const name = draftName.trim(); if (renamingId && name) { setNodes((current) => current.map((node) => node.id === renamingId ? { ...node, name } : node, ), ); } setRenamingId(null); } function create(parent: TreeNode, kind: TreeNode["kind"]) { counter.current += 1; const node: TreeNode = { id: `new-${counter.current}`, name: kind === "file" ? "untitled.ts" : "new-folder", parent: parent.id, kind, git: kind === "file" ? "U" : undefined, }; setNodes((current) => [...current, node]); setExpanded((current) => current.includes(parent.id) ? current : [...current, parent.id], ); pendingRename.current = node; renameClaimsFocus.current = true; } function copyPath(value: string) { void navigator.clipboard?.writeText(value).catch(() => {}); setStatus(`Copied ${value}`); } function remove(id: string) { const doomed = new Set([id]); let grew = true; while (grew) { grew = false; for (const node of nodes) { if (node.parent && doomed.has(node.parent) && !doomed.has(node.id)) { doomed.add(node.id); grew = true; } } } setNodes((current) => current.filter((node) => !doomed.has(node.id))); if (doomed.has(activeId)) setActiveId(""); } function renderLevel(parent: string | null, depth: number): React.ReactNode { const level = childrenOf(nodes, parent); if (level.length === 0) return null; return ( <ul className="flex flex-col"> {level.map((node) => { const isFolder = node.kind === "folder"; const isOpen = expanded.includes(node.id); const isRenaming = renamingId === node.id; const path = pathOf(nodes, node.id); const Icon = isFolder ? isOpen ? FolderOpen : Folder : node.name.endsWith(".md") || node.name.endsWith(".json") ? FileText : FileCode2; return ( <li key={node.id}> {isRenaming ? ( <div className="flex items-center gap-1.5 py-0.5 pr-2" style={{ paddingLeft: depth * 12 + 22 }} > <Icon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> <input ref={renameRef} aria-label={`Rename ${node.name}`} value={draftName} onChange={(event) => setDraftName(event.target.value)} onBlur={commitRename} onKeyDown={(event) => { if (event.key === "Enter") commitRename(); if (event.key === "Escape") setRenamingId(null); }} className="h-6 min-w-0 flex-1 rounded-sm border border-ring bg-background px-1.5 font-mono text-xs outline-none ring-2 ring-ring/30" /> </div> ) : ( <ContextMenu onOpenChangeComplete={(open) => { if (!open && pendingRename.current) { startRename(pendingRename.current); pendingRename.current = null; } }} > <ContextMenuTrigger onKeyDown={openMenuWithShiftF10} render={<button type="button" />} aria-expanded={isFolder ? isOpen : undefined} aria-current={activeId === node.id ? "true" : undefined} aria-label={`${node.name}${node.git ? `, ${gitLabel[node.git]}` : ""}`} onClick={() => { if (isFolder) { setExpanded((current) => isOpen ? current.filter((id) => id !== node.id) : [...current, node.id], ); } else { setActiveId(node.id); } }} className={cn( "flex h-7 w-full items-center gap-1.5 rounded-sm pr-2 text-left font-mono text-xs outline-none hover:bg-sidebar-accent/70 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset data-popup-open:bg-sidebar-accent data-popup-open:ring-1 data-popup-open:ring-ring/40", activeId === node.id && "bg-sidebar-accent text-sidebar-accent-foreground", )} style={{ paddingLeft: depth * 12 + 6 }} > <ChevronRight aria-hidden="true" className={cn( "size-3.5 shrink-0 text-muted-foreground transition-transform", isOpen && "rotate-90", !isFolder && "invisible", )} /> <Icon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" /> <span className="min-w-0 flex-1 truncate"> {node.name} </span> {node.git ? ( <span aria-hidden="true" className={cn( "flex size-4 shrink-0 items-center justify-center rounded-xs text-[10px] font-semibold", node.git === "M" ? "bg-warning text-warning-foreground" : "bg-success text-success-foreground", )} > {node.git} </span> ) : null} </ContextMenuTrigger> <ContextMenuContent className="w-60" // A rename started from this menu owns focus next; returning // it to this row would blur the rename input straight away. finalFocus={() => { const keep = !renameClaimsFocus.current; renameClaimsFocus.current = false; return keep; }} > {isFolder ? ( <> <ContextMenuItem onClick={() => create(node, "file")}> <FilePlus aria-hidden="true" /> New file… </ContextMenuItem> <ContextMenuItem onClick={() => create(node, "folder")}> <FolderPlus aria-hidden="true" /> New folder… </ContextMenuItem> </> ) : ( <> <ContextMenuItem onClick={() => setActiveId(node.id)}> <FileCode2 aria-hidden="true" /> Open </ContextMenuItem> <ContextMenuItem onClick={() => setStatus(`Opened ${node.name} to the side.`) } > <PanelRight aria-hidden="true" /> Open to the side <ContextMenuShortcut>⌘↵</ContextMenuShortcut> </ContextMenuItem> </> )} <ContextMenuSeparator /> <ContextMenuSub> <ContextMenuSubTrigger> <Clipboard aria-hidden="true" /> Copy path </ContextMenuSubTrigger> <ContextMenuSubContent className="w-64"> <ContextMenuItem onClick={() => copyPath(`~/code/${ROOT}/${path}`)} > Absolute path <ContextMenuShortcut>⌥⌘C</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem onClick={() => copyPath(path)}> Relative path <ContextMenuShortcut>⇧⌥⌘C</ContextMenuShortcut> </ContextMenuItem> </ContextMenuSubContent> </ContextMenuSub> {node.git ? ( <ContextMenuSub> <ContextMenuSubTrigger> <GitBranch aria-hidden="true" /> Source control </ContextMenuSubTrigger> <ContextMenuSubContent className="w-48"> <ContextMenuItem onClick={() => { setNodes((current) => current.map((item) => item.id === node.id ? { ...item, git: undefined } : item, ), ); setStatus(`Staged ${path}.`); }} > Stage changes </ContextMenuItem> <ContextMenuItem variant="destructive" onClick={() => { if (node.git === "U") { remove(node.id); } else { setNodes((current) => current.map((item) => item.id === node.id ? { ...item, git: undefined } : item, ), ); } setStatus(`Discarded changes in ${path}.`); }} > Discard changes </ContextMenuItem> </ContextMenuSubContent> </ContextMenuSub> ) : null} <ContextMenuSeparator /> <ContextMenuItem onClick={() => { pendingRename.current = node; renameClaimsFocus.current = true; }} > <Pencil aria-hidden="true" /> Rename… <ContextMenuShortcut>F2</ContextMenuShortcut> </ContextMenuItem> <ContextMenuItem variant="destructive" onClick={() => { remove(node.id); setStatus(`Moved ${path} to Trash.`); }} > <Trash2 aria-hidden="true" /> Delete <ContextMenuShortcut>⌘⌫</ContextMenuShortcut> </ContextMenuItem> </ContextMenuContent> </ContextMenu> )} {isFolder && isOpen ? renderLevel(node.id, depth + 1) : null} </li> ); })} </ul> ); } const activePath = activeId ? pathOf(nodes, activeId) : ""; const changes = nodes.filter((node) => node.git).length; return ( <section aria-labelledby="context-menu-14-title" className="flex w-full max-w-xs flex-col overflow-hidden rounded-xl border bg-sidebar text-sidebar-foreground" > <header className="flex items-center justify-between gap-2 border-b border-sidebar-border px-3 py-2"> <h3 id="context-menu-14-title" className="text-[11px] font-semibold tracking-wide text-muted-foreground uppercase" > Explorer · {ROOT} </h3> <span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground tabular-nums"> <GitBranch aria-hidden="true" className="size-3" /> {changes} {changes === 1 ? "change" : "changes"} </span> </header> <nav aria-label="Project files" className="p-1.5"> {nodes.length === 0 ? ( <div className="flex flex-col items-center gap-2 px-2 py-6 text-center"> <p className="text-xs text-muted-foreground"> This workspace is empty. </p> <Button variant="outline" size="xs" onClick={() => { setNodes(initialNodes); setExpanded(["src", "components"]); setActiveId("button"); setStatus("Restored files from Trash."); }} > <RotateCcw aria-hidden="true" /> Restore from Trash </Button> </div> ) : ( renderLevel(null, 0) )} </nav> <footer className="border-t border-sidebar-border px-3 py-2 font-mono text-[11px] text-muted-foreground"> <p className="truncate">{activePath || "No file open"}</p> <p aria-live="polite" className="truncate font-sans"> {status || "Right-click a file or folder"} </p> </footer> </section> ); }
npx shadcn@latest add @sevenui/component/context-menu-14pnpm dlx shadcn@latest add @sevenui/component/context-menu-14yarn dlx shadcn@latest add @sevenui/component/context-menu-14bunx --bun shadcn@latest add @sevenui/component/context-menu-14