/* Hallmark · component: mobile side navigation · genre: modern-minimal · theme: Quiet · pre-emit critique: P5 H5 E5 S5 R5 V5 */ import { Link } from '@tanstack/react-router' import { Calendar, ChevronRight, Command, Mail, Moon, Plus, Sun, Users } from 'lucide-react' import { type ReactNode, useEffect, useRef } from 'react' import { Tooltip, TooltipContent, TooltipTrigger } from '#shared/components/ui/tooltip' import { initials } from '#shared/lib/presentation' import { cn } from '#shared/lib/utils' import { APP_RAIL_WIDTH_CLASS, CHROME_ROW_CLASS } from '../config/layout.js' import { CALENDAR_HOME_PATH, CONTACTS_HOME_PATH, MAIL_HOME_PATH, SETTINGS_PATH, } from '../config/route-paths.js' import { themeToggleLabel, toggleTheme } from '../config/theme.js' import { useThemeToggleState } from '../lib/use-theme-toggle-state.js' import { useUserPreferences } from '../preferences/user-preferences.js' export type MailboxAccountOption = { email: string handle: string active: boolean } type AppRailNavProps = { email: string displayName?: string accounts?: MailboxAccountOption[] active: 'mail' | 'calendar' | 'contacts' | 'settings' onOpenCommandPalette?: () => void } type AppRailMobileNavProps = AppRailNavProps & { onNavigate: () => void showDestinations?: boolean } function formatOrgLabel(appName: string): string { return appName .split(/[-_\s]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' ') } export function AppRailLogo({ appName, className }: { appName: string; className?: string }) { const orgLabel = formatOrgLabel(appName) const orgInitials = initials(appName) return ( ) } export function AppRailNav({ email, displayName, accounts = [], active, onOpenCommandPalette, }: AppRailNavProps) { const { isDark, mounted } = useThemeToggleState() const [preferences] = useUserPreferences() const effectiveDisplayName = displayName || preferences.displayName const accountLabel = effectiveDisplayName ? `${effectiveDisplayName} · ${email}` : email return ( ) } /** * The mobile counterpart to the persistent desktop rail. It is intended for a * temporary navigation sheet, so it uses labelled, touch-sized rows instead of * reserving a narrow column in the app viewport. */ export function AppRailMobileNav({ email, displayName, accounts = [], active, onOpenCommandPalette, onNavigate, showDestinations = true, }: AppRailMobileNavProps) { const { isDark, mounted } = useThemeToggleState() const [preferences] = useUserPreferences() const effectiveDisplayName = displayName || preferences.displayName const accountLabel = effectiveDisplayName ? `${effectiveDisplayName} · ${email}` : email const accountName = effectiveDisplayName || email return ( ) } function DesktopAccountSwitcher({ accounts }: { accounts: MailboxAccountOption[] }) { const active = accounts.find((account) => account.active) const detailsRef = useRef(null) useEffect(() => { function closeOnExternalInteraction(event: Event) { const details = detailsRef.current if (details?.open && !details.contains(event.target as Node)) details.open = false } document.addEventListener('pointerdown', closeOnExternalInteraction) document.addEventListener('focusin', closeOnExternalInteraction) return () => { document.removeEventListener('pointerdown', closeOnExternalInteraction) document.removeEventListener('focusin', closeOnExternalInteraction) } }, []) if (!active) return null const localPart = active.email.split('@')[0] || active.email return (

Switch inbox

{accounts.map((account) => (
))}
) } function AccountSwitcher({ accounts, onNavigate, }: { accounts: MailboxAccountOption[] onNavigate?: () => void }) { const active = accounts.find((account) => account.active) if (!active) return null return (
) } const MOBILE_NAV_ITEM_CLASS = 'flex min-h-12 w-full items-center gap-3 whitespace-nowrap rounded-lg px-3 text-sm font-medium text-muted-foreground transition-[background-color,color,transform] duration-[var(--dur-fast)] ease-[var(--ease-out)] hover:bg-muted hover:text-foreground active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50' function RailLink({ to, label, isActive, ariaLabel, children, }: { to: string label: string isActive: boolean ariaLabel: string children: ReactNode }) { return ( {isActive ? ) } function RailButton({ children, ariaLabel, shortcut, onClick, type = 'button', }: { children: ReactNode ariaLabel: string shortcut?: string onClick?: () => void type?: 'button' | 'submit' }) { return ( ) } function RailTooltip({ label, shortcut, children, }: { label: string shortcut?: string children: ReactNode }) { return ( {children} {label} {shortcut ? {shortcut} : null} ) } function MobileNavLink({ to, label, isActive, onNavigate, children, }: { to: string label: string isActive: boolean onNavigate: () => void children: ReactNode }) { return ( {children} {label} {isActive ?