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.
264 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|