feat(nav): global ⌘K command palette
Adds a global command launcher reachable from anywhere via ⌘K (Mac) / Ctrl+K (Win), or "/" when no input is focused. Three sections: - **Navigation** — every primary route from the top-bar nav, plus the full Admin sub-menu, role-gated against the session. - **Recent activity** — last 5 audits and last 5 device observations, lazy-loaded once on first open from /api/dashboard/overview. - **Companies** — fuzzy search against the active customer list from /api/companies, kicks in once the user types 2+ characters. Selecting a company deep-links to /configuration-items?company=<id>. Top-bar exposes a small "Search · ⌘K" pill on md+ for discoverability, sized to fit between the Status indicator and the Theme toggle. Implementation: - shadcn `command` primitive (uses cmdk under the hood); declined the bundled dialog overwrite to keep our existing dialog.tsx. - CommandPalette mounted once in app/layout.tsx so it lives outside the AppNavigation re-renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3fa41c25a3
commit
a0894fe946
7 changed files with 521 additions and 0 deletions
|
|
@ -349,6 +349,15 @@ below is the working backlog; expand as we go.
|
|||
expandable-row patterns (useful for `/veeam-analysis`-style
|
||||
drill-downs).
|
||||
|
||||
### Command palette (2026-05-03)
|
||||
- [x] **Cmd+K / Ctrl+K** opens a global launcher
|
||||
(`components/navigation/command-palette.tsx`). Three sections:
|
||||
Navigation (every primary route, role-gated), Recent activity
|
||||
(last 5 audits + 5 observations from `/api/dashboard/overview`),
|
||||
Companies (filtered client-side from `/api/companies`). The `/`
|
||||
key also opens it when no input is focused. Top-bar shows a small
|
||||
"Search · ⌘K" button (md+) for discoverability.
|
||||
|
||||
### Status & dashboard split (2026-05-03)
|
||||
- [x] Move integration health + sync health off `/dashboard` onto a
|
||||
dedicated `/status` route. Top-bar `<StatusIndicator />` links there.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google";
|
|||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { AppNavigation } from "@/components/navigation/app-navigation";
|
||||
import { CommandPalette } from "@/components/navigation/command-palette";
|
||||
import { TaglineFooter } from "@/components/branding/tagline-footer";
|
||||
import { Toaster } from "sonner";
|
||||
import { AuthProvider } from "@/components/auth/auth-provider";
|
||||
|
|
@ -57,6 +58,7 @@ export default function RootLayout({
|
|||
<main className="flex-1">{children}</main>
|
||||
<TaglineFooter />
|
||||
</div>
|
||||
<CommandPalette />
|
||||
</AuthProvider>
|
||||
<Toaster position="top-right" richColors />
|
||||
</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -315,6 +315,16 @@ export function AppNavigation() {
|
|||
|
||||
{/* Right Side Actions */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }))}
|
||||
className="hidden md:inline-flex h-9 items-center gap-2 rounded-md border bg-muted/40 px-2.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||
aria-label="Open command palette"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<span className="hidden lg:inline">Search</span>
|
||||
<kbd className="num text-[10px] bg-background border rounded-sm px-1 py-px">⌘K</kbd>
|
||||
</button>
|
||||
<StatusIndicator />
|
||||
<ThemeToggle />
|
||||
<UserMenu />
|
||||
|
|
|
|||
298
components/navigation/command-palette.tsx
Normal file
298
components/navigation/command-palette.tsx
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/* CommandPalette — global ⌘K / Ctrl+K launcher.
|
||||
*
|
||||
* Three sections:
|
||||
* • Navigation — every primary route from the top-bar nav,
|
||||
* plus admin shortcuts.
|
||||
* • Recent activity — last 5 analyses + last 5 device observations,
|
||||
* pulled lazily from /api/dashboard/overview.
|
||||
* • Companies — fuzzy search against /api/companies (kicks in
|
||||
* once the user types a query).
|
||||
*
|
||||
* Mounted in the root layout so it's available on every page. */
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Server,
|
||||
Activity,
|
||||
HardDrive,
|
||||
Sparkles,
|
||||
Brain,
|
||||
Search,
|
||||
AlertTriangle,
|
||||
Database,
|
||||
GitCompare,
|
||||
Plug,
|
||||
Power,
|
||||
Users,
|
||||
Workflow,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
TrendingUp,
|
||||
Building2,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
|
||||
// ── Static navigation entries ────────────────────────────────────────
|
||||
|
||||
interface CommandRoute {
|
||||
href: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
icon: React.ElementType;
|
||||
/** Comma-separated extra keywords to match. */
|
||||
keywords?: string;
|
||||
superAdminOnly?: boolean;
|
||||
}
|
||||
|
||||
const ROUTES: CommandRoute[] = [
|
||||
// Primary
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard, keywords: 'home ops operations today kpi' },
|
||||
{ href: '/status', label: 'System status', icon: Activity, keywords: 'health integrations workers sync uptime' },
|
||||
{ href: '/configuration-items', label: 'Configuration items', icon: Server, keywords: 'ci devices assets' },
|
||||
{ href: '/backup-status', label: 'Backup status', icon: HardDrive, keywords: 'veeam rpo' },
|
||||
{ href: '/veeam-comparison', label: 'RPO comparison', icon: GitCompare, keywords: 'pulse veeam datto rmm' },
|
||||
{ href: '/veeam-analysis', label: 'Backup ticket analysis', icon: Brain, keywords: 'veeam category resolution' },
|
||||
// Engagement (super-admin)
|
||||
{ href: '/engagement', label: 'Engagement overview', icon: TrendingUp, superAdminOnly: true, keywords: 'hours teams email' },
|
||||
{ href: '/engagement/profile', label: 'Engagement · employee profile', icon: TrendingUp, superAdminOnly: true, keywords: 'employee resource' },
|
||||
// Analyzer
|
||||
{ href: '/analyzer/tickets', label: 'Analyzer · browse tickets', icon: Search, keywords: 'tickets analyzer browse' },
|
||||
{ href: '/analyzer/reports', label: 'Analyzer · aggregate reports', icon: Sparkles, keywords: 'aggregate report' },
|
||||
{ href: '/analyzer/queue', label: 'Analyzer · needs review', icon: Sparkles, keywords: 'human review queue' },
|
||||
{ href: '/analyzer/itglue/applications', label: 'IT Glue · applications', icon: Database, keywords: 'flexible asset' },
|
||||
{ href: '/analyzer/itglue/configurations', label: 'IT Glue · configurations', icon: Database, keywords: 'configuration' },
|
||||
// Admin (super-admin)
|
||||
{ href: '/admin', label: 'Admin home', icon: Activity, superAdminOnly: true, keywords: 'admin dashboard' },
|
||||
{ href: '/admin/integrations', label: 'Admin · integrations', icon: Power, superAdminOnly: true, keywords: 'toggle disable enable s1 sentinelone datto itglue' },
|
||||
{ href: '/admin/sync', label: 'Admin · sync schedules', icon: Clock, superAdminOnly: true, keywords: 'cron schedules autotask veeam' },
|
||||
{ href: '/admin/workflow', label: 'Admin · workflow rules', icon: Workflow, superAdminOnly: true, keywords: 'classifier ai prompt' },
|
||||
{ href: '/admin/rmm-overshell', label: 'Admin · RMM Overshell', icon: Activity, superAdminOnly: true, keywords: 'datto execute script' },
|
||||
{ href: '/admin/itglue-writes', label: 'Admin · IT Glue write log', icon: Database, superAdminOnly: true, keywords: 'audit revert' },
|
||||
{ href: '/admin/device-link-conflicts', label: 'Admin · device-link conflicts', icon: AlertTriangle, superAdminOnly: true, keywords: 'mapping conflicts xref' },
|
||||
{ href: '/admin/users', label: 'Admin · users & roles', icon: Users, superAdminOnly: true, keywords: 'invite role permission' },
|
||||
// Account
|
||||
{ href: '/settings', label: 'Settings', icon: Settings, keywords: 'profile account' },
|
||||
{ href: '/settings/security', label: 'Security · 2FA & sessions', icon: ShieldCheck, keywords: 'two factor totp logout' },
|
||||
];
|
||||
|
||||
interface OverviewResponse {
|
||||
observations: Array<{ id: string; kind: string; hostname: string | null; companyName: string | null }>;
|
||||
audits: Array<{ id: string; hostname: string | null; companyName: string | null }>;
|
||||
}
|
||||
|
||||
interface CompanyHit {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CompaniesApiRow {
|
||||
id: number;
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
// ── Component ────────────────────────────────────────────────────────
|
||||
|
||||
export function CommandPalette() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [recents, setRecents] = useState<OverviewResponse | null>(null);
|
||||
const [allCompanies, setAllCompanies] = useState<CompanyHit[] | null>(null);
|
||||
const [companiesLoading, setCompaniesLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const isSuperAdmin = (session?.user as { role?: string } | undefined)?.role === 'super-admin';
|
||||
|
||||
// Bind ⌘K / Ctrl+K (and / when nothing else is focused).
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
const isCmdK = (e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey);
|
||||
const isSlash =
|
||||
e.key === '/' &&
|
||||
!(document.activeElement instanceof HTMLInputElement) &&
|
||||
!(document.activeElement instanceof HTMLTextAreaElement) &&
|
||||
!(document.activeElement as HTMLElement | null)?.isContentEditable;
|
||||
if (isCmdK || isSlash) {
|
||||
e.preventDefault();
|
||||
setOpen((v) => !v);
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, []);
|
||||
|
||||
// Lazy-load recent activity once on first open.
|
||||
useEffect(() => {
|
||||
if (!open || recents) return;
|
||||
void fetch('/api/dashboard/overview', { cache: 'no-store' })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((j: OverviewResponse | null) => {
|
||||
if (j) setRecents(j);
|
||||
})
|
||||
.catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
}, [open, recents]);
|
||||
|
||||
// Companies are loaded once on first open (the endpoint returns the
|
||||
// full active customer list — small enough to filter client-side).
|
||||
useEffect(() => {
|
||||
if (!open || allCompanies !== null) return;
|
||||
setCompaniesLoading(true);
|
||||
void fetch('/api/companies', { cache: 'no-store' })
|
||||
.then((r) => (r.ok ? r.json() : { companies: [] }))
|
||||
.then((j: { companies?: CompaniesApiRow[] }) => {
|
||||
const items: CompanyHit[] = (j.companies ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
name: c.companyName,
|
||||
}));
|
||||
setAllCompanies(items);
|
||||
})
|
||||
.catch(() => setAllCompanies([]))
|
||||
.finally(() => setCompaniesLoading(false));
|
||||
}, [open, allCompanies]);
|
||||
|
||||
const companyMatches = (() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q.length < 2 || !allCompanies) return [];
|
||||
return allCompanies
|
||||
.filter((c) => c.name.toLowerCase().includes(q))
|
||||
.slice(0, 8);
|
||||
})();
|
||||
|
||||
const go = useCallback(
|
||||
(href: string) => {
|
||||
setOpen(false);
|
||||
router.push(href);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const visibleRoutes = ROUTES.filter((r) => !r.superAdminOnly || isSuperAdmin);
|
||||
|
||||
return (
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Search Pulse"
|
||||
description="Jump to any page, recent ticket, or company."
|
||||
>
|
||||
<CommandInput
|
||||
placeholder="Search pages, tickets, companies…"
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results.</CommandEmpty>
|
||||
|
||||
<CommandGroup heading="Navigation">
|
||||
{visibleRoutes.map((r) => (
|
||||
<CommandItem
|
||||
key={r.href}
|
||||
value={`${r.label} ${r.keywords ?? ''}`}
|
||||
onSelect={() => go(r.href)}
|
||||
>
|
||||
<r.icon className="h-4 w-4" />
|
||||
<span>{r.label}</span>
|
||||
{r.hint && <CommandShortcut>{r.hint}</CommandShortcut>}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
{recents && recents.audits.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Recent audits">
|
||||
{recents.audits.slice(0, 5).map((a) => (
|
||||
<CommandItem
|
||||
key={a.id}
|
||||
value={`audit ${a.hostname ?? ''} ${a.companyName ?? ''}`}
|
||||
onSelect={() => go(`/analyzer/itglue/configurations`)}
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{a.hostname ?? '(unanchored)'}
|
||||
{a.companyName && (
|
||||
<span className="text-muted-foreground"> · {a.companyName}</span>
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{recents && recents.observations.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Recent observations">
|
||||
{recents.observations.slice(0, 5).map((o) => (
|
||||
<CommandItem
|
||||
key={o.id}
|
||||
value={`observation ${o.hostname ?? ''} ${o.companyName ?? ''} ${o.kind}`}
|
||||
onSelect={() => go('/configuration-items')}
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{o.hostname ?? '(unanchored)'}
|
||||
{o.companyName && (
|
||||
<span className="text-muted-foreground"> · {o.companyName}</span>
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{companyMatches.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Companies">
|
||||
{companyMatches.map((c) => (
|
||||
<CommandItem
|
||||
key={c.id}
|
||||
value={`company ${c.name}`}
|
||||
onSelect={() =>
|
||||
go(
|
||||
`/configuration-items?company=${encodeURIComponent(String(c.id))}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="truncate">{c.name}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Tip">
|
||||
<CommandItem disabled value="hint">
|
||||
<Plug className="h-4 w-4" />
|
||||
<span className="text-muted-foreground">Press</span>
|
||||
<CommandShortcut>⌘ K</CommandShortcut>
|
||||
<span className="text-muted-foreground">or</span>
|
||||
<CommandShortcut>/</CommandShortcut>
|
||||
<span className="text-muted-foreground">to open this panel anywhere.</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
}
|
||||
184
components/ui/command.tsx
Normal file
184
components/ui/command.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -33,6 +33,7 @@
|
|||
"better-auth": "^1.4.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ioredis": "^5.9.0",
|
||||
|
|
@ -8433,6 +8434,22 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-id": "^1.1.0",
|
||||
"@radix-ui/react-primitive": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/code-block-writer": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"better-auth": "^1.4.10",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ioredis": "^5.9.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue