Import source: src/components/shell/command-palette
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ArrowRight, Search, SunMoon, Zap } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { routeItems } from "@/lib/routes";
|
||||
import { useUiStore } from "@/stores/ui-store";
|
||||
|
||||
const commandActions = [
|
||||
{ label: "Create website", path: "/websites/new", keywords: ["website", "new"] },
|
||||
{ label: "Add domain", path: "/domains/new", keywords: ["domain", "dns"] },
|
||||
{ label: "Create database", path: "/databases/new", keywords: ["mysql", "postgres"] },
|
||||
{ label: "Create email", path: "/email/accounts", keywords: ["mailbox", "mail"] },
|
||||
{ label: "Open terminal", path: "/advanced/terminal", keywords: ["shell", "ssh"] },
|
||||
{ label: "Run backup", path: "/backups/new", keywords: ["snapshot", "restore"] },
|
||||
{ label: "View logs", path: "/advanced/logs", keywords: ["error", "access"] },
|
||||
{ label: "Open settings", path: "/settings", keywords: ["preferences"] },
|
||||
{ label: "Switch server", path: "/servers", keywords: ["server"] },
|
||||
{ label: "Switch account", path: "/users", keywords: ["account"] },
|
||||
];
|
||||
|
||||
type CommandResult = {
|
||||
label: string;
|
||||
description: string;
|
||||
path: string;
|
||||
category: string;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
export function CommandPalette() {
|
||||
const open = useUiStore((state) => state.commandOpen);
|
||||
const setOpen = useUiStore((state) => state.setCommandOpen);
|
||||
const setTheme = useUiStore((state) => state.setTheme);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const results = useMemo<CommandResult[]>(() => {
|
||||
const term = query.toLowerCase();
|
||||
const pages = routeItems
|
||||
.filter((item) =>
|
||||
[item.label, item.description, item.category, ...item.keywords].some((value) =>
|
||||
value.toLowerCase().includes(term),
|
||||
),
|
||||
)
|
||||
.slice(0, 12)
|
||||
.map((item) => ({
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
path: item.path,
|
||||
category: item.category,
|
||||
icon: item.icon,
|
||||
}));
|
||||
const actions = commandActions
|
||||
.filter((item) =>
|
||||
[item.label, ...item.keywords].some((value) => value.toLowerCase().includes(term)),
|
||||
)
|
||||
.map((item) => ({ ...item, description: "Command", category: "Actions" }));
|
||||
return query
|
||||
? [...actions, ...pages]
|
||||
: [
|
||||
...commandActions
|
||||
.slice(0, 5)
|
||||
.map((item) => ({ ...item, description: "Command", category: "Actions" })),
|
||||
...pages.slice(0, 7),
|
||||
];
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Command palette</DialogTitle>
|
||||
<DialogDescription>
|
||||
Search tools, pages, websites, domains, users, servers, settings, and actions.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className="h-10 pl-9 text-base"
|
||||
placeholder="Type a command or resource name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-h-[52vh] overflow-y-auto rounded-lg border border-border/60">
|
||||
<button
|
||||
className="group flex w-full items-center gap-3 border-b border-border/50 px-3 py-2.5 text-left text-sm transition-colors hover:bg-primary/[0.07]"
|
||||
onClick={() => {
|
||||
setTheme("system");
|
||||
setOpen(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors group-hover:bg-primary/15 group-hover:text-primary">
|
||||
<SunMoon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="flex-1">Toggle theme</span>
|
||||
<span className="text-xs text-muted-foreground">Theme</span>
|
||||
</button>
|
||||
{results.map((result) => {
|
||||
const Icon = result.icon ?? Zap;
|
||||
return (
|
||||
<Link
|
||||
key={`${result.category}-${result.path}-${result.label}`}
|
||||
href={result.path}
|
||||
onClick={() => setOpen(false)}
|
||||
className="group flex items-center gap-3 border-b border-border/50 px-3 py-2.5 text-sm transition-colors last:border-b-0 hover:bg-primary/[0.07]"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors group-hover:bg-primary/15 group-hover:text-primary">
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium">{result.label}</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{result.description}
|
||||
</span>
|
||||
</span>
|
||||
<span className="hidden text-xs text-muted-foreground sm:inline">
|
||||
{result.category}
|
||||
</span>
|
||||
<ArrowRight className="h-3.5 w-3.5 -translate-x-1 text-muted-foreground opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{!results.length ? (
|
||||
<div className="px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
No matching command.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Press Enter after selecting a row</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<kbd>Ctrl</kbd>
|
||||
<kbd>K</kbd>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandPaletteHotkey() {
|
||||
const setOpen = useUiStore((state) => state.setCommandOpen);
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [setOpen]);
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user