#591 - app search bar UI

This commit is contained in:
Saeed Vaziry
2025-05-15 22:38:24 +03:00
parent b8ba83949b
commit edd4ba1bc2
6 changed files with 271 additions and 31 deletions

29
package-lock.json generated
View File

@ -8,6 +8,7 @@
"@headlessui/react": "^2.2.0",
"@hookform/resolvers": "^5.0.1",
"@inertiajs/react": "^2.0.0",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
@ -1201,6 +1202,34 @@
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.13.tgz",
"integrity": "sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.13",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-slot": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz",

View File

@ -29,6 +29,7 @@
"@headlessui/react": "^2.2.0",
"@hookform/resolvers": "^5.0.1",
"@inertiajs/react": "^2.0.0",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",

View File

@ -0,0 +1,43 @@
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '@/components/ui/command';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { CommandIcon, SearchIcon } from 'lucide-react';
export default function AppCommand() {
const [open, setOpen] = useState(false);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
return (
<div>
<Button className="px-1!" variant="outline" size="sm" onClick={() => setOpen(true)}>
<span className="sr-only">Open command menu</span>
<SearchIcon className="ml-1 size-3" />
Search...
<span className="bg-accent flex h-6 items-center justify-center rounded-sm border px-2 text-xs">
<CommandIcon className="mr-1 size-3" /> K
</span>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Create server</CommandItem>
<CommandItem>Create project</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</div>
);
}

View File

@ -3,24 +3,28 @@ import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator } from
import { ProjectSwitch } from '@/components/project-switch';
import { SlashIcon } from 'lucide-react';
import { ServerSwitch } from '@/components/server-switch';
import AppCommand from '@/components/app-command';
export function AppHeader() {
return (
<header className="bg-background -ml-1 flex h-12 shrink-0 items-center gap-2 border-b p-4 md:-ml-2">
<SidebarTrigger className="-ml-1 md:hidden" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<ProjectSwitch />
</BreadcrumbItem>
<BreadcrumbSeparator>
<SlashIcon />
</BreadcrumbSeparator>
<BreadcrumbItem>
<ServerSwitch />
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<header className="bg-background -ml-1 flex h-12 shrink-0 items-center justify-between gap-2 border-b p-4 md:-ml-2">
<div className="flex items-center">
<SidebarTrigger className="-ml-1 md:hidden" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<ProjectSwitch />
</BreadcrumbItem>
<BreadcrumbSeparator>
<SlashIcon />
</BreadcrumbSeparator>
<BreadcrumbItem>
<ServerSwitch />
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<AppCommand />
</header>
);
}

View File

@ -0,0 +1,155 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -20,6 +20,17 @@ import InputError from '@/components/ui/input-error';
import UserSelect from '@/components/user-select';
import { useForm } from '@inertiajs/react';
import { LoaderCircleIcon, TrashIcon } from 'lucide-react';
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
AlertDialogCancel,
AlertDialogAction,
} from '@/components/ui/alert-dialog';
function AddUser({ project }: { project: Project }) {
const [open, setOpen] = useState(false);
@ -84,30 +95,27 @@ function RemoveUser({ project, user }: { project: Project; user: User }) {
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
<TrashIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Remove user</DialogTitle>
<DialogDescription>Remove user from {project.name}.</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove user</AlertDialogTitle>
<AlertDialogDescription>Remove user from {project.name}.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button onClick={submit} variant="destructive" disabled={form.processing}>
{form.processing && <LoaderCircleIcon className="animate-spin" />}
Remove user
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}