From dec6920bab71986fa55e16868d195fa62762272e Mon Sep 17 00:00:00 2001 From: SrashtiChauhan Date: Mon, 25 May 2026 22:35:28 +0530 Subject: [PATCH] feat: add global command palette with keyboard shortcuts --- frontend/app/components/AppShell.tsx | 8 +- frontend/app/components/CommandPalette.tsx | 232 +++++++++++++++++++++ frontend/tailwind.config.ts | 16 ++ package-lock.json | 17 ++ package.json | 1 + 5 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 frontend/app/components/CommandPalette.tsx create mode 100644 frontend/tailwind.config.ts diff --git a/frontend/app/components/AppShell.tsx b/frontend/app/components/AppShell.tsx index 2c5a6d7..e7f9b39 100644 --- a/frontend/app/components/AppShell.tsx +++ b/frontend/app/components/AppShell.tsx @@ -1,4 +1,5 @@ "use client"; +import CommandPalette from "./CommandPalette"; import { usePathname } from "next/navigation"; import Sidebar from "./Sidebar"; @@ -19,8 +20,13 @@ export default function AppShell({ children }: { children: React.ReactNode }) { return (
+ + {!hideSidebar && } -
{children}
+ +
+ {children} +
); } diff --git a/frontend/app/components/CommandPalette.tsx b/frontend/app/components/CommandPalette.tsx new file mode 100644 index 0000000..3e05876 --- /dev/null +++ b/frontend/app/components/CommandPalette.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; + +const commands = [ + { id: 1, label: "Dashboard", href: "/dashboard" }, + { id: 2, label: "Projects", href: "/projects" }, + { id: 3, label: "Insights Analytics", href: "/insights/analytics" }, + { id: 4, label: "Insights Tasks", href: "/insights/tasks" }, + { id: 5, label: "Insights Feed", href: "/insights/feed" }, +]; + +export default function CommandPalette() { + const router = useRouter(); + + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState(0); + const [recentCommands, setRecentCommands] = useState([]); + + + useEffect(() => { + const stored = localStorage.getItem("flowforge-recent-commands"); + + if (stored) { + setRecentCommands(JSON.parse(stored)); + } + }, []); + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + const isShortcut = + (event.ctrlKey || event.metaKey) && + event.key.toLowerCase() === "k"; + + if (isShortcut) { + event.preventDefault(); + setOpen((prev) => !prev); + } + + if (event.key === "Escape") { + setOpen(false); + setQuery(""); + } + } + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + const filteredCommands = useMemo(() => { + if (!query.trim()) return commands; + + return commands.filter((command) => + command.label.toLowerCase().includes(query.toLowerCase()) + ); + }, [query]); + + useEffect(() => { + function handleNavigation(event: KeyboardEvent) { + if (!open) return; + + if (event.key === "ArrowDown") { + event.preventDefault(); + + setSelected((prev) => + prev === filteredCommands.length - 1 ? 0 : prev + 1 + ); + } + + if (event.key === "ArrowUp") { + event.preventDefault(); + + setSelected((prev) => + prev === 0 ? filteredCommands.length - 1 : prev - 1 + ); + } + + if (event.key === "Enter") { + const command = filteredCommands[selected]; + + if (command) { + const updatedRecent = [ + command, + ...recentCommands.filter((item) => item.id !== command.id), + ].slice(0, 5); + + setRecentCommands(updatedRecent); + + localStorage.setItem( + "flowforge-recent-commands", + JSON.stringify(updatedRecent) + ); + + router.push(command.href); + + setOpen(false); + setQuery(""); + } + } + } + + window.addEventListener("keydown", handleNavigation); + + return () => { + window.removeEventListener("keydown", handleNavigation); + }; + }, [open, filteredCommands, selected, router]); + + if (!open) return null; + + return ( +
+
+ +
+ + search + + + setQuery(event.target.value)} + placeholder="Search pages and actions..." + className="flex-1 bg-transparent outline-none text-on-surface placeholder:text-outline" + /> + + + ESC + +
+ + {!query && recentCommands.length > 0 && ( + <> +
+ Recent +
+ + {recentCommands.map((command) => ( + + ))} + +
+ +)} + +
+ +
+ Navigation +
+ + {filteredCommands.length === 0 ? ( +
+ + + search_off + + +

+ No commands found +

+ +

+ Try searching for pages or actions. +

+
+ ) : ( + filteredCommands.map((command, index) => ( + + )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..bc6d3d6 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,16 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + ], + + theme: { + extend: {}, + }, + + plugins: [require("tailwindcss-animate")], +}; + +export default config; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e9f31d6..c48fb07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "lucide": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "tailwindcss-animate": "^1.0.7", "zod": "^4.4.3" } }, @@ -6407,6 +6408,22 @@ } } }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT", + "peer": true + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/package.json b/package.json index 860b2bd..cc37aa6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "lucide": "^1.8.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "tailwindcss-animate": "^1.0.7", "zod": "^4.4.3" } }