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