quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 1m6s
CD / quality (push) Successful in 1m21s
CD / publish (push) Successful in 2m36s
Соседние Frame-колонки как в EvoBGP вместо вложенной оболочки; compact empty через IconTile elevated; счётчики ReUI Badge. Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { EmptyState } from '@/components/empty-state'
|
|
import { Badge } from '@/components/reui/badge'
|
|
import {
|
|
Frame,
|
|
FrameHeader,
|
|
FramePanel,
|
|
FrameTitle,
|
|
} from '@/components/reui/frame'
|
|
import { IconTile } from '@/components/reui/icon-tile'
|
|
import { cn } from '@cfdm/ui/lib/utils'
|
|
|
|
/**
|
|
* Sibling Frame columns for dashboard attention queue.
|
|
* Preview: https://reui.io/preview/base/dashboard-1 · https://reui.io/preview/base/stats-12
|
|
* Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/icon-tile
|
|
*/
|
|
export interface AttentionQueueColumn {
|
|
id: string
|
|
title: string
|
|
icon: LucideIcon
|
|
iconClassName?: string
|
|
count: number
|
|
countVariant?: 'destructive' | 'warning' | 'secondary' | 'destructive-light' | 'warning-light'
|
|
emptyTitle: string
|
|
emptyDescription: string
|
|
emptyAction?: ReactNode
|
|
children: ReactNode
|
|
}
|
|
|
|
interface AttentionQueueProps {
|
|
columns: AttentionQueueColumn[]
|
|
className?: string
|
|
}
|
|
|
|
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
|
|
|
export function AttentionQueue({ columns, className }: AttentionQueueProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'grid min-w-0 items-start gap-2 @3xl:grid-cols-3',
|
|
className,
|
|
)}
|
|
>
|
|
{columns.map((column) => {
|
|
const Icon = column.icon
|
|
const isEmpty = column.count === 0
|
|
return (
|
|
<Frame key={column.id} dense spacing="sm" className="min-w-0 w-full">
|
|
<FrameHeader>
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<IconTile
|
|
variant="elevated"
|
|
size="sm"
|
|
className={cn(DEFAULT_ICON_CLASS, column.iconClassName)}
|
|
aria-hidden="true"
|
|
>
|
|
<Icon />
|
|
</IconTile>
|
|
<FrameTitle className="min-w-0 truncate">{column.title}</FrameTitle>
|
|
{column.count > 0 ? (
|
|
<Badge
|
|
size="sm"
|
|
variant={column.countVariant ?? 'secondary'}
|
|
className="tabular-nums"
|
|
>
|
|
{column.count}
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
</FrameHeader>
|
|
<FramePanel className="min-w-0">
|
|
{isEmpty ? (
|
|
<div className="flex min-h-28 items-center justify-center py-4">
|
|
<EmptyState
|
|
icon={Icon}
|
|
title={column.emptyTitle}
|
|
description={column.emptyDescription}
|
|
action={column.emptyAction}
|
|
centered={false}
|
|
stackedIcon={false}
|
|
/>
|
|
</div>
|
|
) : (
|
|
column.children
|
|
)}
|
|
</FramePanel>
|
|
</Frame>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|