{"topic":"states","format":"markdown","content":"# States\n\nGoal: the interface has an answer for every moment — loading, empty, error,\ndisabled, success — not just the happy path with data. Users judge quality in\nexactly these in-between moments.\n\nMethod: for every view, list its states (initial load, loaded, empty,\npartial, error, slow, and for mutations: submitting, succeeded, failed).\nAnything unhandled is a defect.\n\n## 1. Loading\n\n- **Under ~300ms, show nothing.** An indicator that flashes for 100ms reads\n  as jank. Delay indicators: a 300ms delay on the indicator with opacity\n  starting at 0, or equivalent logic in JS. Once it shows, keep it at least\n  400ms, so it doesn't flicker off either. The work itself starts at once:\n  only the showing waits. \"Saving…\" labels follow the same timing.\n- **Avoid spinners.** Prefer a shimmer on the label that names the work (the\n  text dims and a light band passes across it, a moving `mask-image`\n  gradient). A spinner is a last resort for\n  a tiny indeterminate wait with no label at all. A small icon that has to\n  say \"working\" breathes in place. The one turning glyph allowed is a\n  12–16px arc in the icon slot of a step that's running, in a list of steps\n  whose words name the work; it holds still when paused and breathes with\n  reduced motion.\n- **Skeletons only for a known layout on page open.** Grey bars advertise\n  slowness before anything is slow, so they wait behind the same 300ms delay\n  as any busy state, and only where the loaded layout is known: one element\n  per bone, sized to the line it stands for, and one viewport-fixed sheen\n  shared by every bone, never a shimmer per element. Anywhere else, hold the\n  space the result will take (a fixed slot or `min-height` matching the\n  loaded layout) and put one shimmering line in it that names the work\n  (\"Loading messages…\"), so nothing shifts when it lands and the wait reads\n  as progress. Reduced motion: still.\n- **Buttons that submit**: keep the label and its width; shimmer the label\n  while pending instead of swapping it for a spinner. Ignore presses while\n  pending (busy, not disabled, so focus stays). Re-enable on failure.\n- Images: always reserve space (width/height or `aspect-ratio`) so nothing\n  reflows on load. Optional nicety: fade the image in over `--ui-dur-fast` once loaded\n  against the muted surface color.\n\n## 2. Empty\n\nAn empty state is an onboarding surface, not an apology. Structure:\n\n1. One-line statement of what would be here (\"No projects yet\").\n2. One line of value or guidance (\"Projects group your deployments and\n   environments\").\n3. The primary action to fix it (a real button: \"Create project\") — when an\n   action exists.\n\nKeep it visually quiet: one illustration or none, centered in the space the\ncontent would occupy, `text-balance` on the copy. Never an icon sitting in a\nrounded square or circle; that reads as an app icon, not as empty space.\nIllustrations are clean isometric line drawings on a 2:1 grid (slopes stay\ncrisp) that clearly show their subject, one stroke weight across the set\n(the icon set's medium) with round caps and mitered joins. Corners are\ncrisp: at most about a pixel of easing, never rounded off; only what is\nround in life is round (a lens, a cord, a binder ring, a slot). Parts that\ntouch in life touch exactly: a hinge is a shared edge, a handle starts on\nits rim, a stacked card lies on the one below. Text on a drawn card is\nfilled bars, not lines. Faces are opaque, in two tones at most: the surface and\none soft fill of the text color mixed into it, all from tokens so they read\nin light and dark. Few lines, and large enough to be the focal point: about\n200px in a panel, 260px on a page. Never leave a\nbare \"No data\" / \"0 results\" string, and never render an empty table header\nrow with nothing under it.\n\nSearch/filter empties are different from true empties: say what was searched\n(\"No results for 'foo'\") and offer a reset (\"Clear filters\").\n\n## 3. Error\n\n- Field-level validation errors: adjacent to the field (4–6px below), red\n  text at readable size (not 10px), the field border shifts to the error\n  color, and the message says how to fix it — \"Password needs 8+ characters\",\n  not \"Invalid input\". Set `aria-invalid` and link the message with\n  `aria-describedby`.\n- Validate on blur or submit, not on every keystroke while the user is still\n  typing their first attempt. Clear the error as soon as the fix is made.\n- Mutation failures: keep the user's input, say what happened and what to do,\n  offer retry. Never silently swallow a failed action — a button that does\n  nothing on failure is the worst state in this file.\n- Full-view errors get the empty-state structure: what broke, one line of\n  context, a retry action.\n\n## 4. Disabled\n\n- Disabled is a last resort; prefer enabled-with-validation-message so the\n  user learns *why*. When genuinely disabled: reduced opacity (~50%) +\n  `cursor: default` on the wrapper (disabled elements don't fire cursor),\n  and never remove the element from tab order silently if a tooltip explains\n  the why.\n- Don't animate disabled elements' hover states — no hover response is part\n  of the affordance.\n\n## 5. Success & feedback\n\n- Every action gets an acknowledgment within 100ms — even if it's just the\n  pressed state. For mutations: optimistic UI where safe (toggle, like,\n  rename), pending state where not (payment, delete).\n- Toasts: enter with a small rise + fade (~200ms), auto-dismiss after\n  4–6 seconds (longer if there's an action in the toast), dismissible, stack\n  from one consistent corner. Success toasts are short (\"Saved\"); don't\n  toast things the user can already see changed on screen.\n- Inline confirmation beats a toast when the change is visible in place — a\n  brief highlight (background tint fading out over ~1s) on the row that just\n  changed is often better feedback than a corner notification.\n\n## 6. Destructive actions\n\n- Confirm before irreversible deletes — but make the dialog specific: name\n  the thing being deleted, and make the confirming button say the verb\n  (\"Delete project\", not \"OK\"). The destructive button gets the danger\n  treatment; the safe action is the default focus.\n- After delete, animate the removal (~200ms fade/collapse) so the list\n  visibly changes rather than snapping.\n\n## 7. Focus & keyboard (verify here even though foundations set it up)\n\n- Every interactive element shows a visible `:focus-visible` state.\n- Dialogs trap focus, return it to the trigger on close, close on Escape.\n- No positive `tabindex` anywhere; tab order follows visual order.\n"}