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:
@@ -11,6 +11,8 @@
|
||||
* - Grid is primary focal point; AppNav is secondary chrome
|
||||
*/
|
||||
|
||||
import { NavLink } from 'react-router'
|
||||
import { CalendarDays, List } from 'lucide-react'
|
||||
import { ColorLegend, type LegendMember } from './ColorLegend.js'
|
||||
|
||||
interface AppNavProps {
|
||||
@@ -102,8 +104,23 @@ function PhoneNav({
|
||||
)
|
||||
}
|
||||
|
||||
/** Tablet/Desktop: 240px left sidebar — app name + color legend */
|
||||
/** Tablet/Desktop: 240px left sidebar — app name + nav links + color legend */
|
||||
function DesktopNav({ members }: { members: LegendMember[] }) {
|
||||
const navLinkStyle = ({ isActive }: { isActive: boolean }): React.CSSProperties => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: 'var(--space-2) var(--space-3, 12px)',
|
||||
borderRadius: 'var(--space-1, 4px)',
|
||||
textDecoration: 'none',
|
||||
fontSize: 'var(--text-body-size, 15px)',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? 'var(--color-member-0)' : 'var(--color-text-primary)',
|
||||
background: isActive ? 'color-mix(in srgb, var(--color-member-0) 10%, transparent)' : 'transparent',
|
||||
minHeight: '44px',
|
||||
transition: 'color 0.1s ease, background 0.1s ease',
|
||||
})
|
||||
|
||||
return (
|
||||
<nav
|
||||
style={{
|
||||
@@ -133,6 +150,25 @@ function DesktopNav({ members }: { members: LegendMember[] }) {
|
||||
FamilySync
|
||||
</div>
|
||||
|
||||
{/* Section nav links */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--space-1, 4px)',
|
||||
marginBottom: 'var(--space-6)',
|
||||
}}
|
||||
>
|
||||
<NavLink to="/calendar" style={navLinkStyle} aria-label="Calendar">
|
||||
<CalendarDays size={18} aria-hidden="true" />
|
||||
Calendar
|
||||
</NavLink>
|
||||
<NavLink to="/lists" style={navLinkStyle} aria-label="Lists">
|
||||
<List size={18} aria-hidden="true" />
|
||||
Lists
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
{/* Color legend */}
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import React from 'react'
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
|
||||
// window.matchMedia is polyfilled in src/test-setup.ts (loaded via vitest.config setupFiles)
|
||||
|
||||
@@ -128,7 +129,9 @@ function makeQueryClient() {
|
||||
function renderWithClient(ui: React.ReactElement) {
|
||||
const client = makeQueryClient()
|
||||
return render(
|
||||
<QueryClientProvider client={client}>{ui}</QueryClientProvider>,
|
||||
<MemoryRouter initialEntries={['/calendar']}>
|
||||
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user