Files
galaxy-game/ui/docs/error-state-ux.md
T
Ilia Denisov 8dcaf1c6c6
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Failing after 7m13s
feat(ui): error & state UX — error surface, view states, map selection, sheet gestures (F4)
- lib/error/: classify any caught error into a stable ErrorKind from the
  transport signal (HTTP status / Connect Code / fetch TypeError /
  navigator.onLine); map to translated error.* messages via reportError
  (sticky Retry toast for retryable kinds) or errorMessageKey (inline).
  Mail compose now surfaces the translated 403/error inline.
- lib/ui/view-state.svelte: shared loading/empty/error placeholder with
  the right live-region role + optional action; entity tables
  (races/sciences/ship-classes) migrated, rest adopt incrementally.
- map/selection-ring.ts: accent ring around the selected planet, fed into
  the map buildExtras alongside the reach circles.
- lib/ui/sheet-dismiss.ts: tap-outside + drag-handle swipe-down dismissal
  for the planet/ship-group bottom-sheets (hand-rolled pointer events).

Tests: error, view-state, selection-ring, sheet-dismiss (761 total).
Docs: ui/docs/error-state-ux.md (+ index); F4 marked done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:29:11 +02:00

2.4 KiB

Error & state UX

How the client gives consistent, actionable feedback for failures and for loading / empty / error states.

Error surface (src/lib/error/)

Server result_codes are downstream-opaque (only "ok" is contractual — docs/ARCHITECTURE.md), so the client keys UX off the reliable transport-level signal instead.

  • classify.ts collapses any caught value into a stable ErrorKind (offline, network, auth, forbidden, conflict, notFound, rateLimit, server, unknown) from navigator.onLine, an AuthError HTTP status, a Connect Code, or a fetch TypeError. isRetryable marks the transient kinds.
  • report.ts maps each kind to a translated error.* message and either:
    • reportError(err, { onRetry? }) — raises a toast (a sticky toast with a Retry action for a retryable kind that supplies onRetry, otherwise auto-dismiss); or
    • errorMessageKey(err) — returns the translated key for a view to render the message inline (e.g. the mail compose 403).

Messages live under error.* (en + ru); the action label is common.retry. New call sites adopt this surface incrementally; the mail compose dialog is the worked inline example.

View states (src/lib/ui/view-state.svelte)

<ViewState kind="loading|empty|error" message=… [actionLabel onAction] /> is the one placeholder for a view or panel: a spinner for loading, an accent action button when given, and the right live-region role (status for loading/empty, alert for error). The entity tables (races / sciences / ship-classes) use it; remaining views adopt it incrementally.

Selected-planet marker (src/map/selection-ring.ts)

When the SelectionStore holds a planet, the map draws one accent ring tight around it (computeSelectionRing, fed into the map's buildExtras alongside the reach circles). Ship-group selection is not ringed — groups are addressed by report index and have no single stable map coordinate.

Mobile bottom-sheet dismissal (src/lib/ui/sheet-dismiss.ts)

use:sheetDismiss={{ onDismiss }} adds two hand-rolled (no-dependency) gestures to the planet / ship-group bottom-sheets: tap anywhere outside the sheet, or drag its [data-sheet-handle] grabber down past a threshold. The swipe starts only from the grabber so it never fights the sheet's own content scrolling.