feat(05-08): SettingsSheet (master toggle D-09) + AppNav avatar promoted to button
- Create SettingsSheet.tsx: bottom sheet (role=dialog, z:301, Escape+backdrop-close) with FamilySync Notifications toggle (role=switch, aria-checked, 44px target) wired to usePushSubscription setEnabled + permission state and inline permission-denied hint (AlertCircle + 'How to enable') when denied - Promote PhoneNav avatar div to button with onOpenSettings onClick + aria-label - Add onOpenSettings prop to DesktopNav; add avatar button at sidebar bottom - Thread onOpenSettings through AppNavProps - Add @keyframes spin to tokens.css (fixes missing keyframe used by SettingsSheet + SyncStateToast)
This commit is contained in:
@@ -19,25 +19,42 @@ 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 }: AppNavProps) {
|
||||
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} />
|
||||
return (
|
||||
<PhoneNav
|
||||
currentUserColor={currentUserColor}
|
||||
currentUserName={currentUserName}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <DesktopNav members={members} />
|
||||
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)'
|
||||
@@ -69,43 +86,50 @@ function PhoneNav({
|
||||
FamilySync
|
||||
</span>
|
||||
|
||||
{/* User color avatar — aria-label + title per reviewer note */}
|
||||
<div
|
||||
{/* User avatar — promoted to button to open Settings sheet (D-09) */}
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
aria-label={`${displayName} — open settings`}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
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,
|
||||
cursor: 'default',
|
||||
// Ensure 44px tap area with padding
|
||||
minWidth: '44px',
|
||||
minHeight: '44px',
|
||||
padding: '6px',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
aria-label={displayName}
|
||||
title={displayName}
|
||||
role="img"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
/** Tablet/Desktop: 240px left sidebar — app name + nav links + color legend */
|
||||
function DesktopNav({ members }: { members: LegendMember[] }) {
|
||||
/** 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',
|
||||
@@ -183,6 +207,50 @@ function DesktopNav({ members }: { members: LegendMember[] }) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user