From 761376fd6b8bc76b714c331a9640ec722d00b72d Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:10:56 +0330 Subject: [PATCH] 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. --- src/components/shell/command-palette.tsx | 168 +++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/components/shell/command-palette.tsx diff --git a/src/components/shell/command-palette.tsx b/src/components/shell/command-palette.tsx new file mode 100644 index 0000000..6a1843f --- /dev/null +++ b/src/components/shell/command-palette.tsx @@ -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(() => { + 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 ( + + + + Command palette + + Search tools, pages, websites, domains, users, servers, settings, and actions. + + +
+
+ + setQuery(event.target.value)} + className="h-10 pl-9 text-base" + placeholder="Type a command or resource name" + /> +
+
+ + {results.map((result) => { + const Icon = result.icon ?? Zap; + return ( + 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]" + > + + + + + {result.label} + + {result.description} + + + + {result.category} + + + + ); + })} + {!results.length ? ( +
+ No matching command. +
+ ) : null} +
+
+ Press Enter after selecting a row + + Ctrl + K + +
+
+
+
+ ); +} + +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; +}