Skip to content

UI — Navigation model (L0)

UI domain — L0

The web client presents one window onto a single mail store. This domain owns the shell: how that window is divided, what each region shows, and how the reader moves between regions with pointer or keyboard. The data those regions render (mailbox roles, smart mailboxes, conversations) is defined by the mail state domain (docs/state/mail/L1) and delivered through the client renderer (docs/client/L1); here we only define the navigational frame around it.

Scope note: the UI domain is being authored incrementally. This L0 covers the navigation model; other UI sections referenced from code (message list, message detail, data fetching, component hierarchy) are not yet written.

Purpose

A mail client lives or dies by how fast the reader can move: between mailboxes, through a list, into a message, and back out. The navigation model exists to make that movement predictable and pointer-free. Every region is reachable and operable from the keyboard, the current location is always visible, and the keys mean the same thing regardless of how the reader got there.

A confirmation surface appears over the window it affects: its scrim and dialog are scoped to that surface’s bounds (e.g. the compose close prompt covers only the composer), the rest of the app stays interactive, and Escape resolves the prompt rather than the surface behind it.

Views

A view is what the middle pane lists. There are two kinds:

  • Source mailbox — a concrete folder on one account (e.g. Personal / Archive), identified by (sourceId, mailboxId). Folders carry a stable role (inbox, archive, drafts, sent, junk, trash) independent of their display name.
  • Smart mailbox — a saved query spanning accounts (e.g. All Mail, or the built-in per-role mailboxes). A smart mailbox may carry the same role vocabulary as a folder; the built-in defaults cover every role plus a role-less All Mail.

The currently displayed view is the effective view. It is the unit the sidebar selects, the message list reads, and the go-to commands resolve against.

Panes and focus

The shell is up to three horizontally-stacked panes:

PaneAlways presentFocusableContents
sidebaryesyessmart mailboxes, then each account and its folders
listyesyesthe effective view’s messages
detailonly when a message is selectednothe selected message

Exactly one pane is the active pane — the target of within-pane keys. Active focus is set by pointing at a pane (pointer-down) or by rotating with Shift+H / Shift+L, which step left/right between sidebar and list and wrap at the ends. (Tab is deliberately left to native focus traversal, so assistive-technology users keep normal control-to-control tabbing.) The active pane carries a visible focus ring.

The detail pane is never the active pane: it only displays the list’s selected message. While a message is open the list stays the active pane, so j/k there step the selection — which is exactly what the detail pane follows. This keeps reading a single-pane concern (the list) rather than a focusable reader that has to hand navigation back and forth.

Plain h/l are within-pane horizontal keys, not pane movement: in the list’s conversation view they collapse / expand the focused thread (like a file tree).

Going to a mailbox

Go-to commands jump the effective view to a role without touching the sidebar. They come in two flavors:

  • Context-aware (inbox, archive, trash): when the reader is already in a source-mailbox view, the jump stays within that account and selects its folder of the requested role; otherwise (or when that account has no such folder) it selects the matching smart mailbox.
  • Force-smart (any role): always selects the matching smart mailbox, ignoring context — the escape hatch from an account-local view back to the cross-account roll-up. When several mailboxes claim a role, the built-in default wins.

This split means the only behavioral difference between the two flavors arises inside a source mailbox; everywhere else they coincide.

Selection versus focus

Two cursors coexist and must not be conflated:

  • Selection is durable: which view is shown, and which message is open. It survives moving focus between panes and drives what the list and detail render.
  • Focus is transient: the active pane. Within a pane, j/k move directly on the selection — in the sidebar they select the adjacent mailbox immediately (no separate roving cursor to commit), so the selected row’s own highlight is the cursor and is always visible. This matches the list, where j/k open the adjacent message; there is no browse-without-selecting mode.

The selected item’s highlight is accent only while its pane is the active pane; when another pane is focused, the selected item greys out. Accent always means “focused here,” so a glance at the sidebar or list tells you where input will land.

Because focus and highlight share one cursor, the list anchors to its first row when it becomes the active pane with rows but no selection — there is no state where a message is the navigation anchor but not highlighted. An explicit clear (background click, to close the detail pane and reclaim list width) is the one exception: it is left cleared until the user navigates or switches views, so closing the reader is not immediately undone.

The concrete key bindings that drive this model are specified in docs/ui/L1.