- 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>
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.tscollapses any caught value into a stableErrorKind(offline,network,auth,forbidden,conflict,notFound,rateLimit,server,unknown) fromnavigator.onLine, anAuthErrorHTTP status, a ConnectCode, or a fetchTypeError.isRetryablemarks the transient kinds.report.tsmaps each kind to a translatederror.*message and either:reportError(err, { onRetry? })— raises a toast (a sticky toast with a Retry action for a retryable kind that suppliesonRetry, otherwise auto-dismiss); orerrorMessageKey(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.