Перевести KPI и агентские тайлы на IconTile, skip-link и live regions. Co-authored-by: Cursor <cursoragent@cursor.com>
64 lines
1.5 KiB
TypeScript
64 lines
1.5 KiB
TypeScript
import { CircleAlertIcon } from 'lucide-react'
|
|
import {
|
|
Alert,
|
|
AlertDescription,
|
|
AlertTitle,
|
|
} from '@/components/reui/alert'
|
|
import { Button } from '@evofw/ui/components/button'
|
|
import { Skeleton } from '@evofw/ui/components/skeleton'
|
|
|
|
interface QueryStateProps {
|
|
isLoading?: boolean
|
|
isError?: boolean
|
|
error?: Error | null
|
|
onRetry?: () => void
|
|
skeleton?: React.ReactNode
|
|
children: React.ReactNode
|
|
}
|
|
|
|
function DefaultSkeleton() {
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<Skeleton className="h-8 w-1/3" />
|
|
<Skeleton className="h-40 w-full" />
|
|
<Skeleton className="h-40 w-full" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function QueryState({
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
onRetry,
|
|
skeleton,
|
|
children,
|
|
}: QueryStateProps) {
|
|
if (isLoading) {
|
|
return (
|
|
<div role="status" aria-live="polite" aria-busy="true">
|
|
{skeleton ?? <DefaultSkeleton />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<Alert variant="destructive">
|
|
<CircleAlertIcon />
|
|
<AlertTitle>Не удалось загрузить данные</AlertTitle>
|
|
<AlertDescription className="flex flex-col gap-2">
|
|
<span>{error?.message ?? 'Произошла ошибка при загрузке.'}</span>
|
|
{onRetry ? (
|
|
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
Повторить
|
|
</Button>
|
|
) : null}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
|
|
return children
|
|
}
|