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.
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
/**
|
|
* BottomTabBar — fixed-bottom navigation for phone (≤767px).
|
|
*
|
|
* UI-SPEC §BottomTabBar:
|
|
* - Fixed bottom, full-width, 56px + env(safe-area-inset-bottom)
|
|
* - Background: var(--color-surface-dim)
|
|
* - Border-top: 1px solid var(--color-border)
|
|
* - Two equal tabs: Calendar (CalendarDays icon) and Lists (List icon)
|
|
* - Active tab: icon + label in var(--color-member-0), 2px border-bottom indicator
|
|
* - Inactive tab: icon + label in var(--color-text-muted)
|
|
* - Touch target: min 44px guaranteed by 56px bar height
|
|
* - z-index: 200 (below dialogs at 300)
|
|
*
|
|
* Uses react-router NavLink for real-URL active detection (D-17).
|
|
* Only rendered on phone (≤767px) — DesktopNav handles desktop navigation.
|
|
* Returns null on desktop so it does not overlay the AppNav sidebar avatar/Settings
|
|
* button (FIX 4). Same breakpoint (767px) as AppNav's phone/desktop switch.
|
|
*/
|
|
|
|
import { NavLink } from 'react-router';
|
|
import { CalendarDays, List } from 'lucide-react';
|
|
|
|
function isPhone(): boolean {
|
|
return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
|
|
}
|
|
|
|
const tabBase: React.CSSProperties = {
|
|
flex: 1,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '3px',
|
|
textDecoration: 'none',
|
|
fontSize: 'var(--text-label-size, 13px)',
|
|
fontWeight: 400,
|
|
lineHeight: 'var(--text-label-line-height, 1.4)',
|
|
fontFamily: 'var(--font-family-base)',
|
|
color: 'var(--color-text-muted)',
|
|
minHeight: '44px',
|
|
borderBottom: '2px solid transparent',
|
|
transition: 'color 0.1s ease, border-color 0.1s ease',
|
|
userSelect: 'none',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
};
|
|
|
|
const tabActiveOverride: React.CSSProperties = {
|
|
color: 'var(--color-member-0)',
|
|
borderBottom: '2px solid var(--color-member-0)',
|
|
};
|
|
|
|
export function BottomTabBar() {
|
|
// Phone-only: return null on desktop (≥768px) so the fixed bar does not overlay
|
|
// the AppNav sidebar's Settings/avatar button (FIX 4). Consistent with the
|
|
// isPhone() breakpoint used in AppNav and CalendarShell.
|
|
if (!isPhone()) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<nav
|
|
aria-label="Main navigation"
|
|
style={{
|
|
position: 'fixed',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 'calc(56px + env(safe-area-inset-bottom, 0px))',
|
|
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
display: 'flex',
|
|
background: 'var(--color-surface-dim)',
|
|
borderTop: '1px solid var(--color-border)',
|
|
zIndex: 200,
|
|
}}
|
|
>
|
|
<NavLink
|
|
to="/calendar"
|
|
aria-label="Calendar"
|
|
style={({ isActive }) => ({
|
|
...tabBase,
|
|
...(isActive ? tabActiveOverride : {}),
|
|
})}
|
|
>
|
|
<CalendarDays size={22} aria-hidden="true" />
|
|
<span>Calendar</span>
|
|
</NavLink>
|
|
|
|
<NavLink
|
|
to="/lists"
|
|
aria-label="Lists"
|
|
style={({ isActive }) => ({
|
|
...tabBase,
|
|
...(isActive ? tabActiveOverride : {}),
|
|
})}
|
|
>
|
|
<List size={22} aria-hidden="true" />
|
|
<span>Lists</span>
|
|
</NavLink>
|
|
</nav>
|
|
);
|
|
}
|