add skeleton
This commit is contained in:
@@ -7,6 +7,7 @@ import { qk } from '@/shared/api/query-keys'
|
|||||||
import { cn } from '@/shared/lib/cn'
|
import { cn } from '@/shared/lib/cn'
|
||||||
import { Button } from '@/shared/ui/button'
|
import { Button } from '@/shared/ui/button'
|
||||||
import { Card, CardContent } from '@/shared/ui/card'
|
import { Card, CardContent } from '@/shared/ui/card'
|
||||||
|
import { StoragePanelSkeleton } from './StoragePanelSkeleton'
|
||||||
import { getStorageStats } from './api'
|
import { getStorageStats } from './api'
|
||||||
import { formatBytes, percentOf } from './format'
|
import { formatBytes, percentOf } from './format'
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export function StoragePanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading || !data) return <p className="text-muted-foreground">{t('common.loading')}</p>
|
if (isLoading || !data) return <StoragePanelSkeleton />
|
||||||
|
|
||||||
const { volumeTotalBytes: total, volumeFreeBytes: free, storageBytes: storage } = data
|
const { volumeTotalBytes: total, volumeFreeBytes: free, storageBytes: storage } = data
|
||||||
const volumeKnown = total > 0
|
const volumeKnown = total > 0
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Card, CardContent } from '@/shared/ui/card'
|
||||||
|
import { Skeleton } from '@/shared/ui/skeleton'
|
||||||
|
|
||||||
|
/** Строк в составе хранилища заранее не знаем — берём типичное число, чтобы не пустовала карточка. */
|
||||||
|
const AREA_ROWS = 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скелетон панели хранилища: подсчёт размеров идёт по диску и занимает секунды. Повторяет вёрстку
|
||||||
|
* {@link StoragePanel} блок в блок, чтобы после загрузки ничего не переехало.
|
||||||
|
*/
|
||||||
|
export function StoragePanelSkeleton() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4" role="status" aria-label={t('common.loading')}>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
<Skeleton className="h-3 w-56" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton className="h-3 w-32" />
|
||||||
|
<Skeleton className="h-8 w-28 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Skeleton className="h-4 w-full rounded-full" />
|
||||||
|
|
||||||
|
<div className="grid gap-3 text-sm sm:grid-cols-4">
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<div key={i} className="flex flex-col gap-1.5">
|
||||||
|
<Skeleton className="h-3 w-20" />
|
||||||
|
<Skeleton className="h-5 w-24" />
|
||||||
|
<Skeleton className="h-3 w-12" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||||
|
<Skeleton className="h-4 w-36" />
|
||||||
|
<Skeleton className="h-3 w-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Skeleton className="h-3 w-full rounded-full" />
|
||||||
|
|
||||||
|
<div className="flex flex-col divide-y divide-border">
|
||||||
|
{Array.from({ length: AREA_ROWS }, (_, i) => (
|
||||||
|
<div key={i} className="flex flex-wrap items-center gap-x-3 gap-y-1 py-2">
|
||||||
|
<Skeleton className="h-3 w-3 shrink-0" />
|
||||||
|
<div className="flex min-w-48 flex-col gap-1.5">
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
<Skeleton className="h-3 w-44" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="ml-auto h-4 w-20" />
|
||||||
|
<Skeleton className="h-3 w-14" />
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { type HTMLAttributes, forwardRef } from 'react'
|
||||||
|
import { cn } from '@/shared/lib/cn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заглушка блока на время загрузки. Только форма — размеры задаёт вызывающий код, чтобы скелетон
|
||||||
|
* повторял реальную вёрстку и страница не прыгала при появлении данных.
|
||||||
|
*/
|
||||||
|
export const Skeleton = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
// Скелетон — визуальный шум для скринридера: статус загрузки объявляет контейнер.
|
||||||
|
aria-hidden
|
||||||
|
className={cn('animate-pulse rounded-sm bg-muted-foreground/20', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Skeleton.displayName = 'Skeleton'
|
||||||
Reference in New Issue
Block a user