refactor(e10): move the platform components into components/ui/ and enforce the boundary
The app HAS a platform layer -- AnchoredPopover (22 importers, with arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17), PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among ~160 feature components with nothing marking it as one. Several of those files describe themselves as 'the one place' or 'canonical' and were still bypassed by hand-rolled markup, because a layer nobody can see is a layer nobody uses; three separate inventory passes of this codebase misread it the same way. Moves the 33 domain-free modules into components/ui/, rewrites the imports in 89 files, and makes the boundary a lint rule rather than a convention: inside ui/ a one-level-up import (a feature component) and any lib/api slice are errors. The rule was verified to fire on both and to leave generic lib/ helpers alone. README.md states which side of the line a component belongs on and why. No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests all green; no backend file touched.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
|
||||
// Centered loading state: a spinning accent ring + an optional label. The single shared loading
|
||||
// visual for a page/section's first load, so all of them read the same (and can be swapped for
|
||||
// skeletons later in one place). `role=status`/`aria-live` announces it to screen readers.
|
||||
export function LoadingState({ label, className = "" }: { label?: ReactNode; 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"
|
||||
>
|
||||
<Loader2 className="w-6 h-6 animate-spin text-accent" aria-hidden />
|
||||
<span>{label ?? t("common.loading")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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}`}
|
||||
// An error is announced promptly (assertive); a plain loading line is polite.
|
||||
role={tone === "error" ? "alert" : "status"}
|
||||
aria-live={tone === "error" ? "assertive" : "polite"}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 text-center">
|
||||
{tone === "error" && <AlertTriangle className="w-4 h-4 shrink-0 text-danger" />}
|
||||
{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 && <no data>` 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". The `&& <no data>` guard matters: a
|
||||
// transient refetch error while data is already on screen keeps the data rather than blanking it to
|
||||
// an error. `StateMessage` is the shared building block pages wire into their loading/empty ternary.
|
||||
Reference in New Issue
Block a user