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:
Lucas Berger
2026-06-09 21:45:59 -04:00
parent 458d6e4fef
commit 1de4aa5a3e
3 changed files with 444 additions and 23 deletions
+91 -23
View File
@@ -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>
)
}