# The state matrix — every screen × its states, dialogs & URLs

The one page to review the whole surface from. Each row is a state the codegen
build must produce; each has a **URL that forces it** (open in a browser, or feed
it to the screenshot gate) and is checked by [`verify.mjs`](verify.mjs) — which
enumerates this list from the live DOM, so it can't silently drift from the
gallery.

- **Open a state:** `gallery.html?app=1#<screen>` or `…#<screen>/<state>`, plus
  `&theme=dark|light` and `&w=1024|1440`. A dialog: `…#dialog/<id>`.
- **Check everything:** `node verify.mjs` (hard-fails on JS errors, dead nav,
  unreachable states, dialogs that won't open/close). `--shots` writes a PNG per
  state to `shots/`; `--update` re-baselines the DOM-diff signal; `--theme dark`
  sweeps dark.
- **Add a state:** add a `force()` to the `STATES` registry in `gallery.html`
  (search `STATES={`), pointing at the existing switch — never a second copy —
  then re-run `verify.mjs --update`.

Counts today: **21 screens · 66 named states · 34 dialogs.** Run `verify.mjs` for
the live total (it enumerates the registry from the DOM).

## Screens with named alternate states

| Screen | Default | Named states (`#screen/state`) |
|---|---|---|
| **workspace** | `#workspace` | `loading` · `offline` · `thread` · `pins` · `files` · `voicebar` · `slowmode` · `timedout` |
| **explorer** | `#explorer` | `loading` · `grid` · `list` · `feedview` · `trash` · `empty` · `starred` · `readonly` · `locked` · `detailsloading` |
| **settings** | `#settings` | `general` · `channels` · `members` · `roles` · `invites` · `moderation` · `audit` · `storage` |
| **notifications** | `#notifications` | `loading` · `all` · `mentions` · `threads` · `saved` · `empty` |
| **feed** | `#feed` | `loading` · `empty` |
| **search** | `#search` | `loading` · `empty` (no-results) |
| **dms** | `#dms` | `loading` · `convo` · `friends` · `empty` |
| **friends** | `#friends` | `loading` · `all` · `pending` · `blocked` |
| **profile** | `#profile` | `loading` · `public` (stranger POV) · `mutual` (friend POV) · `empty` |
| **create** | `#create` | `error` (name taken) *(2-step wizard: identity → channels)* |
| **auth** | `#auth` | `signin` · `sent` · `claim` *(the sign-in → magic-link → claim-handle order)* |
| **usersettings** | `#usersettings` | `profile` · `account` · `notifs` · `appearance` · `privacy` · `storage` |
| **shared** | `#shared` | `expired` (dead link) |
| **deadinvite** | `#deadinvite` | `expired` · `revoked` · `full` · `already` (member) |

## Loading coverage — every async surface has a skeleton

Per CODEGEN #49, each surface that fetches data has a forced, screenshottable
loading state. Skeletons are built from the shared `.skelcard` / `.skelmsg` /
`.skelrow` shapes (shimmer over `--surface`), never a spinner.

| Async surface | Loading URL |
|---|---|
| Feed grid | `#feed/loading` |
| File-explorer folder grid | `#explorer/loading` |
| Chat message **stream** + **members rail** | `#workspace/loading` |
| **Details pane** (media + metadata) | `#explorer/detailsloading` |
| DM conversation stream | `#dms/loading` |
| Notifications list | `#notifications/loading` |
| Search results | `#search/loading` |
| Profile works grid | `#profile/loading` |
| Friends list | `#friends/loading` |

## Screens at default only (one composed state)

`usersettings` · `shared` · `newserver` · `join` · `vc` · `e404` · `deadinvite` ·
`denied` · `blocked` · `pending`. Each is reachable at `#<screen>`; the system
ones (`e404`, `deadinvite`, `denied`, `blocked`, `pending`) are themselves the
alternate/error states of the flows that reach them, and `vc` is the voice-call
WIP placeholder.

## Dialogs (all reachable at `#dialog/<id>`, all in §⑥ of the catalog)

Menus: `dlMenu` `saveMenu` `emojiMenu` `msgMenu` `cardMenu` `serverMenu`
`notifMenu` `channelMenu` `dmMenu` `folderMenu` `exViewMenu`.
Modals: `channelModal` `inviteModal` `fwdModal` `newDmModal` `reportModal`
`editChanModal` `catModal` `gdmModal` `statusModal` `helpModal` `epModal`
`moveModal` `timeoutModal` `banModal` `arModal` `leaveModal` `delSrvModal`
`srvNotifModal` `shareModal` `storageModal` `uModal`.

## Coverage — remaining nice-to-haves

The Phase-D backlog is closed (batches A–C, 2026-08-22), and the optional
nice-to-haves that remained are now **built too** (2026-08-22): notifications
`empty`, `usersettings` panel states, `shared/expired`, and the four
`deadinvite` copy variants (expired/revoked/full/already-member) — all in the
registry and picked up by `verify.mjs`.

Add any future state the same way — a `force()` in the registry pointing at a
real node — and `verify.mjs` picks it up automatically.
