Files
familysync/apps/pwa/src/components/AppNav.tsx
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

264 lines
7.3 KiB
TypeScript

/**
* AppNav — top navigation bar (phone) / left sidebar (tablet/desktop).
*
* UI-SPEC §AppNav:
* - Phone: 48px top bar, app name "FamilySync" left, user color swatch right
* - Tablet/Desktop: 240px left sidebar, app name + ColorLegend
* - Accent colors NOT on chrome (60/30/10 split)
*
* Reviewer note (UI-SPEC §Post-Verification):
* - Phone-nav avatar/color swatch needs aria-label + title (icon-only affordance)
* - Grid is primary focal point; AppNav is secondary chrome
*/
import { NavLink } from 'react-router';
import { CalendarDays, List } from 'lucide-react';
import { ColorLegend, type LegendMember } from './ColorLegend.js';
interface AppNavProps {
members?: LegendMember[];
currentUserColor?: string;
currentUserName?: string;
/** Called when the user avatar is tapped — opens the Settings sheet. */
onOpenSettings?: () => void;
}
export function AppNav({
members = [],
currentUserColor,
currentUserName,
onOpenSettings,
}: AppNavProps) {
const isMobile = typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
if (isMobile) {
return (
<PhoneNav
currentUserColor={currentUserColor}
currentUserName={currentUserName}
onOpenSettings={onOpenSettings}
/>
);
}
return (
<DesktopNav
members={members}
currentUserColor={currentUserColor}
currentUserName={currentUserName}
onOpenSettings={onOpenSettings}
/>
);
}
/** Phone: 48px top bar — app name left, user avatar right */
function PhoneNav({
currentUserColor,
currentUserName,
onOpenSettings,
}: {
currentUserColor?: string;
currentUserName?: string;
onOpenSettings?: () => void;
}) {
const displayName = currentUserName ?? 'User';
const color = currentUserColor ?? 'var(--color-member-0)';
return (
<header
style={{
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 var(--space-4)',
background: 'var(--color-surface)',
borderBottom: '1px solid var(--color-border)',
flexShrink: 0,
fontFamily: 'var(--font-family-base)',
}}
>
{/* App name */}
<span
style={{
fontSize: 'var(--text-display-size)',
fontWeight: 'var(--text-display-weight)',
lineHeight: 'var(--text-display-line-height)',
color: 'var(--color-text-primary)',
letterSpacing: '-0.01em',
}}
>
FamilySync
</span>
{/* User avatar — promoted to button to open Settings sheet (D-09) */}
<button
onClick={onOpenSettings}
aria-label={`${displayName} — open settings`}
style={{
background: 'none',
border: 'none',
cursor: onOpenSettings ? 'pointer' : 'default',
minWidth: '44px',
minHeight: '44px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
borderRadius: 'var(--space-1, 4px)',
flexShrink: 0,
}}
>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: color,
}}
aria-hidden="true"
/>
</button>
</header>
);
}
/** Tablet/Desktop: 240px left sidebar — app name + nav links + color legend + avatar */
function DesktopNav({
members,
currentUserColor,
currentUserName,
onOpenSettings,
}: {
members: LegendMember[];
currentUserColor?: string;
currentUserName?: string;
onOpenSettings?: () => void;
}) {
const navLinkStyle = ({ isActive }: { isActive: boolean }): React.CSSProperties => ({
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: 'var(--space-2) var(--space-3, 12px)',
borderRadius: 'var(--space-1, 4px)',
textDecoration: 'none',
fontSize: 'var(--text-body-size, 15px)',
fontWeight: isActive ? 600 : 400,
color: isActive ? 'var(--color-member-0)' : 'var(--color-text-primary)',
background: isActive
? 'color-mix(in srgb, var(--color-member-0) 10%, transparent)'
: 'transparent',
minHeight: '44px',
transition: 'color 0.1s ease, background 0.1s ease',
});
return (
<nav
style={{
width: '240px',
flexShrink: 0,
background: 'var(--color-surface)',
borderRight: '1px solid var(--color-border)',
display: 'flex',
flexDirection: 'column',
padding: 'var(--space-6) var(--space-4)',
fontFamily: 'var(--font-family-base)',
overflowY: 'auto',
}}
aria-label="Main navigation"
>
{/* App name */}
<div
style={{
fontSize: 'var(--text-display-size)',
fontWeight: 'var(--text-display-weight)',
lineHeight: 'var(--text-display-line-height)',
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-6)',
letterSpacing: '-0.01em',
}}
>
FamilySync
</div>
{/* Section nav links */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--space-1, 4px)',
marginBottom: 'var(--space-6)',
}}
>
<NavLink to="/calendar" style={navLinkStyle} aria-label="Calendar">
<CalendarDays size={18} aria-hidden="true" />
Calendar
</NavLink>
<NavLink to="/lists" style={navLinkStyle} aria-label="Lists">
<List size={18} aria-hidden="true" />
Lists
</NavLink>
</div>
{/* Color legend */}
<div
style={{
fontSize: 'var(--text-label-size)',
fontWeight: 600,
color: 'var(--color-text-muted)',
marginBottom: 'var(--space-2)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
}}
>
Calendars
</div>
<ColorLegend members={members} />
{/* User avatar — at bottom of sidebar, opens Settings sheet (D-09) */}
<div style={{ marginTop: 'auto', paddingTop: 'var(--space-6)' }}>
<button
onClick={onOpenSettings}
aria-label={`${currentUserName ?? 'User'} — open settings`}
style={{
background: 'none',
border: 'none',
cursor: onOpenSettings ? 'pointer' : 'default',
minWidth: '44px',
minHeight: '44px',
display: 'flex',
alignItems: 'center',
gap: 'var(--space-3, 12px)',
padding: 'var(--space-2) var(--space-3, 12px)',
borderRadius: 'var(--space-1, 4px)',
width: '100%',
textAlign: 'left',
}}
>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: currentUserColor ?? 'var(--color-member-0)',
flexShrink: 0,
}}
aria-hidden="true"
/>
<span
style={{
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{currentUserName ?? 'User'}
</span>
</button>
</div>
</nav>
);
}