Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-317Done

5.7.5 The bell + unread badge + notification drawer in the shell header — live count, deep links, mark-read on open (no whole-tree refresh)

Estimate: 35m · Depends on: 5.7.1, 5.7.4

The user-facing surface: the bell + badge + drawer in the shell header. Design-gated (5.7.1) and built on the read/mark API (5.7.4).

Build (in app/(authed)/_components/): a NotificationBell mounted in TopNav (desktop) + the SidebarHeader/mobile affordance — the bell IconButton + the unread badge (count from getUnreadCount, 99+ cap, zero = no badge). A bounded poll refreshes the count on an interval while mounted + on navigation (the documented no-realtime decision — no realtime substrate, the 5.1 live-comments precedent). Opening the bell renders a NotificationDrawer (the popover/drawer; sheet on mobile): the Direct / Watching segmented filter (Watching disabled — the 5.4 seam), the overflow "Mark all as read", and the cursor-paged feed (newest 20 + "Show more" driving the 5.7.4 list read — never load-all). Each row is one clickable target that deep-links to the issue (/issues/[key]) AND marks itself read on open. Loading skeleton, empty state, ErrorState.

Inline-edit memory (load-bearing). Mark-read and mark-all-read are inline mutations: the success response IS the confirmation — the badge + row state update from the mutation's OWN returned unreadCount / row, NOT a router.refresh() / revalidatePath whole-tree fan-out (the refresh fan-out is what CAUSED the revert bug; the cell trusts its own success). Opening the badge marks "seen" (clears the badge count) per the Jira mirror, distinct from per-row read.

A11y: the bell is a labelled button announcing the unread count (e.g. "Notifications, 3 unread"); the drawer is a keyboard-navigable feed/list (the Linear J/K + U model, surfaced via standard controls); read state is conveyed as text + the dot, not colour alone; focus returns to the bell on close; no nested interactive elements inside a row (the portal-popover / nested-button lessons). The strict axe sweep covers the header with the drawer open.

Acceptance criteria

  • The bell renders in the header with the unread badge (matching design/notifications/{bell,drawer}.mock.html panel-for-panel: badge states, row grammar, unread-vs-read, Direct default + Watching-disabled seam, overflow mark-all); the count refreshes on poll + navigation.
  • Opening the drawer marks "seen" (badge clears); clicking a row deep-links to the issue AND marks that row read with the unread count decrementing from the mutation response, with no router.refresh()/revalidatePath; "Mark all as read" clears every dot in one request and survives reload.
  • The feed pages (newest 20 + "Show more"), never an unbounded read; empty / loading / error states match the design; colour/shape only through --el-*/element tokens.
  • The header axe sweep with the drawer open is clean (labelled bell announcing the count, keyboard-navigable feed, text-conveyed read state, focus return, no nested interactive elements).

Context refs

  • design/notifications/{bell,drawer}.mock.html + design-notes (5.7.1) — THE layout authority
  • app/(authed)/_components/TopNav.tsx / SidebarHeader.tsx / SidebarNav.tsx — the shell header components the bell mounts in; the existing header-control + popover patterns
  • notificationsService + routes (5.7.4) — the read/mark API; the unread-count poll endpoint
  • The inline-edit-no-whole-tree-refresh memory (no router.refresh fan-out on the mark-read success path) + the portal-popover / nested-button memories
  • Story 5.7 description — the no-realtime (poll) decision + the seen-vs-read split