# Interaction conventions — channels-manager

> Status: **BINDING on every sweep mockup and every build that follows one**
> (operator, 2026-07-29: the sweep is worthless unless grounded in researched
> principles rather than model memory). Sits under `design/tokens.md` in
> precedence: tokens rule how a screen looks, this rules how it behaves. Where
> a line here collides with `docs/DECISIONS.md`, DECISIONS wins and you surface
> the collision instead of reconciling it.
>
> Every line carries a source key, resolved in **§4**. A line marked
> `(judgment)` is a house call with no external source; those are few and
> deliberate. Nothing else here is allowed to be a vibe.

This is an **operator console**: a handful of named people, all day on a desktop
monitor, doing investigative work under time pressure (`PRODUCT`). Not a
consumer app, not a marketing site. Density, keyboard reach and honest state are
the whole job.

## §1 Principles

1. **Speed is a feature and the bands are known.** 0.1s reads as instant, 1.0s
   is the limit for uninterrupted thought, 10s is the limit of held attention
   and owes a progress readout plus an escape. Over ~1s: optimistic state or a
   skeleton at once. Over 10s: progress plus cancel. (`NN-RT`)
2. **Every action gets feedback**, appropriate and within a reasonable time. No
   silent success, no silent failure. (`NN-H1`, `SHN-3`, `PRODUCT`)
3. **Plain words, the operator's words.** User's language and familiar
   concepts, no internal jargon. Only the integrator pages (ApiDocs, curl
   panel) are excepted. (`NN-H2`, `DEC`)
4. **Undo beats confirm, and the operator stays in control.** Reserve modal
   confirms for the irreversible; over-used confirms stop preventing anything
   ("cry wolf too many times, people stop paying attention"). Ship undo anyway,
   plus a marked exit from any unwanted state. (`NN-CD`, `NN-H3`, `SHN-6/7`)
5. **Direct manipulation.** Act on the displayed object with physical,
   incremental, reversible actions whose effect is immediately visible. Edit the
   row before opening a form about the row. (`NN-DM`)
6. **One pattern per job, and it is the conventional one.** Follow platform and
   industry convention rather than inventing; one job never gets two
   treatments. Opinionated defaults beat configurability: "flexible software
   lets everyone invent their own workflows, which eventually creates chaos".
   (`NN-H4`, `SHN-1`, `LIN-M`, `CLAUDE.md`)
7. **Prevent the error before catching it.** Eliminate error-prone conditions
   or confirm at the point of commitment. (`NN-H5`, `SHN-5`)
8. **Recognition over recall.** Options, filters and current state stay visible;
   never make the operator carry an ID or a filter in their head across screens.
   (`NN-H6`, `NN-RR`)
9. **Accelerators for the expert, invisible to the novice.** Frequent actions
   reachable without the mouse; the full list one `?` away. (`NN-H7`, `LIN-KS`)
10. **Minimalist by subtraction, then progressive disclosure.** "Every extra
    unit of information competes with the relevant units." Rare controls defer
    to a second layer and the reveal mechanism is obvious. (`NN-H8`, `NN-PD`,
    `NOTION-W`)
11. **Errors name the cause and the fix**, in plain language, never a dead end.
    (`NN-H9`, `B2`)
12. **Fewer choices and bigger targets are faster.** Decision time grows with
    option count, acquisition time falls with size and proximity. Floor 24x24
    CSS px, house floor 36px. (`IXDF-HICK`, `NN-FL`, `WCAG-258`, `PRODUCT`)
13. **Keyboard operability is not optional and focus is always visible.** All
    functionality keyboard-operable (2.1.1), focus order preserves meaning
    (2.4.3), the indicator is visible (2.4.7) and not hidden behind sticky
    chrome (2.4.11, new in 2.2). Sticky toolbars and selection bars are the
    usual 2.4.11 offenders. (`WCAG-211/243/247/2411`)
14. **Every dynamic surface designs all six states**: empty, loading, error,
    success, partial, offline, each with its entry trigger and its exits.
    Shipping the happy path alone is the named failure. (`B2`, `PRODUCT`)
15. **Colour carries meaning, never identity and never decoration.** One indigo
    accent on zinc; status is a word plus a colour, never colour alone.
    Restricting the palette is an accessibility decision: Stripe limits app
    colours "because color contrast is an important aspect of accessible UI".
    (`TOKENS`, `DEC`, `WCAG-141`, `STRIPE-D`)

## §2 Pattern conventions

### Tables and lists

- Default column order reflects importance to the operator; related columns sit
  adjacent so the eye travels less. (`NN-DT`)
- The first column is a **human-readable identifier** (name, phone, subject),
  never an auto-generated ID. (`NN-DT`)
- **Text left-aligned, numbers right-aligned**, headers copying the alignment of
  the data beneath. (`EU-TBL`)
- Numbers use **tabular figures** (`font-variant-numeric: tabular-nums`,
  OpenType `tnum`) in tables and stat tiles. (`MDN-FVN`, `BUTTERICK`, `TOKENS`)
- Number formatting is consistent within a column: same rounding, same digits,
  same or no prefix. Missing values render as the em-dash glyph, the only place
  that character appears. (`EU-TBL`, `TOKENS`)
- **Row density is a house value, not a per-screen choice**: row padding 9px,
  cells 13px. Carbon offers four row heights as a system-level decision; we made
  ours once and screens do not override it. (`TOKENS`, `CARBON-DT`)
- **Header rows freeze** when the table outgrows the viewport. (`NN-DT`)
- Hairline separators plus **whole-row hover highlight** to hold place. We use
  hover rather than zebra striping because rows already carry hairlines.
  (`NN-DT`, `TOKENS`)
- **Sorting** lives on the header, ascending/descending, with the active column
  and direction visibly marked. (`CARBON-DT`, `EU-TBL`)
- **Row click navigates to the record's detail. It never mutates.**
  (`POLARIS-IT`)
- **Row actions**: one or two inline in a trailing right-aligned cell; three or
  more collapse to an overflow menu, since single-record actions "work only for
  one or two operations". Visible on hover *and* on keyboard focus.
  (`NN-DT`, `WCAG-211`)
- **Pagination, not infinite scroll**, past roughly 50 rows. (`POLARIS-IT`,
  `CARBON-DT`)
- Column hide/reorder, where offered, is easy and shows what is hidden.
  (`NN-DT`)

### Selection and bulk actions

- Checkbox in the leading cell; **shift-click extends a contiguous range**;
  `Cmd/Ctrl+A` selects the page. (`POLARIS-IT`, `LIN-SEL`)
- Selection reveals a **batch action toolbar placed with the table**, not
  floating detached over the page. (`CARBON-DT`, `DEC`)
- The bar states the count and offers **select-across-pages explicitly**
  ("N selected on this page", then "Select all {total}"), never silently.
  (`POLARIS-IT`, `DEC`)
- `Escape` clears the selection. (`LIN-SEL`)
- Destructive bulk actions confirm; everything else offers undo. (`NN-CD`)

### Inbox and threads

- **Master/detail**: list left, thread right, list stays visible so context
  survives. (`MD-WIKI`, secondary source)
- Conversation navigation is `j`/`k` and `u` back to the list. (`GMAIL-KB`)
- Conversation verbs: `r` reply, `a` reply all, `e` archive/resolve, `s` snooze,
  `x` select. Assignment `Shift+A` to self, `Shift+G` to a teammate. Front ships
  three interchangeable schemes, which proves the set is a preference, not a
  law: pick one and hold it. (`GMAIL-KB`, `FRONT-KB`)
- **Internal notes read as internal at a glance**: `warn-soft` fill plus an
  explicit "Internal note" label, never colour alone. The yellow-note convention
  is real but third-party-reported for Zendesk, so ours derives from our tokens.
  The composer remembers its mode per conversation. (`ZD-NOTE` third-party,
  `TOKENS`, `WCAG-141`)
- **Snooze in a shared inbox snoozes for everyone**, and the UI says so.
  (`FRONT-SNOOZE`)
- Sending is never silent: an optimistic row appears at once, then resolves to
  sent or failed with a reason. (`NN-H1`, `NN-RT`)

### Canvas editors (the flow builder)

Figma is the convention set. Where Figma has no answer it is marked.

- **Pan**: hold `Space` and drag; arrow keys pan with nothing selected, `Shift`
  for a larger step. (`FIG-KB`)
- **Zoom**: `Shift+1` fit, `Shift+2` to selection, `Shift +`/`Shift -` to step,
  modifier-scroll to zoom. (`FIG-ZOOM`)
- **Multi-select**: `Shift`+click adds or removes, drag on empty canvas
  marquees, `Cmd/Ctrl+A` selects all. (`FIG-SEL`)
- **Quick add / command menu**: `Cmd/Ctrl+K`. That is the current Figma binding;
  older docs say `Cmd/Ctrl+/`, so cite the current one. (`FIG-QA`)
- **Inline rename**: double-click the node name on canvas, `Escape` to leave.
  (`FIG-REN`)
- **Undo/redo** `Cmd/Ctrl+Z` and `Cmd/Ctrl+Shift+Z` (plus `Ctrl+Y`), no-ops
  while the caret is in a text field so the field owns its own undo. **Save** is
  `Cmd/Ctrl+S` and works even while typing. Both already correct in
  `useCanvasShortcuts.ts`; keep it. (`CODE`, `SHN-6`)
- **A minimap is optional, not a Figma convention.** Figma ships none; every one
  on offer is a community plugin. Zoom-to-fit plus a node list is the cheaper
  answer for an oversized flow. (`FIG-MINI` verified absence; substitute is
  `judgment`)
- Node deletion stays with the canvas library's own `Delete` handling, not a
  second global handler. (`CODE`)

### Filters and facets

- Only the **one or two filters used on most visits stay inline**; the rest
  collapse behind one "Filters" control with a badge counting active ones.
  (`NN-PD`, `DEC`)
- **Active filters stay visible and individually removable** without reopening
  the panel. (`NN-H6`, `NN-RR`)
- Filter state lives in the **URL**; saved views are URL-only. (`DEC`)
- Every option list is pulled from code or the API, never hand-written in a
  mockup. (`DEC`)
- Filtered-to-zero is a **different empty state** from never-had-data and offers
  "clear filters", not "create your first thing". (`STRIPE-EMPTY`, `B2`)

### Detail panels

- Side panel enters from the right over a scrim at `min(440px, 100%)` and
  **never replaces the page's own header, action row or tabs**. Escape, scrim
  and explicit close all dismiss. Only the panel body scrolls. (`TOKENS`)
- **Default to side-by-side context, not a takeover.** Stripe's rule is "default
  to `ContextView`", reserving the full canvas for a workflow that genuinely
  does not need the page behind it. (`STRIPE-D`)
- Opening a panel never loses list position or selection. (`NN-H3`)
- A panel is a dialog for focus: focus moves in on open, `Escape` closes, focus
  returns to the invoker. (`APG-DLG`)

### Forms

- **Composite widgets (select, dialog, menu, tooltip, tabs, toggle, combobox)
  build on the chosen headless primitives library**, skinned with `--cm-*`.
  Plain text inputs stay dependency-free. A native `<select>` is not a designed
  dropdown. (`DEC`)
- Field chrome is one spec: surface fill, 1px `line-strong`, radius 4, padding
  10/12, accent hover border, 2px `accent-ink` focus ring at 2px offset,
  small-caps labels. (`TOKENS`)
- **Validate at the point of commitment**, and constrain the input rather than
  letting it be typed wrong. (`NN-H5`, `SHN-5`)
- Every reachable field state is specified: enabled, hover, focus-visible,
  pressed, disabled, error, loading. Focus-visible and disabled are the ones
  people forget. (`D1`)
- Error text sits with the field and its slot reserves height so the layout
  never jumps. (`NN-H9`, `CODE`)
- **Dialogs**: focus in on open, `Tab` cycles within, `Escape` closes, focus
  returns. (`APG-DLG`, `PRODUCT`)

### Empty, loading and error states

- **Render order is loading, error, empty, then the list.** (`STRIPE-EMPTY`)
- **Skeletons, not spinners**, for sub-second content; progress plus cancel past
  ten seconds. (`PRODUCT`, `NN-RT`)
- Empty copy: the title **states what is missing** and does not sell ("No
  successful payments." not "Try creating your first payment to get started!").
  Description under about 14 words, active voice. The action label echoes the
  title ("No contacts" leads to "Add contact", not "Get started").
  (`STRIPE-EMPTY`)
- Three empties are three screens: first-run, filtered-to-zero, all-removed.
  Never a create-first CTA when items exist but are filtered out.
  (`STRIPE-EMPTY`, `B2`)
- Empty and thin states are **computed from live system state, never canned**.
  (`DEC`)
- Error banners are full-width, persistent, and carry the recovery action.
  (`STRIPE-BANNER`)
- Status chips are read-only and perform no action; if it is clickable it is not
  a chip. Vocabulary maps to `ok`/`warn`/`bad`/`info` and always carries its
  word. (`STRIPE-BADGE`, `TOKENS`)

### Keyboard

**Must have a shortcut**, because an operator repeats them all day: list
navigation, open/close the detail, the primary create, assignment, the primary
state change (resolve/archive/approve), search, undo.

Conventional bindings. Deviate only with a written reason.

| action | key | source |
|---|---|---|
| command menu / quick actions | `Cmd/Ctrl+K` | `FIG-QA`, `LIN-CMD` |
| shortcut help overlay | `?` | `LIN-KS`, `GMAIL-KB` |
| next / previous in a list | `j` / `k` and arrows | `GMAIL-KB`, `LIN-SEL` |
| toggle selection of hovered row | `x` | `GMAIL-KB`, `LIN-SEL` |
| extend selection | `Shift`+click, `Shift`+arrow | `LIN-SEL`, `POLARIS-IT` |
| select all on page | `Cmd/Ctrl+A` | `LIN-SEL`, `FIG-SEL` |
| clear selection / close / dismiss | `Escape` | `LIN-SEL`, `APG-DLG` |
| back to the list from a record | `u` | `GMAIL-KB` |
| search | `/` | `GMAIL-KB` |
| reply / reply all | `r` / `a` | `GMAIL-KB`, `FRONT-KB` |
| archive or resolve | `e` | `GMAIL-KB`, `FRONT-KB` |
| snooze | `s` | `FRONT-KB` |
| assign to self / teammate | `Shift+A` / `Shift+G` | `FRONT-KB` |
| insert a block or node inline | `/` | `NOTION-SL` |
| undo / redo | `Cmd/Ctrl+Z` / `Cmd/Ctrl+Shift+Z` | `CODE`, `FIG-KB` |
| save | `Cmd/Ctrl+S` | `CODE` |

- **Single-key shortcuts must not fire while the caret is in a text field.**
  Enforced in `useCanvasShortcuts.ts`; every new handler copies it. (`CODE`)
- A table taking arrow-key navigation follows the APG grid pattern: arrows move
  cell to cell, `Home`/`End` to row ends, `Ctrl+Home`/`Ctrl+End` to first and
  last cell, `PageUp`/`PageDown` by a screen, and **only one element of the grid
  sits in the page tab sequence** (roving tabindex). (`APG-GRID`)
- Anything reachable by hover is reachable by keyboard focus. (`WCAG-211`)

## §3 Compliance checklist

Copy into the round's `ANNOTATIONS.md` per screen and tick before submitting.

- [ ] Every colour, size, radius and spacing value comes from a `--cm-*` token.
      No raw hex, no off-scale spacing. (`TOKENS`, `C1`)
- [ ] No navy, no gold outside the logo asset and the web-channel chip. No serif
      outside the wordmark. (`TOKENS`, `DEC`)
- [ ] Light theme only, no `prefers-color-scheme` block. (`TOKENS`)
- [ ] Status is a word plus a colour everywhere; nothing signalled by colour
      alone. (`WCAG-141`)
- [ ] Copy passes the plain-words bar: no engineering vocabulary (integrator
      pages excepted), no em dashes in prose, no question-phrased headings.
      (`DEC`)
- [ ] All six states named for every dynamic surface, or a stated reason a state
      is unreachable. (`B2`)
- [ ] Empty state distinguishes first-run from filtered-to-zero, and its action
      label echoes its title. (`STRIPE-EMPTY`)
- [ ] Loading is a skeleton, not a spinner, for sub-second content. (`PRODUCT`)
- [ ] Errors say what happened and what to do; no error is a dead end.
      (`NN-H9`)
- [ ] Destructive actions confirm; non-destructive ones offer undo instead of a
      confirm. (`NN-CD`)
- [ ] Every interactive element shows a 2px `accent-ink` focus ring at 2px
      offset, and no sticky bar covers a focused element. (`TOKENS`,
      `WCAG-247`, `WCAG-2411`)
- [ ] Every hover-revealed control is reachable by keyboard focus. (`WCAG-211`)
- [ ] Hit targets at least 36px, nothing under 24x24. (`PRODUCT`, `WCAG-258`)
- [ ] Tables: numbers right-aligned and tabular, text left-aligned, headers
      matching, first column human-readable, header frozen if it scrolls.
      (`EU-TBL`, `MDN-FVN`, `NN-DT`)
- [ ] Row click navigates and never mutates; three or more row actions live in
      an overflow menu. (`POLARIS-IT`, `NN-DT`)
- [ ] Selection shows a count, offers select-across-pages explicitly, and
      `Escape` clears it. (`POLARIS-IT`, `LIN-SEL`)
- [ ] Filters: at most two inline, rest behind one badged control, active ones
      visible and removable, options sourced from code. (`NN-PD`, `DEC`)
- [ ] Panels and dialogs: focus in on open, `Escape` closes, focus returns, the
      page's own header and tabs stay on screen. (`APG-DLG`, `TOKENS`)
- [ ] Shortcuts match the §2 table, or the deviation is a written fork with a
      reason. (`NN-H4`)
- [ ] Nothing introduces a second way to do a job that already has one.
      (`CLAUDE.md`, `NN-H4`)
- [ ] `ANNOTATIONS.md` separates BINDING from DEMO and lists every fork with a
      recommendation. (`DEC`)

## §4 Sources

- `NN-H1..H10` Nielsen, *10 Usability Heuristics*, nngroup.com/articles/ten-usability-heuristics/ (1994, upd. 2024-01-30)
- `NN-RT` *Response Times: The 3 Important Limits* · `NN-PD` *Progressive Disclosure* (2006) · `NN-CD` *Confirmation Dialogs Can Prevent User Errors* (2018) · `NN-DM` *Direct Manipulation* (2016, rev. 2024) · `NN-RR` *Recognition vs. Recall* (2024) · `NN-FL` *Fitts's Law* (2022) · `NN-DT` Laubheimer, *Data Tables: Four Major User Tasks* (2022-04-03) · all nngroup.com/articles/
- `SHN-n` Shneiderman, *Eight Golden Rules of Interface Design*, cs.umd.edu/users/ben/goldenrules.html (from *Designing the User Interface* 6e, 2016)
- `IXDF-HICK` interaction-design.org/literature/topics/hick-s-law
- `WCAG-211/243/247/2411/258/141` W3C, *Understanding WCAG 2.2*, w3.org/WAI/WCAG22/Understanding/ (keyboard · focus-order · focus-visible · focus-not-obscured-minimum · target-size-minimum · use-of-color)
- `APG-GRID` / `APG-DLG` W3C WAI-ARIA APG, grid and modal-dialog patterns, w3.org/WAI/ARIA/apg/patterns/
- `LIN-SEL` linear.app/docs/select-issues · `LIN-CMD` linear.app/docs/assigning-issues · `LIN-KS` linear.app/changelog/2021-03-25-keyboard-shortcuts-help · `LIN-M` linear.app/method/introduction
- `FIG-ZOOM` *Adjust your zoom and view options* · `FIG-KB` *Use Figma products with a keyboard* · `FIG-SEL` *Select layers and objects* · `FIG-QA` *Use quick actions* · `FIG-REN` *Rename layers* · all help.figma.com. `FIG-MINI` verified absence: Figma ships no native minimap
- `NOTION-W` notion.com/help/writing-and-editing-basics · `NOTION-SL` notion.com/help/guides/using-slash-commands
- `STRIPE-D` docs.stripe.com/stripe-apps/design · `STRIPE-EMPTY` /patterns/empty-state · `STRIPE-BADGE`, `STRIPE-BANNER` /components/
- `CARBON-DT` v10.carbondesignsystem.com/components/data-table/usage/ · `POLARIS-IT` polaris-react.shopify.com/components/tables/index-table
- `MDN-FVN` MDN *font-variant-numeric* · `BUTTERICK` practicaltypography.com/alternate-figures.html · `EU-TBL` data.europa.eu data-visualisation-guide, *Table design: scannability*
- `GMAIL-KB` support.google.com/mail/answer/6594 · `FRONT-KB` help.front.com/en/articles/2189 · `FRONT-SNOOZE` Front help: shared-inbox snooze applies to all teammates
- `ZD-NOTE` third-party teardowns of Zendesk's yellow internal-note convention; precedent only, not vendor-primary
- `MD-WIKI` *Master-detail interface*, Wikipedia; secondary. No NN/g article on three-pane inbox layout was found
- `B2` `C1` `D1` harness design playbooks (interaction design · visual composition · design system)
- `TOKENS` `DEC` `PRODUCT` `CLAUDE.md` this repo's own law · `CODE` `apps/web/src/components/flows/useCanvasShortcuts.ts`

**Citation weaknesses, stated rather than hidden.** Linear publishes no crawlable
shortcut page, so `S`/`P`/`C` and the `G`-then-letter chord are widely reported
but not first-party verified, and are therefore absent from the §2 table.
Linear's often-quoted millisecond latency budgets could not be confirmed from a
Linear page, so §1.1 cites Nielsen's limits instead. Material Design's alignment
rule could not be fetched (JS-rendered), so the EU guide carries that
convention. Intercom does not document its note-versus-reply visual convention,
so ours derives from our own tokens. Shneiderman's 1983 three-property
definition of direct manipulation is cited through NN/g, not the original IEEE
paper.
