feat: Unify navigation with consistent header and centered bottom menu
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Removed duplicate MobileNav AppBar from desktop view
- Added unified header bar across all screen sizes (mobile + desktop)
- Positioned connection status on left, user menu on right in header
- Centered bottom navigation menu on desktop (30% width with 20px margin)
- Hidden floating voice button on desktop (using center button instead)
- Updated voice command button to pink color (#FF69B4) matching design
- Added rounded corners to desktop bottom navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 20:55:42 +00:00
parent 75e5c2866d
commit fb9f9d25c7
3 changed files with 121 additions and 141 deletions

View File

@@ -62,134 +62,30 @@ export const AppShell = ({ children }: AppShellProps) => {
flexDirection: 'column', flexDirection: 'column',
minHeight: '100vh', minHeight: '100vh',
bgcolor: 'background.default', bgcolor: 'background.default',
pb: { xs: '64px', md: 0 }, // Space for tab bar on mobile pb: '64px', // Space for tab bar on both mobile and desktop
}}> }}>
{!isMobile && <MobileNav />} {/* Header Bar - Both Mobile and Desktop */}
<Box
{/* Mobile Header Bar */} sx={{
{isMobile && ( position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 48,
bgcolor: 'background.paper',
borderBottom: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
zIndex: 1200,
boxShadow: 1,
}}
>
{/* Connection Status & Presence Indicator */}
<Box <Box
sx={{ sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 48,
bgcolor: 'background.paper',
borderBottom: '1px solid',
borderColor: 'divider',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
zIndex: 1200,
boxShadow: 1,
}}
>
{/* Connection Status & Presence Indicator */}
<Box
sx={{
display: 'flex',
gap: 1,
}}
>
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
<Chip
icon={isConnected ? <Wifi /> : <WifiOff />}
label={isConnected ? t('connection.live') : t('connection.offline')}
size="small"
color={isConnected ? 'success' : 'default'}
sx={{
fontWeight: 600,
}}
/>
</Tooltip>
{isConnected && presence.count > 1 && (
<Tooltip title={t('connection.familyMembersOnline', { count: presence.count })}>
<Chip
icon={<People />}
label={presence.count}
size="small"
color="primary"
sx={{
fontWeight: 600,
}}
/>
</Tooltip>
)}
</Box>
{/* User Menu Button - Top Right */}
<IconButton
onClick={handleMenuOpen}
size="small"
aria-label="user menu"
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
>
<Avatar
sx={{
width: 32,
height: 32,
bgcolor: 'primary.main',
fontSize: '0.875rem',
}}
>
{user?.name?.charAt(0).toUpperCase() || 'U'}
</Avatar>
</IconButton>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
onClick={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
sx={{
mt: 1,
}}
>
<MenuItem onClick={() => handleNavigate('/settings')}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.settings')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleNavigate('/children')}>
<ListItemIcon>
<ChildCare fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.children')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleNavigate('/family')}>
<ListItemIcon>
<Group fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.family')}</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" color="error" />
</ListItemIcon>
<ListItemText>{t('navigation.logout')}</ListItemText>
</MenuItem>
</Menu>
</Box>
)}
{/* Connection Status & Presence Indicator - Desktop Only */}
{!isMobile && (
<Box
sx={{
position: 'fixed',
top: 16,
right: 16,
zIndex: 1200,
display: 'flex', display: 'flex',
gap: 1, gap: 1,
}} }}
@@ -202,7 +98,6 @@ export const AppShell = ({ children }: AppShellProps) => {
color={isConnected ? 'success' : 'default'} color={isConnected ? 'success' : 'default'}
sx={{ sx={{
fontWeight: 600, fontWeight: 600,
boxShadow: 1,
}} }}
/> />
</Tooltip> </Tooltip>
@@ -216,13 +111,72 @@ export const AppShell = ({ children }: AppShellProps) => {
color="primary" color="primary"
sx={{ sx={{
fontWeight: 600, fontWeight: 600,
boxShadow: 1,
}} }}
/> />
</Tooltip> </Tooltip>
)} )}
</Box> </Box>
)}
{/* User Menu Button - Top Right */}
<IconButton
onClick={handleMenuOpen}
size="small"
aria-label="user menu"
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
>
<Avatar
sx={{
width: 32,
height: 32,
bgcolor: 'primary.main',
fontSize: '0.875rem',
}}
>
{user?.name?.charAt(0).toUpperCase() || 'U'}
</Avatar>
</IconButton>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
onClick={handleMenuClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
sx={{
mt: 1,
}}
>
<MenuItem onClick={() => handleNavigate('/settings')}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.settings')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleNavigate('/children')}>
<ListItemIcon>
<ChildCare fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.children')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleNavigate('/family')}>
<ListItemIcon>
<Group fontSize="small" />
</ListItemIcon>
<ListItemText>{t('navigation.family')}</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={handleLogout}>
<ListItemIcon>
<Logout fontSize="small" color="error" />
</ListItemIcon>
<ListItemText>{t('navigation.logout')}</ListItemText>
</MenuItem>
</Menu>
</Box>
<Container <Container
maxWidth={isTablet ? 'md' : 'lg'} maxWidth={isTablet ? 'md' : 'lg'}
@@ -230,13 +184,13 @@ export const AppShell = ({ children }: AppShellProps) => {
flex: 1, flex: 1,
px: { xs: 2, md: 3 }, px: { xs: 2, md: 3 },
py: 3, py: 3,
pt: { xs: '64px', md: 3 }, // Add top padding for header bar on mobile pt: '64px', // Add top padding for header bar on both mobile and desktop
}} }}
> >
{children} {children}
</Container> </Container>
{isMobile && <TabBar />} <TabBar />
</Box> </Box>
); );
}; };

View File

@@ -2,7 +2,7 @@
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { BottomNavigation, BottomNavigationAction, Paper, Fab, Box } from '@mui/material'; import { BottomNavigation, BottomNavigationAction, Paper, Fab, Box, IconButton } from '@mui/material';
import { import {
Home, Home,
Timeline, Timeline,
@@ -33,10 +33,13 @@ export const TabBar = () => {
aria-label="Primary navigation" aria-label="Primary navigation"
sx={{ sx={{
position: 'fixed', position: 'fixed',
bottom: 0, bottom: { xs: 0, md: 20 },
left: 0, left: { xs: 0, md: '50%' },
right: 0, right: { xs: 0, md: 'auto' },
transform: { xs: 'none', md: 'translateX(-50%)' },
width: { xs: '100%', md: '30%' },
zIndex: 1000, zIndex: 1000,
borderRadius: { xs: 0, md: 2 },
}} }}
elevation={3} elevation={3}
> >
@@ -50,6 +53,7 @@ export const TabBar = () => {
showLabels showLabels
sx={{ sx={{
height: 64, height: 64,
borderRadius: { xs: 0, md: 2 },
'& .MuiBottomNavigationAction-root': { '& .MuiBottomNavigationAction-root': {
minWidth: 60, minWidth: 60,
'&.Mui-selected': { '&.Mui-selected': {
@@ -60,7 +64,7 @@ export const TabBar = () => {
> >
{tabs.map((tab) => { {tabs.map((tab) => {
if (tab.value === 'voice') { if (tab.value === 'voice') {
// Center voice button placeholder // Center voice button
return ( return (
<Box <Box
key="voice-placeholder" key="voice-placeholder"
@@ -70,7 +74,28 @@ export const TabBar = () => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
/> >
<IconButton
aria-label="voice command"
onClick={() => {
const voiceButton = document.querySelector('[aria-label="voice input"]') as HTMLButtonElement;
if (voiceButton) {
voiceButton.click();
}
}}
sx={{
bgcolor: '#FF69B4',
color: 'white',
width: 48,
height: 48,
'&:hover': {
bgcolor: '#FF1493',
},
}}
>
<Mic />
</IconButton>
</Box>
); );
} }
return ( return (
@@ -85,7 +110,7 @@ export const TabBar = () => {
</BottomNavigation> </BottomNavigation>
</Paper> </Paper>
{/* Voice Command Floating Button - Centered */} {/* Voice Command Floating Button - Mobile Only */}
<Fab <Fab
color="secondary" color="secondary"
aria-label="voice command" aria-label="voice command"
@@ -97,6 +122,7 @@ export const TabBar = () => {
} }
}} }}
sx={{ sx={{
display: { xs: 'flex', md: 'none' },
position: 'fixed', position: 'fixed',
bottom: 40, bottom: 40,
left: '50%', left: '50%',
@@ -104,9 +130,9 @@ export const TabBar = () => {
zIndex: 1100, zIndex: 1100,
width: 56, width: 56,
height: 56, height: 56,
bgcolor: 'secondary.main', bgcolor: '#FF69B4',
'&:hover': { '&:hover': {
bgcolor: 'secondary.dark', bgcolor: '#FF1493',
}, },
}} }}
> >

View File

@@ -323,7 +323,7 @@ export function VoiceFloatingButton() {
return ( return (
<> <>
{/* Floating button positioned in bottom-right - Hidden on mobile since we have TabBar center button */} {/* Floating button positioned in bottom-right - Hidden on desktop since we have TabBar center button */}
<Tooltip title="Voice Command (Beta)" placement="left"> <Tooltip title="Voice Command (Beta)" placement="left">
<Fab <Fab
color="primary" color="primary"
@@ -335,7 +335,7 @@ export function VoiceFloatingButton() {
bottom: 24, bottom: 24,
right: 24, right: 24,
zIndex: 1000, zIndex: 1000,
display: { xs: 'none', md: 'flex' }, // Hide on mobile (xs) and small screens, show on medium+ display: 'none', // Hidden - using TabBar center button instead
}} }}
> >
<MicIcon /> <MicIcon />