mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
The Theme sub-trigger inherited the shared sub-trigger padding (py-1.5, no min-height) while every sibling row carries min-h-11 py-2, so the Theme row sat a few pixels shorter and the menu read as unevenly spaced. Match the item height on this instance only, leaving other dropdowns' sub-triggers untouched.
193 lines
7.7 KiB
TypeScript
193 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Bell,
|
|
ChevronDown,
|
|
LogIn,
|
|
LogOut,
|
|
Monitor,
|
|
Moon,
|
|
Settings,
|
|
Shield,
|
|
Sun,
|
|
} from "lucide-react";
|
|
import { Logo } from "@/components/ui/logo";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import Link from "next/link";
|
|
import { useAuth, useLogout } from "@/hooks/use-auth";
|
|
import { useTheme } from "@/components/providers";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
function getInitials(name: string): string {
|
|
return name
|
|
.split(/[\s._-]+/)
|
|
.slice(0, 2)
|
|
.map((s) => s[0]?.toUpperCase() ?? "")
|
|
.join("");
|
|
}
|
|
|
|
export function TopBar() {
|
|
const { user, isLoading } = useAuth();
|
|
const logout = useLogout();
|
|
const { theme, setTheme } = useTheme();
|
|
const { t } = useTranslations();
|
|
const avatarUrl = user?.avatarUrl ?? null;
|
|
const isAdmin = user?.role === "ADMIN";
|
|
|
|
const themeIcon =
|
|
theme === "system" ? (
|
|
<Monitor className="h-4 w-4" />
|
|
) : theme === "dark" ? (
|
|
<Moon className="h-4 w-4" />
|
|
) : (
|
|
<Sun className="h-4 w-4" />
|
|
);
|
|
|
|
return (
|
|
<header
|
|
className="bg-card/80 border-border sticky top-0 z-40 flex h-16 items-center justify-between border-b px-4 backdrop-blur-md md:px-6"
|
|
// iOS PWA on notched iPhones overlays the status bar onto the
|
|
// sticky header unless we reserve the safe-area inset. The
|
|
// inline style adds `safe-area-inset-top` as padding-top on
|
|
// devices that report one and is a no-op on every other
|
|
// platform; the inner h-16 content area stays nominally 64 px.
|
|
style={{ paddingTop: "env(safe-area-inset-top, 0px)" }}
|
|
>
|
|
{/* Mobile logo */}
|
|
<Link href="/" className="flex items-center gap-2 md:hidden">
|
|
<Logo className="text-primary" size={20} />
|
|
<span className="font-bold tracking-tight">HealthLog</span>
|
|
</Link>
|
|
|
|
{/* Desktop: empty spacer (user controls are in sidebar) */}
|
|
<div className="hidden md:block" />
|
|
|
|
{/* Mobile-only auth section (desktop uses sidebar user section) */}
|
|
<div className="flex items-center gap-2 md:hidden">
|
|
{isLoading ? (
|
|
<Skeleton className="bg-muted h-4 w-20 rounded" />
|
|
) : user ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
aria-label={t("nav.userMenu")}
|
|
// v1.4.25 W8 — hit the WCAG 2.5.5 44 px touch-target floor on
|
|
// mobile. The text-only py-1.5 was previously ~30 px tall.
|
|
//
|
|
// v1.4.34 IW-G — keyboard users get a visible ring on
|
|
// focus-visible. The previous `focus:outline-none` killed
|
|
// the focus indicator without replacing it.
|
|
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 flex min-h-11 min-w-11 items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
>
|
|
<Avatar className="size-7">
|
|
{avatarUrl && (
|
|
<AvatarImage src={avatarUrl} alt={user.username} />
|
|
)}
|
|
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
|
{getInitials(user.username)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="hidden sm:inline">{user.username}</span>
|
|
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-60">
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/settings/account" className="cursor-pointer">
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
{t("nav.settings")}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/notifications" className="cursor-pointer">
|
|
<Bell className="mr-2 h-4 w-4" />
|
|
{t("nav.notifications")}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
{isAdmin && (
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/admin" className="cursor-pointer">
|
|
<Shield className="mr-2 h-4 w-4" />
|
|
{t("nav.admin")}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
)}
|
|
{/* v1.4.36 W4e — About moved into the Admin Console
|
|
(`/admin/about`). The dropdown entry was redundant
|
|
for the small audience that still reaches it (admins
|
|
only, on the order of once or twice a year). */}
|
|
<DropdownMenuSub>
|
|
{/* v1.22.1 — the shared sub-trigger ships `py-1.5` and no
|
|
min-height, while every sibling `DropdownMenuItem` carries
|
|
`min-h-11 py-2`. Left as-is the Theme row sits a few pixels
|
|
shorter than the rows around it and the menu reads as
|
|
unevenly spaced. Match the item height here (instance-level
|
|
override, so no other dropdown's sub-trigger is touched). */}
|
|
<DropdownMenuSubTrigger className="min-h-11 py-2">
|
|
{themeIcon}
|
|
<span className="ml-2">{t("nav.theme")}</span>
|
|
</DropdownMenuSubTrigger>
|
|
<DropdownMenuSubContent>
|
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
<Monitor className="mr-2 h-4 w-4" />
|
|
{t("nav.themeSystem")}
|
|
{theme === "system" && (
|
|
<span className="text-primary ml-auto text-xs">
|
|
✓
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
<Moon className="mr-2 h-4 w-4" />
|
|
{t("nav.themeDark")}
|
|
{theme === "dark" && (
|
|
<span className="text-primary ml-auto text-xs">
|
|
✓
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
<Sun className="mr-2 h-4 w-4" />
|
|
{t("nav.themeLight")}
|
|
{theme === "light" && (
|
|
<span className="text-primary ml-auto text-xs">
|
|
✓
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuSubContent>
|
|
</DropdownMenuSub>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => logout.mutate()}
|
|
className="text-destructive focus:text-destructive focus:bg-destructive/10 cursor-pointer"
|
|
>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
{t("nav.logout")}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<Link
|
|
href="/auth/login"
|
|
// v1.4.25 W8 — match the WCAG 2.5.5 44 px touch-target floor.
|
|
className="text-muted-foreground hover:text-foreground flex min-h-11 min-w-11 items-center gap-2 rounded-md px-2 text-sm transition-colors"
|
|
>
|
|
<LogIn className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{t("nav.login")}</span>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|