1"use client";
2
3import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
4import { SearchIcon } from "lucide-react";
5import type * as React from "react";
6import { cn } from "~/lib/utils";
7import {
8 Autocomplete,
9 AutocompleteCollection,
10 AutocompleteGroup,
11 AutocompleteGroupLabel,
12 AutocompleteInput,
13 AutocompleteItem,
14 AutocompleteList,
15 AutocompleteListHeading,
16 AutocompleteListVirtualized,
17} from "~/components/ui/autocomplete";
18import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles";
19import { Button } from "~/components/ui/button";
20
21const CommandDialog = CommandDialogPrimitive.Root;
22
23const CommandDialogPortal = CommandDialogPrimitive.Portal;
24
25function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
26 return <CommandDialogPrimitive.Trigger data-slot="command-dialog-trigger" {...props} />;
27}
28
29function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
30 return (
31 <CommandDialogPrimitive.Backdrop
32 className={cn(DIALOG_BACKDROP_CLASS, className)}
33 data-slot="command-dialog-backdrop"
34 {...props}
35 />
36 );
37}
38
39function CommandDialogViewport({ className, ...props }: CommandDialogPrimitive.Viewport.Props) {
40 return (
41 <CommandDialogPrimitive.Viewport
42 className={cn(
43 "pointer-events-none fixed inset-0 z-50 flex flex-col items-center px-4 py-[max(--spacing(4),4vh)] sm:py-[10vh]",
44 className,
45 )}
46 data-slot="command-dialog-viewport"
47 {...props}
48 />
49 );
50}
51
52function CommandDialogPopup({
53 className,
54 children,
55 onBackdropPointerDown,
56 ...props
57}: CommandDialogPrimitive.Popup.Props & {
58 onBackdropPointerDown?: React.PointerEventHandler<HTMLDivElement>;
59}) {
60 return (
61 <CommandDialogPortal>
62 <CommandDialogBackdrop onPointerDown={onBackdropPointerDown} />
63 <CommandDialogViewport>
64 <CommandDialogPrimitive.Popup
65 className={cn(
66 DIALOG_POPUP_CLASS,
67 "pointer-events-auto max-h-105 max-w-xl text-foreground",
68 className,
69 )}
70 data-slot="command-dialog-popup"
71 {...props}
72 >
73 {children}
74 </CommandDialogPrimitive.Popup>
75 </CommandDialogViewport>
76 </CommandDialogPortal>
77 );
78}
79
80function Command({
81 autoHighlight = "always",
82 keepHighlight = true,
83 ...props
84}: React.ComponentProps<typeof Autocomplete>) {
85 return (
86 <Autocomplete
87 autoHighlight={autoHighlight}
88 inline
89 keepHighlight={keepHighlight}
90 open
91 {...props}
92 />
93 );
94}
95
96function CommandInput({
97 className,
98 placeholder,
99 ...props
100}: React.ComponentProps<typeof AutocompleteInput>) {
101 return (
102 <div className="px-[var(--command-shell-inset)] py-1.5 [&_[data-slot=autocomplete-start-addon]]:ps-[calc(var(--command-shell-inset)+0.0625rem)]">
103 <AutocompleteInput
104 autoFocus
105 className={cn(
106 "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-placeholder *:data-[slot=autocomplete-input]:ps-9! sm:*:data-[slot=autocomplete-input]:ps-[calc(var(--command-shell-inset)+1.5rem)]!",
107 className,
108 )}
109 placeholder={placeholder}
110 size="lg"
111 startAddon={<SearchIcon className="translate-x-0.5 text-icon-muted" />}
112 {...props}
113 />
114 </div>
115 );
116}
117
118function CommandList({ className, ...props }: React.ComponentProps<typeof AutocompleteList>) {
119 return (
120 <AutocompleteList
121 className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
122 data-slot="command-list"
123 {...props}
124 />
125 );
126}
127
128// Pair with a LegendList child; the list owns scrolling and spacing.
129function CommandListVirtualized({
130 className,
131 ...props
132}: React.ComponentProps<typeof AutocompleteListVirtualized>) {
133 return (
134 <AutocompleteListVirtualized
135 className={cn("flex max-h-[inherit] flex-col", className)}
136 data-slot="command-list"
137 {...props}
138 />
139 );
140}
141
142function CommandListHeading({
143 className,
144 ...props
145}: React.ComponentProps<typeof AutocompleteListHeading>) {
146 return (
147 <AutocompleteListHeading className={className} data-slot="command-group-label" {...props} />
148 );
149}
150
151function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
152 return (
153 <div
154 className={cn(
155 "relative min-h-0 overflow-hidden rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl bg-transparent **:data-[slot=scroll-area-scrollbar]:mt-2 [touch-action:pan-y]",
156 className,
157 )}
158 {...props}
159 />
160 );
161}
162
163function CommandGroup({ className, ...props }: React.ComponentProps<typeof AutocompleteGroup>) {
164 return <AutocompleteGroup className={className} data-slot="command-group" {...props} />;
165}
166
167function CommandGroupLabel({
168 className,
169 ...props
170}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
171 return (
172 <AutocompleteGroupLabel className={className} data-slot="command-group-label" {...props} />
173 );
174}
175
176function CommandCollection({ ...props }: React.ComponentProps<typeof AutocompleteCollection>) {
177 return <AutocompleteCollection data-slot="command-collection" {...props} />;
178}
179
180// Pass `active` when the palette tracks the highlighted row itself; the
181// primitive's hover and keyboard highlight are then ignored so the two never
182// disagree, and the row shows the active surface instead.
183function CommandItem({
184 className,
185 active,
186 ...props
187}: React.ComponentProps<typeof AutocompleteItem> & { active?: boolean }) {
188 return (
189 <AutocompleteItem
190 className={cn(
191 "py-1.5 data-selected:bg-foreground/[0.06] data-highlighted:bg-foreground/[0.09] data-highlighted:text-foreground [&[data-highlighted][data-selected]]:bg-foreground/[0.09] [&[data-highlighted][data-selected]]:text-foreground",
192 active !== undefined &&
193 "cursor-pointer hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit data-selected:bg-transparent data-selected:text-inherit [&[data-highlighted][data-selected]]:bg-transparent [&[data-highlighted][data-selected]]:text-inherit",
194 active && "bg-accent! text-accent-foreground!",
195 className,
196 )}
197 data-active={active || undefined}
198 data-slot="command-item"
199 {...props}
200 />
201 );
202}
203
204function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
205 return (
206 <kbd
207 className={cn(
208 "ms-auto font-medium font-sans text-secondary-label text-xs tracking-widest",
209 className,
210 )}
211 data-slot="command-shortcut"
212 {...props}
213 />
214 );
215}
216
217function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
218 return (
219 <div
220 className={cn(
221 "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-[var(--command-content-inset)] py-2.5 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0",
222 className,
223 )}
224 data-slot="command-footer"
225 {...props}
226 />
227 );
228}
229
230function CommandFooterAction({
231 className,
232 ...props
233}: Omit<React.ComponentProps<typeof Button>, "size" | "variant">) {
234 return (
235 <Button
236 {...props}
237 variant="ghost-muted"
238 size="xs"
239 className={cn("h-auto px-2 text-xs hover:bg-transparent", className)}
240 />
241 );
242}
243
244export {
245 Command,
246 CommandCollection,
247 CommandDialog,
248 CommandDialogPopup,
249 CommandDialogTrigger,
250 CommandFooter,
251 CommandFooterAction,
252 CommandGroup,
253 CommandGroupLabel,
254 CommandInput,
255 CommandItem,
256 CommandList,
257 CommandListHeading,
258 CommandListVirtualized,
259 CommandPanel,
260 CommandShortcut,
261};