★ Design decisions
Two IA calls the team asked the design experts to make, not punt, plus one encoding pattern this surface promotes as a candidate DESIGN.md rule. All reasoned from L1 industry convention (Google Account, GitHub, Linear, Stripe) + perceptual-UX.
Rename the H1 to "Security" (matching the nav label) and pull "how you sign in" — password, passkeys, 2FA — onto this page from /settings/account. Account keeps only profile: name, email, avatar, linked OAuth identity. Why: the universal mental model co-locates sign-in methods and active sessions under one "Security" tab — Google's Security tab does it; GitHub's "Password and authentication" does it. A user asking "is my account safe?" should land in one place that answers both "how do I get in?" and "who's currently in?". "Active Credentials" answered only the second question and forced a context-switch to Account for the first — the more dangerous half. The cost is a one-time component move (PasswordRow, PasskeyManagement, 2FA from Account → Security); the payoff is removing a split that no peer product tolerates.
Demote both to a single "Active under your identity" chip strip that links out. Why: credentials answer "what can sign in as me?" Casts and share-links answer a different question — "what's running or shared under my identity?" — and rendering them as full sections dilutes the page's one job and inflates scroll. As chips they stay visible (status at the edge) without competing. The one carve-out: a live cast running as you is a security action to stop, so the cast chip keeps a one-tap Stop affordance inline; share-links, being passive, get only Manage →. This mirrors how Stripe surfaces "active" secondary objects as status rows that link to their own management surface rather than expanding them in the credentials view.
Every credential/connector row surfaces two timestamps: added (provenance, muted, second line) and last used (recency, emphasized, first line). Why: recency-of-use, not age, is the revoke-safety signal. Showing both lets a user tell "old but actively used → do not revoke" from "old and unused → safe to cull" at a glance — age alone conflates the two and a single relative time hides whichever one you didn't pick. Recently-used rows (< 7 days) carry a sage active-pip (color + shape, not hue alone) and a faint left-edge accent so a live credential reads as load-bearing; their Revoke surfaces a one-beat friction caption ("Used Nh ago — revoke anyway?") that the product backs with a confirm step. Stale rows recede (cool/muted used-time, plain Revoke) so the safe-to-cull set is the visually quiet set. Candidate rule for DESIGN.md: "Credential rows encode two times — provenance (added, muted) + recency (last used, emphasized); recency is the revoke-safety signal."
★ B. Attention state — the hero
/settings/security page.tsxThe full page with the Attention Tray populated. Tray is the only place anxiety is allowed to live — and every row offers exactly one next move. Below it, the page returns to calm.
Security
How you sign in, and everything that can act as you. Review anything that looks unfamiliar.
How you sign in
Methods that authenticate you moved from /settings/accountActive credentials
12 total · grouped by typePosture band = "status at the edge" (§11 Rule 9). One line, surface-2, divider-separated cells. The 7-day sparkline is decorative-but-honest — last bar is the current spike (today's sign-ins). No hero card; anxiety would be manufactured. Counts use font-variant-numeric: tabular-nums so they don't jitter on re-render.
Attention Tray severity is color + SHAPE (§11 Rule 3). danger = octagon ⬣ + red square chip; warn = triangle ▲ + amber square chip; info/review = filled circle ● + sage round chip. A colorblind user reads the glyph and chip-radius, not the hue. Each row has exactly one primary next-move (Copper only on Rotate, since rotation is the affirmative create-like action); everything else is ghost/danger-ghost. Tray is display:none when empty — see State A.
Rows encode two times — provenance + recency (decision 3, DESIGN.md candidate). The right cell carries used … (recency, --text, tabular, the load-bearing revoke-safety signal) on the first line and added/first-seen/paired/installed … (provenance, --text-subtle, smaller) on the second. The duplicated "created …" fragment was removed from .secondary — provenance now lives in the usage cell, not buried in hover-only metadata. Why two times: age conflates "old but live" with "old and idle"; recency-of-use disambiguates them at a glance, so a user can cull confidently without opening each row. On .active rows (used < 7 days) the date provenance label adapts to the credential type — browser sessions first seen, MCP grants added, devices paired, app sessions installed, CLI PATs added.
Active-row cue is color + SHAPE (Rule 3), and is doubly redundant. Recently-used rows get a sage .pip (round, filled — not hue-only) before the used time and a faint sage left-edge accent on the row. Stale rows get neither and their used time reads cool/muted (--text-subtle, normal weight) so the safe-to-cull set is the visually quiet set — the eye is pulled toward what's live, away from what's idle.
Anti-accidental-revoke guard. On .active rows the Revoke button carries a data-guard caption ("Used Nh ago — revoke anyway?") that surfaces on hover/focus as one beat of friction. This mockup shows only the cue; the real product gates revoke of a recently-used credential behind a confirm step (a single dialog: "name was used Nh ago. Revoke it?"). Stale rows keep the plain Revoke with no guard — they read as safe, so we don't tax the common cull path. Devices say "unpair anyway?" to match the verb.
Calm mode. The .added provenance line is optional metadata, so under html[data-calm="true"] it dims to ~50% and reveals on hover/focus — same treatment as the .gkey chips and sign-in descriptions. The used recency line and the active-pip stay full-contrast: they're signal, not noise.
Copper budget on this viewport: exactly one — the Rotate CTA in the tray. The "How you sign in" Add-2FA and "Sign out everywhere else" are ghost; chips use neutral/danger-tint, never Copper. The guard caption is amber (warn), not Copper.
A. Clean state — tray hidden
/settings/security page.tsxEverything healthy. The Attention Tray is removed from the DOM entirely (not collapsed) — calm by default; we never render an empty "0 issues, all good!" card. The posture band carries the all-clear at the edge.
Security
How you sign in, and everything that can act as you.
How you sign in
Methods that authenticate youActive credentials
4 totalEmpty tray = no tray (Apple calm). When the signal list is empty the component returns null — we do not render a celebratory "you're all caught up" card. The posture band's all reviewed dot is the only acknowledgement, and it's at the edge. Manufacturing a green hero where nothing is wrong trains users to ignore the tray when something is wrong.
Both rows are active. Both CLI credentials were used within a day, so both carry the sage pip + left accent + guarded Revoke — the clean state has nothing safe-to-cull, which is exactly why no row recedes. The dual-timestamp cell still shows provenance (added …) underneath so the user can read age even when everything is live.
Identity strip also hidden when there are no active casts or share-links — same rule.
C. Self-healing error — one source failed
/settings/security ActiveCredentialsList.tsxThe MCP grant source timed out. Rule 11: the banner names the failed source, offers Retry, and still shows everything that loaded. Contrast with today's dead-end below.
Security
How you sign in, and everything that can act as you.
Active credentials
9 loaded · MCP pendingBefore → After (Rule 11, self-healing).
MCP grants), keeps the 9 credentials that loaded fully actionable, and offers two retry affordances: a page-level banner button and a localized "Retry just MCP" in the group header that re-fetches only the failed source.
The failed group is not a hole. Its header still renders with a — pending count chip (amber, distinct from the round numeric chips) so the taxonomy stays stable and the user knows the type exists. A retry that succeeds slots the rows in without a layout jump. The error stub row carries no usage cell — there's no credential to time, so we don't fake one.
Loaded rows keep the dual-timestamp + guard. Even mid-failure, the 9 credentials that did load show provenance + recency and guard their active Revoke — degraded loading never strips the safety affordances off the data that is present.
Posture band degrades honestly: "4 types loaded · 1 source pending" instead of claiming a total it can't verify.
Anatomy notes
Component-level rationale. Each lives inline in the states above; collected here for implementors.
1 · Posture band. New reusable pattern candidate. Single-line, edge-placed status summary for any entity page. Cells divider-separated; tabular-nums; one optional Tufte micro-sparkline; right-aligned "you are here" anchor. Proposed token: nothing new — composes from surface-2 + existing text tiers.
2 · Attention Tray (DDR-004). Reuses the mandatory schema in packages/ui/src/AttentionTray/types.ts. Signal sources for this surface: token-expiry, unused-credential sweep, new-location sign-in, 2FA-absent. Severity rendered with the color+shape pair table above. One primary action per row; onDismiss per signal; whole tray unmounts when signals.length === 0.
3 · How you sign in. Pulled from /settings/account per decision 1. Components to move: PasswordRow, PasskeyManagement, the 2FA toggle out of DangerZone. Status pills use color+shape: ● filled = on/set, ▲ triangle = action-needed/off.
4 · Current-device anchor. Pinned above the groups, sage-tinted, 3px left rule + "This device" badge + monogram icon — three redundant signals so it reads as "you are here" even in calm/desaturated mode.
5 · Credential list density + dual timestamp (decision 3). Candidate new spec: row = icon + name + right-aligned usage cell (two values) + ghost Revoke. The usage cell stacks used … (recency, --text, tabular — the load-bearing revoke-safety variable) over added/first-seen/paired/installed … (provenance, --text-subtle). Recency < 7 days marks the row .active: sage pip (color + SHAPE, Rule 3) + faint sage left-edge accent + a data-guard caption on Revoke; the product backs that caption with a confirm step so a live credential gets one beat of friction before revoke. Stale (90d+) rows mute the name + read the used-time cool, drop the pip/accent, and keep a plain Revoke — they're the safe-to-cull set and should recede. Secondary metadata (IP/location/version/firmware/push/expiry) stays max-height:0; opacity:0 and expands on :hover/:focus-within (Rule 10); the duplicated created-date was removed from it since provenance now lives in the usage cell. Per-row Revoke is ghost-until-hover. Group header carries the create/manage affordance and a hover-reveal bulk control — this replaces the floating BulkRevokeRail. Keyboard digits 1–5 still jump to group (the .gkey mono chips); tabindex on rows enables J/K sequential review (Rule 12).
6 · Identity strip. Cast chip keeps inline Stop (remote-stop is a security action); share-links chip is Manage-only (passive). Both link to their own surfaces (/cast, /settings/sharing).
Owner files
apps/web/app/(protected)/settings/security/page.tsx— page shell, posture band, tray hostapps/web/src/components/settings/security/ActiveCredentialsList.tsx— groups + rows + dual-timestamp cell + revoke guard + self-healing bannerpackages/ui/src/AttentionTray/— DDR-004 tray (schema + shell)- Move from
apps/web/app/(protected)/settings/account/:PasswordRow,PasskeyManagement, 2FA toggle (decision 1) - Retire: floating
BulkRevokeRail(bulk control now co-located in group headers)