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
+3 -1
View File
@@ -72,7 +72,7 @@ function isPhone(): boolean {
// ── Component ──────────────────────────────────────────────────────────────
export function CalendarShell() {
export function CalendarShell({ onOpenSettings }: { onOpenSettings?: () => void } = {}) {
// Use per-field selectors so CalendarShell does NOT subscribe to openEventId.
// Without selectors, any popover open/close triggers a full re-render here,
// which rebuilds the Schedule-X config and causes a visible calendar flash (Bug B).
@@ -358,6 +358,7 @@ export function CalendarShell() {
members={members}
currentUserColor={meQuery.data?.user.color}
currentUserName={meQuery.data?.user.displayName ?? undefined}
onOpenSettings={onOpenSettings}
/>
<InstallPrompt />
{calendarContent}
@@ -421,6 +422,7 @@ export function CalendarShell() {
members={members}
currentUserColor={meQuery.data?.user.color}
currentUserName={meQuery.data?.user.displayName ?? undefined}
onOpenSettings={onOpenSettings}
/>
{/* Main content area */}