quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 22s
quality / web (push) Successful in 1m0s
CD / quality (push) Successful in 1m35s
CD / publish (push) Successful in 2m59s
Секции страниц переведены на segmented Tabs (c-tabs-9), статус грида — ToggleGroup в toolbar. Убраны календарь задач, пустые вкладки мониторинга и demo-деревья ReUI. KPI overflow как в CFDM, DetailPanel на карточке модуля, один FrameSection. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
/**
|
|
* ReUI Empty + IconStack — Frame-friendly (empty-state-14 / empty-state-12).
|
|
* Preview: https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12
|
|
* Docs: https://reui.io/docs/components/base/icon-stack
|
|
*/
|
|
import { InboxIcon, type LucideIcon } from 'lucide-react'
|
|
import type { ReactNode } from 'react'
|
|
|
|
import { IconStack } from '@/components/reui/icon-stack'
|
|
import {
|
|
Frame,
|
|
FramePanel,
|
|
} from '@/components/reui/frame'
|
|
import {
|
|
Empty,
|
|
EmptyContent,
|
|
EmptyDescription,
|
|
EmptyHeader,
|
|
EmptyMedia,
|
|
EmptyTitle,
|
|
} from '@evobgp/ui/components/empty'
|
|
import { cn } from '@evobgp/ui/lib/utils'
|
|
|
|
interface EmptyStateProps {
|
|
icon?: LucideIcon | ReactNode
|
|
title: string
|
|
description?: string
|
|
action?: ReactNode
|
|
className?: string
|
|
stackedIcon?: boolean
|
|
centered?: boolean
|
|
/** Wrap in Frame (empty-state-14). Nested grids should leave this false. */
|
|
framed?: boolean
|
|
}
|
|
|
|
function isLucideIcon(icon: LucideIcon | ReactNode): icon is LucideIcon {
|
|
if (typeof icon === 'function') return true
|
|
if (icon != null && typeof icon === 'object' && '$$typeof' in icon) {
|
|
if ('props' in icon) return false
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function EmptyState({
|
|
icon,
|
|
title,
|
|
description,
|
|
action,
|
|
className,
|
|
stackedIcon = true,
|
|
centered = true,
|
|
framed = false,
|
|
}: EmptyStateProps) {
|
|
const Icon = isLucideIcon(icon) ? icon : InboxIcon
|
|
const customIcon = icon && !isLucideIcon(icon) ? icon : null
|
|
|
|
const empty = (
|
|
<Empty
|
|
className={cn(
|
|
'max-w-md flex-none border-0 bg-transparent p-0',
|
|
!centered && !framed && className,
|
|
)}
|
|
>
|
|
<EmptyHeader className="gap-5 text-center">
|
|
<EmptyMedia className="mb-0">
|
|
{customIcon ? (
|
|
stackedIcon ? (
|
|
<IconStack aria-hidden="true" className="h-14 w-12 shrink-0">
|
|
{customIcon}
|
|
</IconStack>
|
|
) : (
|
|
<div className="text-muted-foreground">{customIcon}</div>
|
|
)
|
|
) : stackedIcon ? (
|
|
<IconStack aria-hidden="true" className="h-14 w-12 shrink-0">
|
|
<Icon strokeWidth={1.9} aria-hidden="true" className="size-5" />
|
|
</IconStack>
|
|
) : (
|
|
<span className="bg-muted text-muted-foreground flex size-10 items-center justify-center rounded-lg [&_svg]:size-5">
|
|
<Icon aria-hidden="true" />
|
|
</span>
|
|
)}
|
|
</EmptyMedia>
|
|
<div className="flex flex-col items-center gap-2">
|
|
<EmptyTitle className="text-base font-semibold tracking-tight">{title}</EmptyTitle>
|
|
{description ? (
|
|
<EmptyDescription className="max-w-sm text-sm/relaxed">{description}</EmptyDescription>
|
|
) : null}
|
|
</div>
|
|
</EmptyHeader>
|
|
{action ? (
|
|
<EmptyContent className="mt-1 items-center justify-center">{action}</EmptyContent>
|
|
) : null}
|
|
</Empty>
|
|
)
|
|
|
|
const body = framed ? (
|
|
<Frame dense spacing="sm" className={cn('w-full', !centered && className)}>
|
|
<FramePanel className="flex min-h-[240px] flex-col items-center justify-center gap-4 p-6 sm:p-10">
|
|
{empty}
|
|
</FramePanel>
|
|
</Frame>
|
|
) : (
|
|
empty
|
|
)
|
|
|
|
if (!centered) return body
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex w-full flex-1 items-center justify-center self-stretch py-14 sm:py-16',
|
|
className,
|
|
)}
|
|
>
|
|
{body}
|
|
</div>
|
|
)
|
|
}
|