From 6c487802a54b13eb9ad921485a435543f51aad93 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Sun, 17 May 2026 01:20:06 -0700 Subject: [PATCH] =?UTF-8?q?feat(ux):=20=E2=8C=98K/Ctrl-K=20command=20palet?= =?UTF-8?q?te=20for=20fast=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New CommandPalette over the existing unused cmdk primitive, mounted in the (app) layout. Grouped nav destinations mirroring the sidebar; admin/ manage entries gated by the same isAdmin + navPerms the layout already resolves (so it never offers a route the guard would bounce). Opens on Cmd/Ctrl-K, type-to-filter, Enter to jump. --- app/(app)/layout.tsx | 2 + components/command-palette.tsx | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 components/command-palette.tsx diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index d102a50..6bbab55 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -1,6 +1,7 @@ import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' import { AppSidebar } from '@/components/app-sidebar' +import { CommandPalette } from '@/components/command-palette' import { ConfirmProvider } from '@/components/ui/confirm' import { getRoleLabelMap } from '@/lib/role-labels' @@ -77,6 +78,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod return ( +
+}) { + const router = useRouter() + const [open, setOpen] = useState(false) + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault() + setOpen(o => !o) + } + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, []) + + const allowed = useCallback( + (d: Dest) => { + if (d.adminOnly) return isAdmin + if (d.perm) return isAdmin || !!navPerms[d.perm] + return true + }, + [isAdmin, navPerms], + ) + + const go = (href: string) => { + setOpen(false) + router.push(href) + } + + return ( + + + + No matches. + {NAV.map(section => { + const items = section.items.filter(allowed) + if (items.length === 0) return null + return ( + + {items.map(d => ( + go(d.href)}> + {d.label} + + ))} + + ) + })} + + + ) +}