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.
This commit is contained in:
176
apps/webui/src/components/layout/dashboard-command-palette.tsx
Normal file
176
apps/webui/src/components/layout/dashboard-command-palette.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user