feat: add a compact icon-only top navigation toggle (#1363)

* feat: add a compact icon-only top navigation toggle

Add a browser-local "Top navigation labels" preference under Settings >
Appearance. With it off, the desktop top navigation renders icon-only;
each destination keeps an aria-label, gains a hover/focus tooltip, and
stays reachable from the command palette. The setting defaults on, so
current behavior is preserved, and the mobile navigation always keeps
its labels.

Also left-align the desktop nav (previously centered) and shorten the
longest nav label from "Auto-Update" to "Update" so the bar scans faster.

* feat: let the icon-only top nav be left or centered

Add a "Top navigation alignment" preference under Settings > Appearance
that appears only when top navigation labels are off. It places the
icon-only bar against the left edge (the default) or centered. With
labels on, the nav always stays left so the longer labels read from the
edge. The choice is browser-local and persists per device.
This commit is contained in:
Anso
2026-06-12 10:49:36 -04:00
committed by GitHub
parent 2a4955f56d
commit 1b96f3b980
15 changed files with 391 additions and 38 deletions
+54 -32
View File
@@ -1,8 +1,10 @@
import type { ReactNode } from 'react';
import { Fragment, type ReactNode } from 'react';
import type { LucideIcon } from 'lucide-react';
import { Menu } from 'lucide-react';
import { Button } from './ui/button';
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import type { TopNavAlign } from '@/hooks/use-top-nav-align';
import { cn } from '@/lib/utils';
export interface TopBarNavItem {
@@ -21,6 +23,10 @@ interface TopBarProps {
themeSwitch?: ReactNode;
notifications: ReactNode;
userMenu: ReactNode;
/** Show text labels beside the desktop nav icons. When false, the bar is icon-only. */
showLabels?: boolean;
/** Desktop nav placement in icon-only mode. Ignored while labels are shown (always left). */
navAlign?: TopNavAlign;
}
export function TopBar({
@@ -33,7 +39,12 @@ export function TopBar({
themeSwitch,
notifications,
userMenu,
showLabels = true,
navAlign = 'left',
}: TopBarProps) {
// Centering applies only to the icon-only bar; with labels on the nav stays
// left so the long labels read from the edge.
const centered = !showLabels && navAlign === 'center';
return (
<div
className={cn(
@@ -42,38 +53,49 @@ export function TopBar({
'shadow-chrome-top',
)}
>
{/* LEFT ZONE: reserved spacer (keeps nav visually centered) */}
<div className="flex-1 min-w-0" />
{/* LEFT SPACER: balances the right utilities so the nav centers. */}
{centered && <div className="flex-1 min-w-0" />}
{/* CENTER ZONE: Navigation (hidden on mobile) */}
<nav aria-label="Primary" className="hidden md:flex self-stretch items-stretch">
{navItems.map(({ value, label, icon: Icon }) => {
const isActive = activeView === value;
return (
<button
key={value}
onClick={() => onNavigate(value)}
aria-label={label}
aria-current={isActive ? 'page' : undefined}
className={cn(
'relative inline-flex h-full items-center gap-2 px-4',
'font-mono text-[10px] uppercase tracking-[0.18em] transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',
)}
>
<Icon className="w-4 h-4 shrink-0" strokeWidth={1.5} />
<span className="hidden xl:inline">{label}</span>
{isActive && (
<span
aria-hidden
className="pointer-events-none absolute inset-x-0 -bottom-px h-[2px] bg-brand"
/>
)}
</button>
);
})}
</nav>
{/* NAV ZONE: Navigation (hidden on mobile) */}
<TooltipProvider delayDuration={300} disableHoverableContent>
<nav aria-label="Primary" className="hidden md:flex self-stretch items-stretch">
{navItems.map(({ value, label, icon: Icon }) => {
const isActive = activeView === value;
const button = (
<button
onClick={() => onNavigate(value)}
aria-label={label}
aria-current={isActive ? 'page' : undefined}
className={cn(
'relative inline-flex h-full items-center gap-2 px-4',
'font-mono text-[10px] uppercase tracking-[0.18em] transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/50',
isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',
)}
>
<Icon className="w-4 h-4 shrink-0" strokeWidth={1.5} />
{showLabels && <span className="hidden xl:inline">{label}</span>}
{isActive && (
<span
aria-hidden
className="pointer-events-none absolute inset-x-0 -bottom-px h-[2px] bg-brand"
/>
)}
</button>
);
// Icon-only mode: a tooltip names the destination on hover/focus. With
// labels on, the visible text carries it, so the button renders bare.
return showLabels ? (
<Fragment key={value}>{button}</Fragment>
) : (
<Tooltip key={value}>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="bottom">{label}</TooltipContent>
</Tooltip>
);
})}
</nav>
</TooltipProvider>
{/* RIGHT ZONE: Utilities + identity pin */}
<div className="flex flex-1 min-w-0 items-center justify-end gap-2">