feat(ui): R3 S1 — honest error states across the pages

The error state didn't exist as a concept: a failed main query rendered as an "empty" list
("No channels" while the request actually died), so a backend restart read as "everything is
empty". Add the convention that an empty state may NEVER show while a query is erroring.

- New components/QueryState.tsx: shared StateMessage (loading / error+Retry, house style, with
  role=status/aria-live — the app had zero live regions).
- DataTable gains loading/error/onRetry props (suppresses empty-text on error).
- isError branch (before the empty branch) added across the false-empty pages: Feed (retry),
  Channels, ChannelDiscovery, AuditLog, NotificationsPanel, Messages (conversations + people),
  Scheduler, Stats, PlexBrowse grid, PlexPlaylistView. ChannelPage's main content is Feed, now
  covered. common.loadError/empty strings (en+hu).
This commit is contained in:
2026-07-22 03:58:51 +02:00
parent 2427de3bbc
commit d9a08892a5
14 changed files with 138 additions and 13 deletions
+48
View File
@@ -0,0 +1,48 @@
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { AlertTriangle } from "lucide-react";
// Centered inline status line for a page/section's loading and error(+retry) states, in the house
// style (mirrors Playlists' error block). Text-only by design — R3's goal is to stop pages
// rendering an error or a still-loading fetch AS an "empty" state, not to add skeletons. The
// `role=status`/`aria-live` also gives screen readers the state change (the app had zero live
// regions before).
export function StateMessage({
children,
onRetry,
tone = "muted",
className = "",
}: {
children: ReactNode;
onRetry?: () => void;
tone?: "muted" | "error";
className?: string;
}) {
const { t } = useTranslation();
return (
<div
className={`flex flex-col items-center gap-3 py-10 text-sm text-muted ${className}`}
role="status"
aria-live="polite"
>
<span className="inline-flex items-center gap-2 text-center">
{tone === "error" && <AlertTriangle className="w-4 h-4 shrink-0 text-red-400" />}
{children}
</span>
{onRetry && (
<button
type="button"
onClick={onRetry}
className="px-3 py-1.5 rounded-lg border border-border text-xs text-muted hover:text-fg hover:border-accent transition"
>
{t("common.retry")}
</button>
)}
</div>
);
}
// The honest-state convention every page now follows: check `isError` BEFORE the empty branch so a
// failed fetch renders this message (with Retry) instead of the page's "empty" text — the R3 bug
// where a backend restart read as "No channels". `StateMessage` is the shared building block;
// pages wire it into their existing loading/empty ternary (and DataTable takes loading/error props).