Cinatra — Application Design — Notifications

The unified /notifications surface — one chronological list that holds notifications and pending approvals together, filtered (not clustered), reached from a badge-only bell. It replaces the approvals inbox and the bell flyout. Part of Cinatra Application Design; match it exactly.

Version 0.1.4

I. One surface, one chronological list

Route /notifications · notifications + approvals · one list

One page replaces two. /notifications is the single home for both notifications and pending approvals. It absorbs the former /configuration/approvals inbox and the bell flyout outright — both surfaces are gone (§VIII). It sits in the Wide content width (the max-w-3xl · 768px column defined in the Application Design system, §VII Content widths) — the surface previously carried no width tier of its own. The page opens with the standard page header (title Notifications, a one-line description — no closing rule of its own: the toolbar below replaces it, per the Toolbar component's own rule), then a toolbar (§III), then the list.

Exactly one list, ordered by time. Notifications and pending-approval rows are interleaved into a single chronological list, most-recent first by each item's created time. There are no clusters and no section headers — the Needs your decision / Your requests groupings carried over from the app spec's approvals inbox (app.html §IX) are retired and must not appear here. A view is never split into "your requests" versus "awaiting you"; those are expressed as filters over the one list, never as separate sections or lanes.

Approvals stay read-time federated. Pending-approval rows are read at request time from the existing approval-source registry; they are not materialized as rows in the notifications table. Only the page is unified — the two data models are unchanged behind it. A notification row is a per-user read/unread row; an approval row is an org-shared decide-once item with eligibility (§II).

Example — /notifications, populated (one interleaved list)

Notifications

Everything that needs your attention — updates and pending approvals, newest first.

  • Approve access scope for Outreach agent Awaiting you

    Agent approval · requested by a teammate · 4 minutes ago

  • Prospect list finished — 240 rows

    Prospect Lists · 22 minutes ago

  • Your request to publish “Q3 sequence” Awaiting others

    Promotion approval · you requested this · 1 hour ago

    no action for you
  • Deploy to production succeeded

    Workflows · yesterday

1
Page 1 of 2 · 27 total

II. Row anatomy — one shell, two species

Spaced cards · uniform slots · read-state ≠ eligibility

One row shell, uniform slots. Every row — notification or approval — is rendered through the same shell, whose four regions comprise six uniform slots: a leading icon / kind glyph (slot 1); a title line carrying the title (slot 2) plus, on approval rows, the status pill (slot 3); a subtitle / meta line carrying the source, actor and context (slot 4) together with the timestamp (slot 5); and a trailing status / action slot (slot 6) that carries EITHER the notification's read/unread toggle OR the approval's inline-action. Every row fills all six slots — icon/kind, title, status pill, subtitle/meta, timestamp, and the trailing status/action — even when a given slot is empty. Each row is now its own spaced clickable card, not a hairline-divided row: --surface-strong (white, per design-system Rules of the road #8 — the row is something the viewer touches now), a 1px --line border, a 10–12px radius, and a visible gap between cards; hovering a card lifts it 1px, matching the shared Card component (Application Design — Components).

Read-state is a notifications-only concept. The read/unread toggle and its icon apply to notification rows only. Approval rows carry no read-state at all — they are org-shared decide-once items, so there is no per-user "unread" to show; their status slot carries a status pill instead, and the Unread segment never counts them.

“Needs action” is eligibility, not raw pendingness. The Needs action state — the toggle-group segment (§III) and the bell badge (§IV) — reflects rows the viewer can actually decide, not every pending approval. A read-only passthrough row (a legacy inbox item marked non-actionable) does not qualify; an approval the viewer themselves requested and cannot self-approve does not qualify. Eligibility, evaluated per viewer, drives the row's inline action, the Needs action segment, and the badge; a pending row the viewer cannot act on renders its status pill but exposes no decide action.

Whole-card activation, by species. Clicking anywhere on a card — not only its title — is the card's stretched sibling overlay (a link or button positioned over the card surface, laid beneath the visible content in the same stacking context): a link when the row has somewhere to go, a button when it doesn't. It is never role="button" on the card itself — that forces every descendant presentational to assistive tech, which would break the approval cards' nested inline controls. Those inline controls (Decline / Approve, the read toggle) sit visually above the overlay so they keep receiving their own clicks.

SpeciesCard controlActivation behavior
Notification + hrefStretched linkAuto-marks read, then navigates
Notification, no hrefStretched buttonToggles read / unread
Approval + hrefStretched linkNavigates only — no read semantics (above)
Approval, no href— not card-activatable —Inline decide actions only

Href-less approvals opt out of the whole card. That fourth species carries no stretched overlay at all — the card itself does nothing on click; only its inline decide controls (when the viewer is eligible) are live. The exemption is species-driven, not a fixed list of approval kinds — whichever approval kinds happen to be href-less on a given instance are exempt, and whichever carry an href get the stretched link.

The read/unread toggle — one envelope, two states. Every notification card carries a small icon button in the trailing slot, sitting above the overlay: unread shows a closed envelope (the action marks it read); read shows an opened envelope (the action marks it unread). One glyph pair, two states — the same method as the joined-plug connected/disconnected pair (Application Design — Connectors). Approval cards never carry this control.

Example — notification card, both toggle states
Import finished

Data · 5 min ago

Weekly digest ready

Reports · yesterday · no href

Status slot = the toggle (closed envelope unread, opened envelope read). Top card: href, stretched link, activate → navigated. Bottom card: no href, stretched button, activate → toggled. Timestamp in the meta line.

Example — approval card (no read-state; eligibility-gated action)
Approve connector install Awaiting you

Marketplace · 12 min ago

Status pill on the title line; the trailing slot holds the inline decide because the viewer is eligible. Never a read toggle.

III. Filters — All · Needs action · Unread · In progress

Toolbar · toggle group leads · Mark all read anchors the right

One toolbar, a toggle group leading it. A toolbar (the shared Toolbar component, Application Design — Components) sits above the list. It leads with a single-select toggle group (the shared Toggle / Toggle-group component) whose segments are exactly these switch options, in this order: All (the default, selected on load), Needs action, Unread, In progress; every segment but All carries its live count. Selecting a segment narrows the single chronological list in place — it never switches to a separate list, lane, or route, and never changes the interleaving or the time order of what remains. This is the same one-list-narrowed-in-place behavior the surface has always had; only the control has changed, from chips, to tabs, to this toggle group.

What each segment shows. All is every row (default). Needs action is the eligibility set from §II — only rows the viewer can actually decide (never mere pendingness). Unread is unread notifications only — approval rows, which carry no read-state, are never in it. In progress is the in-flight items (runs and jobs still working), collapsed by job. Selecting a segment applies its filter over the one list; returning to All restores the full list.

Mark all read anchors the far right, as an icon button. A borderless icon button — the double-check glyph, matching its action — sits at the toolbar's far right, separated from the toggle group by the toolbar's own hairline rule, exactly like the connection-filter toolbar's Sort & display button in Application Design — Connectors. The toolbar itself replaces the page header's closing rule (the Toolbar component's own stated rule); there is no separate tablist rule or content-header row to reason about.

Example — toolbar (toggle group leading, Mark all read trailing)

Selecting a segment narrows the list below in place. There is no second list and no route change — the chronological order of the surviving rows is unchanged. Mark all read is the borderless icon button at the far right of the same toolbar.

IV. The bell — badge + link, no flyout

Badge count · navigates to /notifications

The bell is a badge and a link — nothing more. The top-bar bell shows a count badge and links to /notifications. Clicking it navigates to the page; it does not open an in-place flyout, dropdown, or panel. There is no hover preview and no inline list under the bell — the flyout is retired (§VIII). The whole entry into notifications is the page itself.

The badge counts what needs the viewer. The badge is driven by the same viewer-eligibility signal as the Needs action segment (§III) — it counts what the viewer can actually act on, not raw pendingness. It is not a raw pending-approval count.

Example — bell (badge + link; click navigates, no panel)
2

The badge is the eligibility (Needs action) count. Activating the bell routes to /notifications. No dropdown opens under it.

No flyout

The bell has no attached panel. It is a badge and a link — the only in-place notification UI is the /notifications page. The former flyout list under the bell is removed, not relocated.

V. Empty state & vendor gating

One empty state · vendor rows gated

One universal empty state. When there is nothing to show, the page renders a single empty state whose text is exactly “No notifications”. There is never a per-type or per-source empty state — no "no approvals", no "no marketplace items", no per-kind empties. The one message covers an empty list regardless of which underlying sources are quiet.

Vendor content is gated on registration. Vendor rows and vendor copy render only when the instance is a registered vendor. On any instance that is not a registered vendor, zero vendor rows and zero vendor copy appear — the vendor surface is absent, not merely empty. Sources that are not connected or not configured contribute zero rows and zero UI to this surface (connectivity is owned by configuration, not this page).

Example — empty (the single universal state)

No notifications

Example — non-vendor instance (zero vendor UI)

On a non-vendor instance the list shows only its notifications and approvals. No vendor rows and no vendor copy render at all — the vendor surface does not appear until the strict registered-vendor predicate holds.

VI. Degraded sources — one inline line

One line · never per-source sections

A failed source degrades to one line. Approvals are federated across several sources; when one or more of them fail or time out, the page shows a single inline line reading exactly “some approvals are currently unavailable”, above the list. It never renders a per-source error section, a per-source card, or a stack of per-source failure notices. This explicitly supersedes and retires the app spec's approvals-inbox per-source-card / per-source-error layout (app.html §IX) for this surface — a global chronological sort forbids streamed per-source sections anyway.

Degraded page 1 shows the affordance, never a pager alongside it. When the degraded line above is showing on page 1, the bottom pager (§VII) does not render — the retry affordance takes its place. A degraded page cannot honestly promise a working next, so the two are never shown together.

Example — degraded source (single inline line)
some approvals are currently unavailable

One line for any number of failed sources. The rest of the list renders normally beneath it; no source gets its own error block. On page 1 this line stands in place of the pager.

VII. Pagination — 25 per page

Numbered pages · “X of N” caption · Load more retired

25 rows per page, counted after collapse. The list paginates at the bottom, 25 rows per page, counted over the rendered rows — after job notifications have already been collapsed and the active filter applied, never over raw fetched rows. “Load more” is retired: the surface no longer grows an ever-longer list on scroll.

Known total, like every other list. /notifications uses the shared Pagination component exactly as specified (Application Design — Components) — no exception: numbered pages, the active page ink-filled, surrounding pages line-bordered on white, paired with an “X of N” caption in mono slate. The total is counted over the same rendered row set the pager pages through (post-collapse, active-filter-applied).

When it shows, and what resets it. The pager renders only when there is more than one page — a single page of results shows no pager at all. Switching filters (§III) returns to page 1: a filter narrows the list to a different row count, so any remembered page position from a different filter would be meaningless. A degraded page 1 shows the degraded affordance (§VI) in the pager's place, never both together.

Example — pager (page 2 of 4, 87 rows total)
2
Page 2 of 4 · 87 total

The standard numbered pager, with its “X of N” caption, over the 25-per-page rendered rows. The normative page-prev -> paged / page-next -> paged conformance actions are annotated on the list surface (§I).

VIII. Entry points & retirement

Sidebar Approvals gone · flyout + /configuration/approvals removed

The left-sidebar “Approvals” entry is gone. The sidebar no longer carries an Approvals navigation item. The only entry points into approvals are the bell (§IV) and the /notifications page itself. There is no separate approvals destination in the navigation.

The old surfaces are removed. The /configuration/approvals inbox and the bell flyout are deleted, not relocated. The ?direction=inbox|mine URL machinery is gone from the surface; inbox/mine survives only as an internal domain concept (the approval tools and per-row eligibility still need it), never as a tab, a route parameter, or a visible view here.

Retired here, not patched

The approvals inbox and the bell flyout are superseded by this one surface. Their clustering (Needs your decision / Your requests), their per-source error cards, and the sidebar Approvals item do not carry forward. This page is the normative contract for approvals in the app: where the older surfaces and this one disagree, this page governs.