# 09 · UX principles

> Seven rules that decide arguments. Where a design question has two defensible
> answers, these are how Since Labs picks.

---

## 9.1 Answer one question per surface

A page that answers two questions answers neither. The product's own IA is built
on this: **Home** answers *what happened*, **Apps** answers *where do I go*.
Merging them works at three apps and falls apart at fifty.

Before building a page, write the question it answers in one sentence. If you
need "and", you need two pages.

---

## 9.2 Fixed chrome, growing content

The platform sidebar is **four items, forever**: Home, Apps, Team,
Organization. Apps are reached through the ⌘K launcher and the catalogue — they
never join the nav.

This is the single most important structural decision in the product. Any
navigation that grows with the number of things it lists has a ceiling, and you
hit it in production, in front of a customer, with no cheap fix. **Chrome is
fixed. Content grows.**

The same rule governs the operator console: per-app back-office surfaces hang off
`/admin/apps` from each app's manifest, so the operator nav does not grow either.

---

## 9.3 Search beats browse past about fifteen

A dropdown of everything works up to roughly fifteen items and then quietly
stops. Typing is O(1) in the number of items.

So: the app switcher is **search-first**, on ⌘K, from anywhere. Anything expected
to exceed fifteen entries gets a search field before it gets a filter menu.

---

## 9.4 Progressive disclosure, one branch at a time

Sub-navigation reveals only while its parent branch is current. The sidebar never
shows two app sections expanded at once.

Generally: show the next decision, not every possible decision. Detail lives one
click in, not in an accordion stack on the index.

---

## 9.5 Destructive actions are confirmed, reversible, and logged

Three requirements, all of them:

1. **Confirmed** in a real `<dialog>` with `showModal()` — never a `<details>` popover. A popover inside a card with `overflow-hidden` gets clipped, and the confirm button becomes unreachable on the last row of a table. This has actually happened here.
2. **Named.** The confirmation says what is about to happen, to what: "Delete Maanrakennus Virtanen Oy and all of its data?" — not "Are you sure?"
3. **Logged.** Anything destructive writes to the audit log with an actor and a reason.

The confirm button carries the `destructive` variant and repeats the verb:
`Delete organization`, not `Confirm`.

---

## 9.6 State is never a mystery

Every surface has a designed state for all five:

| State | Requirement |
|---|---|
| **Loading** | A skeleton over a spinner. Nothing at all under ~300ms |
| **Empty** | Says what is not here and what to do. Never a shrug |
| **Partial** | Says what is missing — "Environmental only", "Partial data" — not a blank cell |
| **Error** | Says what happened and what to do. Degrades the region, never the page |
| **Success** | Visible confirmation. A silent save is a bug report |

A page that reads "0" and a page that failed to load must never look the same.

---

## 9.7 Nothing is communicated by colour alone

A status chip carries a dot **and** a word. A chart series is labelled directly.
A required field is marked in text, not by a red border.

This is WCAG 1.4.1, and it is also just correct for an audience reading a phone
in daylight on a job site.

---

## 9.8 Keyboard is a first-class input

- ⌘K / Ctrl-K opens the switcher from anywhere.
- `↑` `↓` move, `↵` opens, `Esc` closes — in every list, dialog and palette.
- Tab order follows visual order. Focus moves into a dialog on open and returns to the trigger on close.
- The focus ring is **never** removed without an equivalent replacement.
- Every interactive element is reachable without a mouse. If it is a `<div>` with an `onClick`, it is a bug.

---

## 9.9 Operator and customer are different products

The same codebase serves two audiences with different needs, and they get
different vocabularies:

| | Workspace (customer) | Operator console (staff) |
|---|---|---|
| Density | Comfortable | Dense |
| Identifiers | Hidden | Visible, in mono |
| Language | Plain — "Your apps" | Precise — "Entitlements" |
| Errors | What to do next | What happened, with the ID |
| Role shown | Membership role | Internal rank |

Never leak operator vocabulary into the workspace. A customer should never see
the word "entitlement", a UUID, or an internal role name.

---

## 9.10 The five-second test

Export any screen or asset. Look at it for five seconds. Close it. Write down
what you remember.

If you cannot recall the main message, the hierarchy is wrong — start again. This
applies to a dashboard as much as to a LinkedIn cover.
