diff options
| author | Dhravya <[email protected]> | 2024-04-13 09:55:29 -0700 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-04-13 09:55:29 -0700 |
| commit | 57e699a6ee35b161cf69aa82bec6c50f114b1055 (patch) | |
| tree | a40d3c983c4d7661797e1c18591019cc09c3f083 /apps/web/src/components | |
| parent | merge (diff) | |
| parent | fix edge case for getting metadata (diff) | |
| download | supermemory-57e699a6ee35b161cf69aa82bec6c50f114b1055.tar.xz supermemory-57e699a6ee35b161cf69aa82bec6c50f114b1055.zip | |
conflicts
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 163 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 172 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 110 | ||||
| -rw-r--r-- | apps/web/src/components/ui/command.tsx | 15 |
4 files changed, 347 insertions, 113 deletions
diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 08b9a750..d0523581 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -10,8 +10,12 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; +import { FilterMemories, FilterSpaces } from "./FilterCombobox"; import { useMemory } from "@/contexts/MemoryContext"; +import { Loader, Plus, X } from "lucide-react"; +import { StoredContent } from "@/server/db/schema"; +import { cleanUrl } from "@/lib/utils"; +import { motion } from "framer-motion"; export function AddMemoryPage() { const { addMemory } = useMemory(); @@ -73,6 +77,8 @@ export function AddMemoryPage() { } export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { + const { addMemory } = useMemory(); + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); const inputRef = useRef<HTMLInputElement>(null); @@ -112,6 +118,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { placeholder="Title of the note" data-modal-autofocus value={name} + disabled={loading} onChange={(e) => setName(e.target.value)} /> <Editor @@ -134,16 +141,41 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { <button onClick={() => { if (check()) { - closeDialog(); + setLoading(true); + addMemory( + { + content, + title: name, + type: "note", + url: "https://notes.supermemory.dhr.wtf/", + image: "", + savedAt: new Date(), + }, + selectedSpacesId, + ).then(closeDialog); } }} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + disabled={loading} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" > - Add + <motion.div + initial={{ x: "-50%", y: "-100%" }} + animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} + className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" + > + <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> + </motion.div> + <motion.div + initial={{ y: "0%" }} + animate={loading && { opacity: 0, y: "30%" }} + > + Add + </motion.div> </button> <DialogClose type={undefined} - className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + disabled={loading} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" > Cancel </DialogClose> @@ -153,6 +185,37 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { } export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { + const { addSpace } = useMemory(); + + const inputRef = useRef<HTMLInputElement>(null); + const [name, setName] = useState(""); + + const [loading, setLoading] = useState(false); + + const [selected, setSelected] = useState<StoredContent[]>([]); + + function check(): boolean { + const data = { + name: name.trim(), + }; + if (!data.name || data.name.length < 1) { + if (!inputRef.current) { + alert("Please enter a name for the note"); + return false; + } + inputRef.current.value = ""; + inputRef.current.placeholder = "Please enter a title for the space"; + inputRef.current.dataset["error"] = "true"; + setTimeout(() => { + inputRef.current!.placeholder = "Enter the name of the space"; + inputRef.current!.dataset["error"] = "false"; + }, 500); + inputRef.current.focus(); + return false; + } + return true; + } + return ( <div className="md:w-[40vw]"> <DialogHeader> @@ -160,23 +223,99 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { </DialogHeader> <Label className="mt-5 block">Name</Label> <Input + ref={inputRef} placeholder="Enter the name of the space" type="url" data-modal-autofocus - className="bg-rgray-4 mt-2 w-full" + value={name} + disabled={loading} + onChange={(e) => setName(e.target.value)} + className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10" /> - <Label className="mt-5 block">Memories</Label> + {selected.length > 0 && ( + <> + <Label className="mt-5 block">Add Memories</Label> + <div className="flex min-h-5 flex-col items-center justify-center py-2"> + {selected.map((i) => ( + <MemorySelectedItem + key={i.id} + onRemove={() => + setSelected((prev) => prev.filter((p) => p.id !== i.id)) + } + {...i} + /> + ))} + </div> + </> + )} <DialogFooter> - <DialogClose + <FilterMemories + selected={selected} + setSelected={setSelected} + disabled={loading} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70" + > + <Plus className="h-5 w-5" /> + Memory + </FilterMemories> + <button type={undefined} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + onClick={() => { + if (check()) { + setLoading(true); + addSpace( + name, + selected.map((s) => s.id), + ).then(() => closeDialog()); + } + }} + disabled={loading} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + <motion.div + initial={{ x: "-50%", y: "-100%" }} + animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} + className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" + > + <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> + </motion.div> + <motion.div + initial={{ y: "0%" }} + animate={loading && { opacity: 0, y: "30%" }} + > + Add + </motion.div> + </button> + <DialogClose + disabled={loading} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" > - Add - </DialogClose> - <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> Cancel </DialogClose> </DialogFooter> </div> ); } + +export function MemorySelectedItem({ + id, + title, + url, + image, + onRemove, +}: StoredContent & { onRemove: () => void }) { + return ( + <div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden"> + <img src={image ?? "/icons/logo_without_bg.png"} className="h-5 w-5" /> + <button + onClick={onRemove} + data-icon + className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none" + > + <X className="h-5 w-5 scale-90" /> + </button> + <span>{title}</span> + <span className="ml-auto block opacity-50">{cleanUrl(url)}</span> + </div> + ); +} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index bd432215..80319ab1 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -20,9 +20,12 @@ import { } from "@/components/ui/popover"; import { SpaceIcon } from "@/assets/Memories"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { useMemory } from "@/contexts/MemoryContext"; +import { SearchResult, useMemory } from "@/contexts/MemoryContext"; +import { useDebounce } from "@/hooks/useDebounce"; +import { StoredContent } from "@/server/db/schema"; -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { +export interface FilterSpacesProps + extends React.ButtonHTMLAttributes<HTMLButtonElement> { side?: "top" | "bottom"; align?: "end" | "start" | "center"; onClose?: () => void; @@ -42,7 +45,7 @@ export function FilterSpaces({ setSelectedSpaces, name, ...props -}: Props) { +}: FilterSpacesProps) { const { spaces } = useMemory(); const [open, setOpen] = React.useState(false); @@ -150,26 +153,57 @@ export function FilterSpaces({ ); } +export type FilterMemoriesProps = { + side?: "top" | "bottom"; + align?: "end" | "start" | "center"; + onClose?: () => void; + selected: StoredContent[]; + setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; +} & React.ButtonHTMLAttributes<HTMLButtonElement>; + export function FilterMemories({ className, side = "bottom", align = "center", onClose, - selectedSpaces, - setSelectedSpaces, - name, + selected, + setSelected, ...props -}: Props) { - const { spaces } = useMemory(); +}: FilterMemoriesProps) { + const { search } = useMemory(); + const [open, setOpen] = React.useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + const query = useDebounce(searchQuery, 500); - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); + const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); + const [isSearching, setIsSearching] = React.useState(false); + + const results = React.useMemo(() => { + console.log("use memo"); + return searchResults.map((r) => r.memory); + }, [searchResults]); + + console.log("memoized", results); + + React.useEffect(() => { + const q = query.trim(); + if (q.length > 0) { + setIsSearching(true); + (async () => { + const results = await search(q, { + filter: { + memories: true, + spaces: false, + }, + }); + setSearchResults(results); + setIsSearching(false); + })(); + } else { + setSearchResults([]); + } + }, [query]); React.useEffect(() => { if (!open) { @@ -177,6 +211,7 @@ export function FilterMemories({ } }, [open]); + console.log(searchResults); return ( <AnimatePresence mode="popLayout"> <LayoutGroup> @@ -191,15 +226,7 @@ export function FilterMemories({ )} {...props} > - <SpaceIcon className="mr-1 h-5 w-5" /> - {name} - <ChevronsUpDown className="h-4 w-4" /> - <div - data-state-on={selectedSpaces.length > 0} - className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" - > - {selectedSpaces.length} - </div> + {props.children} </button> </PopoverTrigger> <PopoverContent @@ -208,56 +235,53 @@ export function FilterMemories({ side={side} className="w-[200px] p-0" > - <Command - filter={(val, search) => - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } - > - <CommandInput placeholder="Filter spaces..." /> - <CommandList asChild> - <motion.div layoutScroll> - <CommandEmpty>Nothing found</CommandEmpty> - <CommandGroup> - {sortedSpaces.map((space) => ( - <CommandItem - key={space.id} - value={space.id.toString()} - onSelect={(val) => { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1, transition: { delay: 0.05 } }} - transition={{ duration: 0.15 }} - layout - layoutId={`space-combobox-${space.id}`} - className="text-rgray-11" - > - <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name} - {selectedSpaces.includes(space.id)} - <Check - data-state-on={selectedSpaces.includes(space.id)} - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </motion.div> - </CommandItem> - ))} - </CommandGroup> - </motion.div> + <Command shouldFilter={false}> + <CommandInput + isSearching={isSearching} + value={searchQuery} + onValueChange={setSearchQuery} + placeholder="Filter memories..." + /> + <CommandList> + <CommandGroup> + <CommandEmpty className="text-rgray-11 py-5 text-center text-sm"> + {isSearching + ? "Searching..." + : query.trim().length > 0 + ? "Nothing Found" + : "Search something"} + </CommandEmpty> + {results.map((m) => ( + <CommandItem + key={m.id} + value={m.id.toString()} + onSelect={(val) => { + setSelected((prev) => + prev.find((p) => p.id === parseInt(val)) + ? prev.filter((v) => v.id !== parseInt(val)) + : [...prev, m], + ); + }} + asChild + > + <div className="text-rgray-11"> + <img + src={m.image ?? "/icons/logo_without_bg.png"} + className="mr-2 h-4 w-4" + /> + {m.title} + <Check + data-state-on={ + selected.find((i) => i.id === m.id) !== undefined + } + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> + </div> + </CommandItem> + ))} + </CommandGroup> </CommandList> </Command> </PopoverContent> diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index f671b72f..970deb68 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -10,6 +10,7 @@ import { Input, InputWithIcon } from "../ui/input"; import { ArrowUpRight, Edit3, + Loader, MoreHorizontal, Plus, Search, @@ -25,7 +26,7 @@ import { } from "../ui/dropdown-menu"; import { useEffect, useMemo, useRef, useState } from "react"; import { Variant, useAnimate, motion } from "framer-motion"; -import { useMemory } from "@/contexts/MemoryContext"; +import { SearchResult, useMemory } from "@/contexts/MemoryContext"; import { SpaceIcon } from "@/assets/Memories"; import { Dialog, @@ -44,10 +45,12 @@ import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; import { ExpandedSpace } from "./ExpandedSpace"; import { StoredContent, StoredSpace } from "@/server/db/schema"; import Image from "next/image"; +import { useDebounce } from "@/hooks/useDebounce"; +import { searchMemoriesAndSpaces } from "@/actions/db"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories } = useMemory(); + const { spaces, deleteSpace, freeMemories, search } = useMemory(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [addMemoryState, setAddMemoryState] = useState< @@ -55,6 +58,11 @@ export function MemoriesBar() { >(null); const [expandedSpace, setExpandedSpace] = useState<number | null>(null); + const [searchQuery, setSearcyQuery] = useState(""); + const [searchLoading, setSearchLoading] = useState(false); + const query = useDebounce(searchQuery, 500); + + const [searchResults, setSearchResults] = useState<SearchResult[]>([]); if (expandedSpace) { return ( @@ -65,14 +73,37 @@ export function MemoriesBar() { ); } + useEffect(() => { + const q = query.trim(); + if (q.length < 1) { + setSearchResults([]); + return; + } + + setSearchLoading(true); + + (async () => { + setSearchResults(await search(q)); + setSearchLoading(false); + })(); + }, [query]); + return ( <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> <div className="w-full px-8"> <h1 className="w-full text-2xl">Your Memories</h1> <InputWithIcon placeholder="Search" - icon={<Search className="text-rgray-11 h-5 w-5 opacity-50" />} + icon={ + searchLoading ? ( + <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> + ) : ( + <Search className="text-rgray-11 h-5 w-5 opacity-50" /> + ) + } className="bg-rgray-4 mt-2 w-full" + value={searchQuery} + onChange={(e) => setSearcyQuery(e.target.value)} /> </div> <div className="mt-2 flex w-full px-8"> @@ -123,17 +154,32 @@ export function MemoriesBar() { ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" > - {spaces.map((space) => ( - <SpaceItem - onDelete={() => {}} - key={space.id} - //onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map((m) => ( - <MemoryItem {...m} key={m.id} /> - ))} + {query.trim().length > 0 ? ( + <> + {searchResults.map(({ type, space, memory }, i) => ( + <> + {type === "memory" && <MemoryItem {...memory!} key={i} />} + {type === "space" && ( + <SpaceItem {...space!} key={i} onDelete={() => {}} /> + )} + </> + ))} + </> + ) : ( + <> + {spaces.map((space) => ( + <SpaceItem + onDelete={() => {}} + key={space.id} + //onClick={() => setExpandedSpace(space.id)} + {...space} + /> + ))} + {freeMemories.map((m) => ( + <MemoryItem {...m} key={m.id} /> + ))} + </> + )} </div> </div> ); @@ -149,15 +195,29 @@ const SpaceExitVariant: Variant = { }, }; -export function MemoryItem({ id, title, image }: StoredContent) { +export function MemoryItem({ id, title, image, type }: StoredContent) { + const name = title + ? title.length > 10 + ? title.slice(0, 10) + "..." + : title + : "<no title>"; + return ( <div className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"> <button data-space-text className="focus-visible:outline-none"> - {title} + {name} </button> <div className="flex h-24 w-24 items-center justify-center"> - <img className="h-16 w-16" id={id.toString()} src={image!} /> + {type === "page" ? ( + <img className="h-16 w-16" id={id.toString()} src={image!} /> + ) : type === "note" ? ( + <div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md"> + <Text className="h-10 w-10" /> + </div> + ) : ( + <></> + )} </div> </div> ); @@ -186,6 +246,8 @@ export function SpaceItem({ return cachedMemories.filter((m) => m.space === id); }, [cachedMemories]); + const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; + return ( <motion.div ref={itemRef} @@ -194,7 +256,7 @@ export function SpaceItem({ className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" > <button data-space-text className="focus-visible:outline-none"> - {name} + {_name} </button> <SpaceMoreButton isOpen={moreDropdownOpen} @@ -287,6 +349,12 @@ export function SpaceItem({ id={id.toString()} images={spaceMemories.map((c) => c.image).reverse() as string[]} /> + ) : spaceMemories.length > 1 ? ( + <MemoryWithImages2 + className="h-24 w-24" + id={id.toString()} + images={spaceMemories.map((c) => c.image).reverse() as string[]} + /> ) : spaceMemories.length === 1 ? ( <MemoryWithImage className="h-24 w-24" @@ -294,11 +362,7 @@ export function SpaceItem({ image={spaceMemories[0].image!} /> ) : ( - <MemoryWithImages2 - className="h-24 w-24" - id={id.toString()} - images={spaceMemories.map((c) => c.image).reverse() as string[]} - /> + <div className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"></div> )} </motion.div> ); diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index 74b7f2e8..5fd64a6c 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -3,10 +3,11 @@ import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; -import { Search } from "lucide-react"; +import { Loader, Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { isSea } from "node:sea"; const Command = React.forwardRef< React.ElementRef<typeof CommandPrimitive>, @@ -39,13 +40,19 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandInput = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { + isSearching?: boolean; + } +>(({ className, isSearching = false, ...props }, ref) => ( <div className="border-rgray-6 flex items-center border-b px-3" cmdk-input-wrapper="" > - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + {isSearching ? ( + <Loader className="shrink-9 mr-2 h-4 w-4 animate-spin opacity-50" /> + ) : ( + <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + )} <CommandPrimitive.Input ref={ref} className={cn( |