Sections
Get Started
UI
Components
- Animated Beam
- Animated List
- Animated Logo
- Animated Theme Toggler
- Avatar Circles
- Bento Grid
- Big Name Text
- Border Beam
- Dock
- Feature Card
- Filter Dropdown
- Footer Link
- Hero Badge
- Hero Buttons
- Hyper Text
- Lens
- Magic Card
- Marquee
- Meteors
- Onboarding Form
- Pricing Card
- Scroll Text
- Section Header
- Site Header
- Status Card
- Status
- Tweet Card
"use client";
import React from "react";
import {
ArrowUpIcon,
AtSignIcon,
BookIcon,
CirclePlusIcon,
Globe,
LayoutGridIcon,
PaperclipIcon,
PlusIcon,
XIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/registry/sase/avatar";
import { Badge } from "@/registry/sase/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/registry/sase/command";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/sase/dropdown-menu";
import { Field, FieldLabel } from "@/registry/sase/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/sase/input-group";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/sase/popover";
import { Switch } from "@/registry/sase/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/sase/tooltip";
const SAMPLE_DATA = {
mentionable: [
{
title: "shadcn",
type: "user" as const,
image: "https://github.com/shadcn.png",
workspace: "Personal",
},
{
title: "vercel",
type: "user" as const,
image: "https://github.com/vercel.png",
workspace: "Work",
},
{
title: "openai",
type: "user" as const,
image: "https://github.com/openai.png",
workspace: "Work",
},
{ title: "Getting Started Guide", type: "page" as const, icon: "📄" },
{ title: "Project Roadmap 2024", type: "page" as const, icon: "🗺️" },
{ title: "Meeting Notes - Q1", type: "page" as const, icon: "📝" },
{ title: "Design System Docs", type: "page" as const, icon: "🎨" },
],
models: [
{ name: "Auto", badge: null },
{ name: "Agent Mode", badge: "NEW" },
{ name: "Plan Mode", badge: "BETA" },
],
};
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[number];
}) {
if (item.type === "user") {
return (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
);
}
return <span>{item.icon}</span>;
}
export function NotionPromptForm() {
const [mentions, setMentions] = React.useState<string[]>([]);
const [mentionPopoverOpen, setMentionPopoverOpen] = React.useState(false);
const [modelPopoverOpen, setModelPopoverOpen] = React.useState(false);
const [scopeMenuOpen, setScopeMenuOpen] = React.useState(false);
const [selectedModel, setSelectedModel] = React.useState(
SAMPLE_DATA.models[0],
);
const grouped = React.useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
if (!acc[item.type]) {
acc[item.type] = [];
}
if (!mentions.includes(item.title)) {
acc[item.type].push(item);
}
return acc;
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>,
);
}, [mentions]);
const hasMentions = mentions.length > 0;
return (
<form className="[--radius:1.2rem]">
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
asChild
onFocusCapture={(e) => e.stopPropagation()}
>
<PopoverTrigger asChild>
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
>
<AtSignIcon /> {!hasMentions && "Add context"}
</InputGroupButton>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
<CommandEmpty>No pages found</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? "Pages" : "Users"}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue]);
setMentionPopoverOpen(false);
}}
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention,
);
if (!item) {
return null;
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full pl-2!"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention));
}}
>
<MentionableIcon item={item} />
{item.title}
<XIcon />
</InputGroupButton>
);
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label="Attach file"
>
<PaperclipIcon />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Attach file</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
{selectedModel.name}
</InputGroupButton>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model);
}
}}
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
>
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
<Globe /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="web-search">
<Globe /> Web Search{" "}
<Switch
id="web-search"
className="ml-auto"
defaultChecked
/>
</label>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="apps">
<LayoutGridIcon /> Apps and Integrations
<Switch id="apps" className="ml-auto" defaultChecked />
</label>
</DropdownMenuItem>
<DropdownMenuItem>
<CirclePlusIcon /> All Sources I can access
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
<Command>
<CommandInput
placeholder="Find or use knowledge in..."
autoFocus
/>
<CommandList>
<CommandEmpty>No knowledge found</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
console.log("Selected user:", user.title);
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
- {user.workspace}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<BookIcon /> Help Center
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<PlusIcon /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
We'll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label="Send"
className="ml-auto rounded-full"
variant="default"
size="icon-sm"
>
<ArrowUpIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
);
}
Installation
pnpm dlx shadcn@latest add https://sase-ui.vercel.app/r/command.json
Usage
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";<Command>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search Emoji</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
</CommandList>
</Command>Examples
Notion-style Prompt
"use client";
import React from "react";
import {
ArrowUpIcon,
AtSignIcon,
BookIcon,
CirclePlusIcon,
Globe,
LayoutGridIcon,
PaperclipIcon,
PlusIcon,
XIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/registry/sase/avatar";
import { Badge } from "@/registry/sase/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/registry/sase/command";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/sase/dropdown-menu";
import { Field, FieldLabel } from "@/registry/sase/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/sase/input-group";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/sase/popover";
import { Switch } from "@/registry/sase/switch";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/sase/tooltip";
const SAMPLE_DATA = {
mentionable: [
{
title: "shadcn",
type: "user" as const,
image: "https://github.com/shadcn.png",
workspace: "Personal",
},
{
title: "vercel",
type: "user" as const,
image: "https://github.com/vercel.png",
workspace: "Work",
},
{
title: "openai",
type: "user" as const,
image: "https://github.com/openai.png",
workspace: "Work",
},
{ title: "Getting Started Guide", type: "page" as const, icon: "📄" },
{ title: "Project Roadmap 2024", type: "page" as const, icon: "🗺️" },
{ title: "Meeting Notes - Q1", type: "page" as const, icon: "📝" },
{ title: "Design System Docs", type: "page" as const, icon: "🎨" },
],
models: [
{ name: "Auto", badge: null },
{ name: "Agent Mode", badge: "NEW" },
{ name: "Plan Mode", badge: "BETA" },
],
};
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[number];
}) {
if (item.type === "user") {
return (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
);
}
return <span>{item.icon}</span>;
}
export function NotionPromptForm() {
const [mentions, setMentions] = React.useState<string[]>([]);
const [mentionPopoverOpen, setMentionPopoverOpen] = React.useState(false);
const [modelPopoverOpen, setModelPopoverOpen] = React.useState(false);
const [scopeMenuOpen, setScopeMenuOpen] = React.useState(false);
const [selectedModel, setSelectedModel] = React.useState(
SAMPLE_DATA.models[0],
);
const grouped = React.useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
if (!acc[item.type]) {
acc[item.type] = [];
}
if (!mentions.includes(item.title)) {
acc[item.type].push(item);
}
return acc;
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>,
);
}, [mentions]);
const hasMentions = mentions.length > 0;
return (
<form className="[--radius:1.2rem]">
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
asChild
onFocusCapture={(e) => e.stopPropagation()}
>
<PopoverTrigger asChild>
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
>
<AtSignIcon /> {!hasMentions && "Add context"}
</InputGroupButton>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
<CommandEmpty>No pages found</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? "Pages" : "Users"}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue]);
setMentionPopoverOpen(false);
}}
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention,
);
if (!item) {
return null;
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full pl-2!"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention));
}}
>
<MentionableIcon item={item} />
{item.title}
<XIcon />
</InputGroupButton>
);
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label="Attach file"
>
<PaperclipIcon />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Attach file</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
{selectedModel.name}
</InputGroupButton>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model);
}
}}
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
>
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
<Globe /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="web-search">
<Globe /> Web Search{" "}
<Switch
id="web-search"
className="ml-auto"
defaultChecked
/>
</label>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="apps">
<LayoutGridIcon /> Apps and Integrations
<Switch id="apps" className="ml-auto" defaultChecked />
</label>
</DropdownMenuItem>
<DropdownMenuItem>
<CirclePlusIcon /> All Sources I can access
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
<Command>
<CommandInput
placeholder="Find or use knowledge in..."
autoFocus
/>
<CommandList>
<CommandEmpty>No knowledge found</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
console.log("Selected user:", user.title);
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
- {user.workspace}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<BookIcon /> Help Center
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<PlusIcon /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
We'll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label="Send"
className="ml-auto rounded-full"
variant="default"
size="icon-sm"
>
<ArrowUpIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
);
}