feat(04-01): wire BrowserRouter + BottomTabBar + empty Lists surface

- App.tsx: BrowserRouter with /calendar, /lists, /lists/:listId routes; / redirects to /calendar
- BottomTabBar.tsx: fixed-bottom 56px tab bar with Calendar + Lists NavLinks, active accent
- AppNav.tsx: add Calendar/Lists NavLinks to desktop sidebar (≥768px)
- ListsIndex.tsx: full-height surface with isLoading/isError/empty state branches; "+ New List" FAB placeholder
- ListDetail.tsx: placeholder stub for /lists/:listId (Plan 04-04 fills in)
- listsStore.ts: Zustand UI-only store (activeTab, createListSheetOpen)
- listsClient.ts: fetchLists + List/ListItem types (initial; Plans 04-02/03 expand)
- Fix Wave-0 RED stubs: add vitest imports so stubs execute (todo) not error on import
- Fix CalendarShell.test.tsx: wrap renderWithClient in MemoryRouter (AppNav uses NavLink)
This commit is contained in:
Lucas Berger
2026-06-09 12:03:52 -04:00
parent 2f25b15949
commit c0088edf44
10 changed files with 563 additions and 3 deletions
+88
View File
@@ -0,0 +1,88 @@
/**
* 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 — DesktopNav handles desktop navigation.
*/
import { NavLink } from 'react-router'
import { CalendarDays, List } from 'lucide-react'
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() {
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>
)
}