Command Menu
Command palettes with groups, shortcuts, and search.
Fromblocks.so
Command Menu with Groups
"use client"
import {
BellIcon,
CalendarIcon,
ChartColumnIcon,
ChartPieIcon,
CircleQuestionMarkIcon,
ClockIcon,
FileTextIcon,
KanbanIcon,
KeyboardIcon,
LayoutDashboardIcon,
LogOutIcon,
MessageSquareIcon,
PaletteIcon,
SettingsIcon,
SquareCheckIcon,
TargetIcon,
TrendingUpIcon,
UsersIcon,
ZapIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { Kbd } from "@/components/ui/kbd"
const workspaceItems = [
{ icon: LayoutDashboardIcon, label: "Dashboard" },
{ icon: KanbanIcon, label: "Projects" },
{ icon: SquareCheckIcon, label: "Tasks" },
{ icon: CalendarIcon, label: "Calendar" },
{ icon: UsersIcon, label: "Team members" },
{ icon: MessageSquareIcon, label: "Messages" },
{ icon: FileTextIcon, label: "Documents" },
{ icon: BellIcon, label: "Notifications" },
{ icon: ClockIcon, label: "Time tracking" },
{ icon: TargetIcon, label: "Goals" },
]
const analyticsItems = [
{ icon: ChartColumnIcon, label: "Overview" },
{ icon: TrendingUpIcon, label: "Performance" },
{ icon: ChartPieIcon, label: "Reports" },
{ icon: ZapIcon, label: "Insights" },
]
const settingsItems = [
{ icon: SettingsIcon, label: "Preferences" },
{ icon: PaletteIcon, label: "Appearance" },
{ icon: KeyboardIcon, label: "Keyboard shortcuts" },
{ icon: CircleQuestionMarkIcon, label: "Help & support" },
{ icon: LogOutIcon, label: "Sign out" },
]
export default function CommandMenu01() {
const [open, setOpen] = useState(true)
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((prev) => !prev)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<>
<Button onClick={() => setOpen(true)} variant="outline">
Open Command Menu
</Button>
<CommandDialog
className="-translate-y-1/2 top-1/2"
onOpenChange={setOpen}
open={open}
showCloseButton={false}
>
<Command>
<CommandInput
className="h-12"
placeholder="Type a command or search..."
/>
<CommandList className="h-[320px] max-h-[320px]">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Workspace">
{workspaceItems.map((item) => (
<CommandItem key={item.label}>
<item.icon className="mr-2 h-5 w-5" />
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading="Analytics">
{analyticsItems.map((item) => (
<CommandItem key={item.label}>
<item.icon className="mr-2 h-5 w-5" />
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
<CommandGroup heading="Settings">
{settingsItems.map((item) => (
<CommandItem key={item.label}>
<item.icon className="mr-2 h-5 w-5" />
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
<div className="flex h-12 items-center justify-end border-t px-3">
<button
className="flex items-center gap-1 text-muted-foreground text-sm hover:text-foreground"
onClick={() => setOpen(false)}
type="button"
>
<span>Close</span>
<Kbd className="ml-1">Esc</Kbd>
</button>
</div>
</Command>
</CommandDialog>
</>
)
}npx shadcn@latest add https://blocks.so/r/command-menu-01.jsonCommand Menu with Keyboard Shortcuts
"use client"
import {
ArrowRightIcon,
AtSignIcon,
CopyIcon,
DownloadIcon,
FileIcon,
FileSearchIcon,
KeyboardIcon,
LayoutTemplateIcon,
LinkIcon,
LogOutIcon,
MessageSquareIcon,
MonitorIcon,
PencilIcon,
PlusIcon,
SendIcon,
SettingsIcon,
UserIcon,
UsersIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
export default function CommandMenu02() {
const [open, setOpen] = useState(true)
const [inputValue, setInputValue] = useState("")
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((prev) => !prev)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<>
<Button onClick={() => setOpen(true)} variant="outline">
Open Command Menu
</Button>
<Dialog onOpenChange={setOpen} open={open}>
<DialogHeader className="sr-only">
<DialogTitle>Command Menu</DialogTitle>
<DialogDescription>
Use the command menu to navigate through the app.
</DialogDescription>
</DialogHeader>
<DialogContent
className="gap-0 overflow-hidden rounded-xl border-border/50 p-0 shadow-lg sm:max-w-lg"
showCloseButton={false}
>
<Command className="p-0 **:data-[slot=input-group]:h-10! **:data-[slot=command-input-wrapper]:grow **:data-[slot=input-group]:rounded-none! **:data-[slot=input-group]:border-0 **:data-[slot=input-group]:bg-transparent **:data-[slot=command-input-wrapper]:p-0 **:data-[slot=input-group-addon]:pl-0!">
<div className="flex h-12 items-center gap-2 border-border/50 border-b px-4">
<CommandInput
className="text-[15px]"
onValueChange={setInputValue}
placeholder="What do you need?"
value={inputValue}
/>
<button
className="flex shrink-0 items-center"
onClick={() => setOpen(false)}
type="button"
>
<Kbd>Esc</Kbd>
</button>
</div>
<CommandList className="max-h-[400px] py-2">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<SettingsIcon aria-hidden />
Account Settings...
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>,</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<UserIcon aria-hidden />
Switch Workspace...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<LogOutIcon aria-hidden />
Log Out
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>Q</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Documents">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<FileIcon aria-hidden />
Search Documents...
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>F</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<PlusIcon aria-hidden />
Create New Document...
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>N</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<FileIcon aria-hidden />
Upload Document...
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>U</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Signing">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<SendIcon aria-hidden />
Request Signature...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<PencilIcon aria-hidden />
Sign a Document...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<UsersIcon aria-hidden />
Bulk Send for Signature...
</CommandItem>
</CommandGroup>
<CommandGroup heading="Templates">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<LayoutTemplateIcon aria-hidden />
Search Templates...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<PlusIcon aria-hidden />
Create New Template...
</CommandItem>
</CommandGroup>
<CommandGroup heading="General">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<MonitorIcon aria-hidden />
Change Theme...
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>T</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<CopyIcon aria-hidden />
Copy Current URL
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>โง</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Navigation">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to <strong className="font-semibold">Inbox</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">Action Required</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">
Waiting for Others
</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">Completed</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to <strong className="font-semibold">Drafts</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">Templates</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">Archive</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to <strong className="font-semibold">Trash</strong>
</span>
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<ArrowRightIcon aria-hidden />
<span>
Go to
<strong className="font-semibold">Settings</strong>
</span>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Quick Actions">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<LinkIcon aria-hidden />
Copy Signing Link
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<DownloadIcon aria-hidden />
Download Document
</CommandItem>
</CommandGroup>
<CommandGroup heading="Help">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<FileSearchIcon aria-hidden />
Search Help Center...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<MessageSquareIcon aria-hidden />
Send Feedback...
</CommandItem>
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<AtSignIcon aria-hidden />
Contact Support
</CommandItem>
</CommandGroup>
<CommandGroup heading="Keyboard Shortcuts">
<CommandItem
className="mx-2 rounded-lg py-2.5"
onSelect={() => setOpen(false)}
>
<KeyboardIcon aria-hidden />
View Keyboard Shortcuts
<CommandShortcut>
<KbdGroup>
<Kbd>โ</Kbd>
<Kbd>/</Kbd>
</KbdGroup>
</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
</>
)
}npx shadcn@latest add https://blocks.so/r/command-menu-02.jsonCommand Menu with Documentation Search
"use client"
import { ArrowRightIcon, CornerDownLeftIcon } from "lucide-react"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Kbd } from "@/components/ui/kbd"
const navItems = [
{ href: "/", label: "Home", keywords: ["home", "main", "index"] },
{
href: "/dashboard",
label: "Dashboard",
keywords: ["dashboard", "overview"],
},
{
href: "/settings",
label: "Settings",
keywords: ["settings", "preferences"],
},
]
const pageGroups = [
{
name: "Getting Started",
pages: [
{
name: "Introduction",
href: "/docs/introduction",
keywords: ["intro", "start"],
},
{
name: "Installation",
href: "/docs/installation",
keywords: ["install", "setup"],
},
{
name: "Quick Start",
href: "/docs/quick-start",
keywords: ["quick", "begin"],
},
],
},
{
name: "Utilities",
pages: [
{
name: "Typography",
href: "/docs/utilities/typography",
keywords: ["text", "font"],
},
{
name: "Colors",
href: "/docs/utilities/colors",
keywords: ["color", "theme"],
},
{
name: "Spacing",
href: "/docs/utilities/spacing",
keywords: ["margin", "padding"],
},
],
},
]
const colorGroups = [
{
name: "Neutral",
colors: [
{
name: "Neutral 50",
className: "neutral-50",
value: "oklch(0.985 0 0)",
},
{
name: "Neutral 100",
className: "neutral-100",
value: "oklch(0.97 0 0)",
},
{
name: "Neutral 200",
className: "neutral-200",
value: "oklch(0.922 0 0)",
},
{
name: "Neutral 500",
className: "neutral-500",
value: "oklch(0.556 0 0)",
},
{
name: "Neutral 900",
className: "neutral-900",
value: "oklch(0.205 0 0)",
},
],
},
{
name: "Blue",
colors: [
{
name: "Blue 50",
className: "blue-50",
value: "oklch(0.97 0.014 254.604)",
},
{
name: "Blue 500",
className: "blue-500",
value: "oklch(0.623 0.214 259.815)",
},
{
name: "Blue 600",
className: "blue-600",
value: "oklch(0.546 0.245 262.881)",
},
],
},
]
export default function CommandMenu03() {
const router = useRouter()
const [open, setOpen] = useState(true)
const copyToClipboard = useCallback((text: string) => {
navigator.clipboard.writeText(text)
}, [])
const runCommand = useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
setOpen((prev) => !prev)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
return (
<>
<Button onClick={() => setOpen(true)} variant="outline">
Open Command Menu
</Button>
<Dialog onOpenChange={setOpen} open={open}>
<DialogContent className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 **:data-[slot=dialog-close]:top-3! **:data-[slot=dialog-close]:right-3! sm:max-w-md dark:bg-neutral-900 dark:ring-neutral-800">
<DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>
Search for a command to run...
</DialogDescription>
</DialogHeader>
<Command className="rounded-none! bg-transparent p-0 **:data-[slot=input-group]:h-9! **:data-[slot=input-group]:rounded-md! **:data-[slot=input-group]:border-input **:data-[slot=input-group]:bg-input/50 **:data-[slot=command-input-wrapper]:p-0 **:data-[slot=command-input]:pr-9 **:data-[slot=command-input]:pl-2! **:data-[slot=input-group-addon]:pl-3!">
<CommandInput placeholder="Search documentation..." />
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
<CommandEmpty className="py-12 text-center text-muted-foreground text-sm">
No results found.
</CommandEmpty>
{navItems.length > 0 && (
<CommandGroup
className="p-0! **:[[cmdk-group-heading]]:scroll-mt-16 **:[[cmdk-group-heading]]:p-3! **:[[cmdk-group-heading]]:pb-1!"
heading="Pages"
>
{navItems.map((item) => (
<CommandItem
className="h-9 rounded-md border border-transparent px-3! font-medium hover:border-input hover:bg-input/50"
key={item.href}
keywords={item.keywords}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
value={`Navigation ${item.label}`}
>
<ArrowRightIcon aria-hidden="true" className="size-4" />
{item.label}
</CommandItem>
))}
</CommandGroup>
)}
{pageGroups.map((group) => (
<CommandGroup
className="p-0! **:[[cmdk-group-heading]]:scroll-mt-16 **:[[cmdk-group-heading]]:p-3! **:[[cmdk-group-heading]]:pb-1!"
heading={group.name}
key={group.name}
>
{group.pages.map((page) => {
const isComponent = page.href.includes("/components/")
return (
<CommandItem
className="h-9 rounded-md border border-transparent px-3! font-medium hover:border-input hover:bg-input/50"
key={page.href}
keywords={page.keywords}
onSelect={() => {
runCommand(() => router.push(page.href))
}}
value={`${group.name} ${page.name}`}
>
{isComponent ? (
<div className="aspect-square size-4 rounded-full border border-muted-foreground border-dashed" />
) : (
<ArrowRightIcon
aria-hidden="true"
className="size-4"
/>
)}
{page.name}
</CommandItem>
)
})}
</CommandGroup>
))}
{colorGroups.map((colorGroup) => (
<CommandGroup
className="p-0! **:[[cmdk-group-heading]]:p-3!"
heading={colorGroup.name}
key={colorGroup.name}
>
{colorGroup.colors.map((color) => (
<CommandItem
className="h-9 rounded-md border border-transparent px-3! font-medium hover:border-input hover:bg-input/50"
key={color.className}
keywords={["color", color.name, color.className]}
onSelect={() => {
runCommand(() => copyToClipboard(color.value))
}}
value={color.className}
>
<div
className="aspect-square size-4 rounded-sm border"
style={{ backgroundColor: color.value }}
/>
{color.className}
<CommandShortcut className="font-mono font-normal text-xs tabular-nums tracking-normal">
{color.value}
</CommandShortcut>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
<div className="absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 font-medium text-muted-foreground text-xs dark:border-t-neutral-700 dark:bg-neutral-800">
<Kbd>
<CornerDownLeftIcon aria-hidden="true" className="size-3" />
</Kbd>
Select
</div>
</DialogContent>
</Dialog>
</>
)
}npx shadcn@latest add https://blocks.so/r/command-menu-03.jsonCommand Menu with Global Search
"use client"
import {
FileArchiveIcon,
FileSpreadsheetIcon,
FileTextIcon,
FolderIcon,
ImageIcon,
MessageCircleIcon,
XIcon,
} from "lucide-react"
import { Command as CommandPrimitive } from "cmdk"
import { type ComponentType, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { cn } from "@/lib/utils"
export type SearchResultKind = "chat" | "image" | "document" | "project"
export type SearchResult = {
id: string
kind: SearchResultKind
title: string
/** Shown under the title while searching. Chats use a text excerpt; files use their type. */
snippet: string
date: string
/** Images only: CSS background for the thumbnail. */
thumbnail?: string
}
type Filter = "all" | SearchResultKind
const filters: { value: Filter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "chat", label: "Chats" },
{ value: "image", label: "Images" },
{ value: "document", label: "Documents" },
{ value: "project", label: "Projects" },
]
const fileIcons: Record<string, ComponentType<{ className?: string }>> = {
pdf: FileTextIcon,
docx: FileTextIcon,
xlsx: FileSpreadsheetIcon,
zip: FileArchiveIcon,
}
const kindIcons: Record<
SearchResultKind,
ComponentType<{ className?: string }>
> = {
chat: MessageCircleIcon,
image: ImageIcon,
document: FileTextIcon,
project: FolderIcon,
}
const lastOpenedChats: SearchResult[] = [
{
id: "c1",
kind: "chat",
title: "Q3 launch checklist",
snippet: "Pin the launch date once legal signs off on the new terms.",
date: "Today",
},
{
id: "c2",
kind: "chat",
title: "Draft onboarding email sequence",
snippet:
"Three emails over five days, starting the morning after launch, each with one clear action.",
date: "Today",
},
]
const recentChats: SearchResult[] = [
{
id: "c3",
kind: "chat",
title: "Compare pricing page layouts",
snippet:
"Three-column pricing works when the middle tier is the one most people should pick. Ship it with the launch.",
date: "Yesterday",
},
{
id: "c4",
kind: "chat",
title: "Summarize customer interviews",
snippet:
"Eight of twelve customers mentioned invoice export first. Nobody brought up the dashboard redesign.",
date: "Yesterday",
},
{
id: "c5",
kind: "chat",
title: "Fix flaky checkout test",
snippet:
"The test races the payment webhook. Wait for the order status instead of a fixed delay.",
date: "Sep 4",
},
{
id: "c6",
kind: "chat",
title: "Write release notes for v2.4",
snippet:
"Lead with the two launch features people asked for most, then list fixes in one short block.",
date: "Sep 2",
},
{
id: "c7",
kind: "chat",
title: "Plan database index cleanup",
snippet:
"Drop the four unused indexes on orders first; they cost the most on writes.",
date: "Aug 30",
},
{
id: "c8",
kind: "chat",
title: "Brainstorm team offsite agenda",
snippet:
"Half a day on the roadmap, half a day on the launch retro, evening free.",
date: "Aug 28",
},
{
id: "c9",
kind: "chat",
title: "Review API rate limit options",
snippet:
"Token bucket per API key, 600 requests a minute. Raise the ceiling for launch week.",
date: "Aug 26",
},
]
const images: SearchResult[] = [
{
id: "i1",
kind: "image",
title: "launch-hero-banner.png",
snippet: "Image",
date: "Sep 3",
thumbnail: "linear-gradient(135deg, #f97316, #db2777)",
},
{
id: "i2",
kind: "image",
title: "launch-pricing-table.png",
snippet: "Image",
date: "Aug 20",
thumbnail: "linear-gradient(135deg, #0ea5e9, #6366f1)",
},
{
id: "i3",
kind: "image",
title: "launch-onboarding-flow.png",
snippet: "Image",
date: "Aug 27",
thumbnail: "linear-gradient(135deg, #22c55e, #0d9488)",
},
{
id: "i4",
kind: "image",
title: "dashboard-dark-mode.png",
snippet: "Image",
date: "Aug 15",
thumbnail: "linear-gradient(135deg, #1e293b, #475569)",
},
{
id: "i5",
kind: "image",
title: "launch-social-card.jpg",
snippet: "Image",
date: "Aug 12",
thumbnail: "linear-gradient(135deg, #a855f7, #f43f5e)",
},
{
id: "i6",
kind: "image",
title: "checkout-error-states.png",
snippet: "Image",
date: "Jul 30",
thumbnail: "linear-gradient(135deg, #eab308, #f97316)",
},
]
const documents: SearchResult[] = [
{
id: "d1",
kind: "document",
title: "Q3_launch_plan.pdf",
snippet: "PDF",
date: "Sep 1",
},
{
id: "d2",
kind: "document",
title: "launch-pricing-proposal.docx",
snippet: "Document",
date: "Aug 22",
},
{
id: "d3",
kind: "document",
title: "pre-launch-interview-notes.md",
snippet: "Document",
date: "Aug 19",
},
{
id: "d4",
kind: "document",
title: "launch-assets-final.zip",
snippet: "File",
date: "Aug 11",
},
{
id: "d5",
kind: "document",
title: "pricing-experiments.xlsx",
snippet: "Spreadsheet",
date: "Aug 14",
},
]
const projects: SearchResult[] = [
{
id: "p1",
kind: "project",
title: "Q3 product launch",
snippet: "Project",
date: "Sep 5",
},
{
id: "p2",
kind: "project",
title: "Launch pricing and packaging",
snippet: "Project",
date: "Aug 30",
},
{
id: "p3",
kind: "project",
title: "Post-launch onboarding redesign",
snippet: "Project",
date: "Aug 21",
},
{
id: "p4",
kind: "project",
title: "Checkout reliability",
snippet: "Project",
date: "Jul 28",
},
]
const searchResults: SearchResult[] = [
...lastOpenedChats,
...recentChats,
...images,
...documents,
...projects,
]
export default function CommandMenu04() {
const [open, setOpen] = useState(true)
return (
<>
<Button onClick={() => setOpen(true)} variant="outline">
Search
</Button>
<GlobalSearch
lastOpened={lastOpenedChats}
onOpenChange={setOpen}
onSelect={() => setOpen(false)}
open={open}
recent={recentChats}
results={searchResults}
/>
</>
)
}
export function GlobalSearch({
open,
onOpenChange,
onSelect,
lastOpened,
recent,
results,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onSelect: (result: SearchResult) => void
lastOpened: SearchResult[]
recent: SearchResult[]
results: SearchResult[]
}) {
const [query, setQuery] = useState("")
const [filter, setFilter] = useState<Filter>("all")
const needle = query.trim().toLowerCase()
const matches = results.filter(
(result) =>
(filter === "all" || result.kind === filter) &&
`${result.title} ${result.snippet}`.toLowerCase().includes(needle)
)
const reset = () => {
setQuery("")
setFilter("all")
}
return (
<CommandDialog
className="-translate-y-1/2 top-1/2 rounded-2xl! sm:max-w-[720px]"
description="Search chats, images, documents, and projects"
onOpenChange={(next) => {
if (!next) {
reset()
}
onOpenChange(next)
}}
open={open}
title="Global search"
>
<Command shouldFilter={false}>
<div className="flex h-16 shrink-0 items-center py-5 pr-4 pl-6">
<CommandPrimitive.Input
className="min-w-0 flex-1 bg-transparent text-base tracking-[-0.02em] outline-hidden placeholder:text-muted-foreground"
onValueChange={setQuery}
placeholder="Search..."
value={query}
/>
<div className="flex shrink-0 items-center gap-1">
{query && (
<>
<button
className="px-2 text-muted-foreground text-sm hover:text-foreground"
onClick={reset}
type="button"
>
Clear
</button>
<span aria-hidden className="mx-1 h-5 w-px bg-border" />
</>
)}
<button
aria-label="Close search"
className="flex size-9 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
onClick={() => onOpenChange(false)}
type="button"
>
<XIcon className="size-5" />
</button>
</div>
</div>
<CommandList className="h-[396px] max-h-[396px] pb-3">
{needle ? (
<>
<div
aria-label="Result type filters"
className="sticky top-0 z-10 flex gap-1 bg-popover px-6 pb-3"
role="tablist"
>
{filters.map((option) => {
const selected = option.value === filter
return (
<button
aria-selected={selected}
className={cn(
"rounded-full px-3.5 py-1.5 text-sm transition-colors",
selected
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
key={option.value}
onClick={() => setFilter(option.value)}
role="tab"
type="button"
>
{option.label}
</button>
)
})}
</div>
<CommandEmpty>No results for โ{query}โ.</CommandEmpty>
{filter === "image" ? (
<div className="grid grid-cols-4 gap-3 px-6">
{matches.map((result) => (
<CommandItem
className="block aspect-square rounded-xl! bg-transparent! p-0 [&_svg]:hidden"
key={result.id}
onSelect={() => onSelect(result)}
value={result.id}
>
<span
aria-label={result.title}
className="block size-full rounded-xl ring-1 ring-foreground/10 ring-offset-2 ring-offset-popover group-data-selected/command-item:ring-foreground/40"
role="img"
style={{ background: result.thumbnail }}
/>
</CommandItem>
))}
</div>
) : (
matches.map((result) => (
<ResultRow
key={result.id}
needle={needle}
onSelect={onSelect}
result={result}
/>
))
)}
</>
) : (
<>
<CommandGroup
className="p-0 **:[[cmdk-group-heading]]:px-6 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:font-normal! **:[[cmdk-group-heading]]:text-sm!"
heading="Last opened"
>
{lastOpened.map((result) => (
<ResultRow
key={result.id}
onSelect={onSelect}
result={result}
/>
))}
</CommandGroup>
<CommandGroup
className="mt-5 p-0 **:[[cmdk-group-heading]]:px-6 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:font-normal! **:[[cmdk-group-heading]]:text-sm!"
heading="Recent chats"
>
{recent.map((result) => (
<ResultRow
key={result.id}
onSelect={onSelect}
result={result}
/>
))}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</CommandDialog>
)
}
function ResultRow({
result,
needle,
onSelect,
}: {
result: SearchResult
needle?: string
onSelect: (result: SearchResult) => void
}) {
const extension = result.title.split(".").pop() ?? ""
const Icon =
result.kind === "document"
? (fileIcons[extension] ?? FileTextIcon)
: kindIcons[result.kind]
return (
<CommandItem
className="mx-3 min-h-12 gap-3 rounded-xl! px-3 py-2 text-sm"
onSelect={() => onSelect(result)}
value={result.id}
>
<span className="flex size-8 shrink-0 items-center justify-center">
<Icon
className={cn("size-5!", extension === "pdf" && "text-red-500")}
/>
</span>
<span className="min-w-0 flex-1">
<span className="block truncate leading-5">
<Highlight needle={needle} text={result.title} />
</span>
{needle && (
<span className="block truncate pt-0.5 text-muted-foreground leading-5">
<Highlight needle={needle} text={result.snippet} />
</span>
)}
</span>
{needle && (
<span className="shrink-0 pl-4 text-muted-foreground text-sm">
{result.date}
</span>
)}
</CommandItem>
)
}
function Highlight({ text, needle }: { text: string; needle?: string }) {
const start = needle ? text.toLowerCase().indexOf(needle) : -1
if (!needle || start === -1) {
return text
}
const end = start + needle.length
return (
<>
{text.slice(0, start)}
<mark className="bg-transparent font-semibold text-foreground">
{text.slice(start, end)}
</mark>
{text.slice(end)}
</>
)
}npx shadcn@latest add https://blocks.so/r/command-menu-04.json