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:
arsam
2026-09-18 23:10:56 +03:30
parent 36e2e34c6d
commit 761376fd6b
+168
View File
@@ -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;
}