Layout reaches zero. The dock, header, user menu, theme switcher, spotlight, shortcuts modal, screen-error fallback, rescan and bug report. Three things worth knowing: CORE_DOCK_ITEMS now carries labelKey instead of label, and one useDockItems() hook resolves the composed list. DashboardLayout, Spotlight and DockSettings each spread the same two sources before, so each would have had to resolve labels itself. Plugin tiles keep the name from their own manifest — a plugin's strings are not ours. Spotlight resolves labels INSIDE the hook, so rank() still matches what the reader can see rather than a dot-path. Its to: '/settings/...' literals are untouched, which is what docs:check parses — verified still 20 offered, 0 missing. The rescan toast said 'N skill(s)'. That is English giving up on a plural, so it is now a real plural entry per noun and reads correctly at 0, 1 and 2 in both locales. ThemeSwitcher used t as its theme loop variable; renamed to entry.
77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
import { Link } from 'react-router';
|
|
import * as Dropdown from '@/components/ui/dropdown-menu';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { User, LogOut, Settings, Puzzle, Bot, UserCog } from 'lucide-react';
|
|
import { useAuth } from 'hooks/useAuth';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
|
|
const { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } = Dropdown;
|
|
|
|
export function UserMenu() {
|
|
const { user, isLoading } = useAuth();
|
|
const { t } = useTranslation();
|
|
if (isLoading) return null;
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-duck-yellow cursor-pointer">
|
|
<Avatar className="h-9 w-9 rounded-full">
|
|
<AvatarImage src={user?.avatar ?? undefined} />
|
|
<AvatarFallback className="bg-duck-teal text-duck-yellow text-sm font-bold rounded-full">
|
|
{user?.name?.charAt(0).toUpperCase() ?? '?'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="z-10 w-48">
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/settings/profile">
|
|
<User className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.profile')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/settings/system">
|
|
<Settings className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.systemSettings')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
{/* No Resources entry. `/settings/resources` has never been a route, so this item sent you to
|
|
the catch-all and out to `/` — a menu entry that looks like navigation and is a bounce.
|
|
(navigation-audit M8.) */}
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/settings/ai">
|
|
<Bot className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.ai')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/settings/integrations">
|
|
<Puzzle className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.integrations')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
{/* Owner only. This is tidiness, not access control — the route stays reachable and its
|
|
endpoints are gated server-side by ownerGate, because a hidden menu item is not a permission
|
|
and anything relying on it being hidden is already wrong. */}
|
|
{user?.role === 'Super Admin' && (
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/settings/user-management">
|
|
<UserCog className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.userManagement')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem asChild className="cursor-pointer">
|
|
<Link to="/auth/signout">
|
|
<LogOut className="me-2 h-4 w-4" />
|
|
{t('header.userMenu.signOut')}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|