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>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { sheetDismiss } from "../src/lib/ui/sheet-dismiss";
|
||||
|
||||
// Only the tap-outside path is unit-tested; the swipe-down drag is
|
||||
// pointer-gesture behaviour covered by manual / e2e checks.
|
||||
describe("sheetDismiss — tap outside", () => {
|
||||
let cleanup: (() => void) | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
cleanup?.();
|
||||
cleanup = null;
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("dismisses on a pointer-down outside the sheet", () => {
|
||||
const node = document.createElement("div");
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(node, outside);
|
||||
const onDismiss = vi.fn();
|
||||
const action = sheetDismiss(node, { onDismiss });
|
||||
cleanup = action.destroy;
|
||||
|
||||
outside.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||
expect(onDismiss).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("ignores a pointer-down inside the sheet", () => {
|
||||
const node = document.createElement("div");
|
||||
const inner = document.createElement("button");
|
||||
node.append(inner);
|
||||
document.body.append(node);
|
||||
const onDismiss = vi.fn();
|
||||
const action = sheetDismiss(node, { onDismiss });
|
||||
cleanup = action.destroy;
|
||||
|
||||
inner.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||
expect(onDismiss).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops listening after destroy", () => {
|
||||
const node = document.createElement("div");
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(node, outside);
|
||||
const onDismiss = vi.fn();
|
||||
sheetDismiss(node, { onDismiss }).destroy();
|
||||
|
||||
outside.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||
expect(onDismiss).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user