forked from unom/punktfunk
Installing a plugin left the sidebar unchanged until a reload. The reason is timing, not caching: the host restarts the scripting runner AFTER the job reports done, and the plugin only registers its UI once that comes back — several seconds later, by which point the one-shot invalidation had already run and found the old list. The nav then waited out the 30 s idle poll, which in practice meant "until I reloaded". Anything that changes the installed set now switches the directory to a 2 s poll for a minute, so the entry lands about a second after the plugin actually comes up. Measured end to end in a browser: 29 s → 7 s, with the plugin registering at 6 s. The plugin entries also never animated. They are rendered outside the `motion.nav` that carries the variants and the stagger, so they inherited neither and simply appeared — most visibly in exactly the case above, where one shows up in a nav that is already on screen. They get their own animation container now, matching the main nav. (A motion-wrapped div around the link, not `motion(Link)`, which erases TanStack's typed `params`.) The accessibility pass on the display form, where the console's densest controls live: - The Custom block's numeric inputs had a `<label>` with no `htmlFor` next to an `<input>` with no `id`, which labels nothing at all — a screen reader announced them as unnamed spin buttons. Single controls are paired properly now; the button groups became real `<fieldset>`/`<legend>`, which is what they are. - Every option group signalled its active choice with fill colour alone. They carry `aria-pressed` now, so the state is available to assistive tech and not only to people who can compare two button variants. - `QueryState`'s error branch is a live region, so a query that fails announces the failure instead of silently swapping one region for another. - Motion honours `prefers-reduced-motion` instead of overriding it. - `<html lang>` follows the locale instead of claiming "en" while the app renders German. Verified: switching to de flips the attribute. - "Close menu", "Language" and "Loading" went through the message catalogue. Also: ten dead message keys removed (a whole removed Clients page and the old Settings token field), and the README no longer tells operators to set the management token under "Settings → API token" — that field is gone and the token has been server-side only for some time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 lines
1.5 KiB
TypeScript
60 lines
1.5 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { ApiError } from "@/api/fetcher";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { m } from "@/paraglide/messages";
|
|
|
|
interface QueryStateProps {
|
|
isLoading: boolean;
|
|
error: unknown;
|
|
refetch?: () => void;
|
|
children: ReactNode;
|
|
}
|
|
|
|
/** Uniform loading/error wrapper for a query-backed view. */
|
|
export function QueryState({
|
|
isLoading,
|
|
error,
|
|
refetch,
|
|
children,
|
|
}: QueryStateProps) {
|
|
if (isLoading) {
|
|
return (
|
|
<div
|
|
role="status"
|
|
className="flex min-h-40 flex-col items-center justify-center gap-3 text-sm text-muted-foreground"
|
|
>
|
|
<Spinner className="size-8" />
|
|
{m.common_loading()}
|
|
</div>
|
|
);
|
|
}
|
|
if (error) {
|
|
const unauthorized = error instanceof ApiError && error.status === 401;
|
|
return (
|
|
// `role="alert"` so the failure is announced. The loading branch above already has
|
|
// role="status"; without this, a query that resolved into an error swapped one silent
|
|
// region for another and a screen-reader user was told only that loading had stopped.
|
|
<div
|
|
role="alert"
|
|
className="rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm"
|
|
>
|
|
<p className="font-medium text-destructive">
|
|
{unauthorized ? m.common_unauthorized() : m.common_error()}
|
|
</p>
|
|
{refetch && !unauthorized && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="mt-3"
|
|
onClick={() => refetch()}
|
|
>
|
|
{m.common_retry()}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
return <>{children}</>;
|
|
}
|