Files
Nexumi/apps/webui/src/components/layout/dashboard-command-palette.tsx
TheOnlyMace 75ac91eecc Add new features and enhancements to the WebUI and dashboard components
- Updated package.json to include new dependencies: `@radix-ui/react-dialog` and `cmdk`.
- Enhanced global styles in globals.css for improved visual depth in dark mode.
- Refactored dashboard page components to improve layout and accessibility, including new icons and responsive design adjustments.
- Implemented suspense handling in commands page for better loading states.
- Improved sidebar navigation with new icons and mobile responsiveness.
- Added FieldAnchor components across various forms for better accessibility and structure.
- Enhanced commands manager with search parameter handling for improved user experience.
- Updated multiple module forms to include FieldAnchor for consistent layout and accessibility.
2026-07-22 19:44:55 +02:00

177 lines
5.6 KiB
TypeScript

'use client';
import { Search, Terminal, Settings2, LayoutGrid } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslations } from '@/components/locale-provider';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@/components/ui/command';
import {
OWNER_NAV_ENTRIES,
buildGuildSearchHref,
buildGuildSearchIndex,
type SearchIndexEntry,
type SearchItemKind
} from '@/lib/dashboard-search-index';
function kindIcon(kind: SearchItemKind) {
switch (kind) {
case 'command':
return Terminal;
case 'setting':
return Settings2;
default:
return LayoutGrid;
}
}
interface DashboardCommandPaletteProps {
guildId?: string;
mode?: 'guild' | 'owner';
}
export function DashboardCommandPalette({ guildId, mode = 'guild' }: DashboardCommandPaletteProps) {
const t = useTranslations();
const router = useRouter();
const [open, setOpen] = useState(false);
const entries = useMemo(() => {
if (mode === 'owner') {
return OWNER_NAV_ENTRIES;
}
return buildGuildSearchIndex();
}, [mode]);
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
setOpen((prev) => !prev);
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, []);
const resolveLabel = useCallback(
(entry: SearchIndexEntry) => {
if (entry.labelOverride) {
return entry.labelOverride;
}
if (entry.labelKey) {
return t(entry.labelKey);
}
return entry.id;
},
[t]
);
function navigateTo(entry: SearchIndexEntry) {
setOpen(false);
if (mode === 'owner') {
router.push(entry.href);
return;
}
if (!guildId) {
return;
}
const href = buildGuildSearchHref(guildId, entry);
router.push(href);
if (entry.hash) {
window.setTimeout(() => {
document.getElementById(entry.hash!)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 250);
}
}
const pages = entries.filter((e) => e.kind === 'page');
const commands = entries.filter((e) => e.kind === 'command');
const settings = entries.filter((e) => e.kind === 'setting');
const isMac =
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="inline-flex h-9 w-full max-w-xs items-center gap-2 rounded-md border border-input bg-background/60 px-3 text-sm text-muted-foreground shadow-sm transition-colors hover:bg-accent/50 hover:text-foreground"
>
<Search className="size-4 shrink-0" />
<span className="flex-1 truncate text-left">{t('search.placeholder')}</span>
<kbd className="pointer-events-none hidden h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:inline-flex">
{isMac ? '⌘' : 'Ctrl'}K
</kbd>
</button>
<CommandDialog open={open} onOpenChange={setOpen} title={t('search.title')}>
<CommandInput placeholder={t('search.placeholder')} />
<CommandList>
<CommandEmpty>{t('search.empty')}</CommandEmpty>
{pages.length > 0 ? (
<CommandGroup heading={t('search.groups.pages')}>
{pages.map((entry) => {
const Icon = kindIcon(entry.kind);
const label = resolveLabel(entry);
return (
<CommandItem
key={entry.id}
value={`${label} ${entry.keywords?.join(' ') ?? ''} ${entry.labelKey ?? ''}`}
onSelect={() => navigateTo(entry)}
>
<Icon className="size-4 text-muted-foreground" />
<span>{label}</span>
</CommandItem>
);
})}
</CommandGroup>
) : null}
{commands.length > 0 ? (
<CommandGroup heading={t('search.groups.commands')}>
{commands.map((entry) => {
const Icon = kindIcon(entry.kind);
const label = resolveLabel(entry);
return (
<CommandItem
key={entry.id}
value={`${label} ${entry.keywords?.join(' ') ?? ''}`}
onSelect={() => navigateTo(entry)}
>
<Icon className="size-4 text-muted-foreground" />
<span>{label}</span>
</CommandItem>
);
})}
</CommandGroup>
) : null}
{settings.length > 0 ? (
<CommandGroup heading={t('search.groups.settings')}>
{settings.map((entry) => {
const Icon = kindIcon(entry.kind);
const label = resolveLabel(entry);
return (
<CommandItem
key={entry.id}
value={`${label} ${entry.keywords?.join(' ') ?? ''} ${entry.labelKey ?? ''}`}
onSelect={() => navigateTo(entry)}
>
<Icon className="size-4 text-muted-foreground" />
<span>{label}</span>
</CommandItem>
);
})}
</CommandGroup>
) : null}
</CommandList>
</CommandDialog>
</>
);
}