feat(05-08): PermissionDeniedBanner + App mount + CalendarShell onOpenSettings wiring

- Create PermissionDeniedBanner.tsx: role=alert banner shown only when
  permission=denied AND notificationsEnabled=1 (OS-revoked case, D-10)
  with AlertCircle icon, 'Notifications blocked' heading, inline 'How to enable'
  button that opens OS-specific instruction sheet (iOS 4-step / Android 4-step)
- Mount PermissionDeniedBanner and SettingsSheet in App.tsx; wire onOpenSettings
  state from avatar tap through CalendarShell → AppNav → PhoneNav/DesktopNav
- CalendarShell accepts optional onOpenSettings prop, threads to both AppNav usages
- playwright-cli verified: banner renders with exact UI-SPEC copy when
  permission=denied+was-enabled; banner absent when permission=granted;
  'How to enable' opens instruction sheet with correct Android steps;
  SettingsSheet opens from avatar click with toggle + permission-denied hint
This commit is contained in:
Lucas Berger
2026-06-09 21:50:48 -04:00
parent 1de4aa5a3e
commit 010a69c047
3 changed files with 302 additions and 2 deletions
+19 -1
View File
@@ -15,21 +15,36 @@
* navigateFallback ('/index.html') in vite.config.ts covers SPA deep-links to
* /lists/* — the SW denylist only excludes /callback, /api/, and /health, so
* /lists/* is served from cache correctly.
*
* Phase 5 additions:
* - PermissionDeniedBanner: shown below AppNav when OS permission revoked (D-10)
* - SettingsSheet: avatar-triggered bottom sheet with master notifications toggle (D-09)
*/
import { useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router'
import { CalendarShell } from './components/CalendarShell.js'
import { ListsIndex } from './routes/ListsIndex.js'
import { ListDetail } from './routes/ListDetail.js'
import { BottomTabBar } from './components/BottomTabBar.js'
import { PushPermissionPrompt } from './components/PushPermissionPrompt.js'
import { PermissionDeniedBanner } from './components/PermissionDeniedBanner.js'
import { SettingsSheet } from './components/SettingsSheet.js'
export default function App() {
const [settingsOpen, setSettingsOpen] = useState(false)
return (
<BrowserRouter>
{/* Permission-denied banner: shown when OS revoked and user had notifications on (D-10) */}
<PermissionDeniedBanner />
<Routes>
<Route path="/" element={<Navigate to="/calendar" replace />} />
<Route path="/calendar" element={<CalendarShell />} />
<Route
path="/calendar"
element={<CalendarShell onOpenSettings={() => setSettingsOpen(true)} />}
/>
<Route path="/lists" element={<ListsIndex />} />
<Route path="/lists/:listId" element={<ListDetail />} />
</Routes>
@@ -37,6 +52,9 @@ export default function App() {
{/* Post-install permission prompt (D-08): renders only when isInstalled() is true
and Notification.permission === 'default' and not dismissed */}
<PushPermissionPrompt />
{/* Settings sheet — master notifications toggle (D-09), opened from avatar */}
<SettingsSheet isOpen={settingsOpen} onClose={() => setSettingsOpen(false)} />
</BrowserRouter>
)
}