/**
* The three states every screen and panel has besides "here is the data": still loading, nothing there,
* and failed. One look for each, so no screen invents its own — and none forgets one.
*
* An error shows the API's own reason (a refusal says why) and, when the caller can, a way to try again.
*/
import { Box, Button, Callout, Flex, Heading, Spinner, Text } from "@radix-ui/themes";
import type { ReactNode } from "react";
export function LoadingState({ label = "Loading…" }: { label?: string }) {
return (
<Flex align="center" gap="2" py="4" role="status" aria-live="polite">
<Spinner />
<Text color="gray">{label}</Text>
</Flex>
);
}
export interface EmptyStateProps {
title: string;
description?: ReactNode;
/** The next step, e.g. "Import a list" — shown only when the caller can take it. */
action?: ReactNode;
}
export function EmptyState({ title, description, action }: EmptyStateProps) {
return (
<Box py="5" px="4" style={{ textAlign: "center", border: "1px dashed var(--gray-a6)", borderRadius: "var(--radius-3)" }}>
<Heading as="h3" size="3" mb="1">{title}</Heading>
{description && <Text as="p" size="2" color="gray" mb={action ? "3" : "0"}>{description}</Text>}
{action && <Flex justify="center">{action}</Flex>}
</Box>
);
}
export interface ErrorStateProps {
/** The reason, in the API's words. */
error: string;
title?: string;
onRetry?: () => void;
}
export function ErrorState({ error, title = "That did not work", onRetry }: ErrorStateProps) {
return (
<Callout.Root color="red" role="alert">
<Callout.Text>
<Text weight="bold">{title}. </Text>
{error}
</Callout.Text>
{onRetry && (
<Box>
<Button size="1" variant="soft" color="red" onClick={onRetry}>Try again</Button>
</Box>
)}
</Callout.Root>
);
}
/** Not found — the same answer the API gives for a row the caller may not see (a 404, never a 403). */
export function NotFoundState({ what = "It", back }: { what?: string; back?: ReactNode }) {
return (
<Box py="6">
<Heading as="h1" size="6" mb="2">Not found</Heading>
<Text as="p" color="gray" mb="3">{what} does not exist, or you cannot see it.</Text>
{back}
</Box>
);
}