Боковое меню, шапка, монитор системы и палитра Ctrl+K переведены на общие блоки макета; данные берутся из живых запросов EvoBGP. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { Badge } from '@/components/reui/badge'
|
|
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
|
|
import { IconTile } from '@/components/reui/icon-tile'
|
|
import { cn } from '@evobgp/ui/lib/utils'
|
|
import type { Tone } from './tone'
|
|
import { StatusBadge } from './bits'
|
|
|
|
export type AppCardItem = {
|
|
id: string
|
|
title: string
|
|
desc: string
|
|
badge: string
|
|
badgeVariant?: 'info-light' | 'warning-light' | 'outline'
|
|
meta: string
|
|
icon: ReactNode
|
|
onSelect?: () => void
|
|
}
|
|
|
|
/**
|
|
* Плитки приложений как на доске «Приложения»: Frame с шапкой, внутри сетка auto-fit от 260px,
|
|
* в плитке иконка, название, плашка действия, описание и адрес моноширинным шрифтом.
|
|
*/
|
|
export function AppCards({
|
|
title,
|
|
description,
|
|
items,
|
|
action,
|
|
}: {
|
|
title: string
|
|
description: string
|
|
items: AppCardItem[]
|
|
action?: ReactNode
|
|
}) {
|
|
return (
|
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full min-w-0">
|
|
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
<FrameTitle>{title}</FrameTitle>
|
|
<FrameDescription className="text-pretty">{description}</FrameDescription>
|
|
</div>
|
|
{action}
|
|
</FrameHeader>
|
|
<FramePanel className="p-3">
|
|
<ul className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,16.25rem),1fr))] gap-3">
|
|
{items.map((c) => {
|
|
const body = (
|
|
<>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="flex min-w-0 items-center gap-2.5">
|
|
<IconTile variant="elevated" size="sm" aria-hidden className="text-muted-foreground">
|
|
{c.icon}
|
|
</IconTile>
|
|
<b className="min-w-0 text-sm leading-tight font-semibold">{c.title}</b>
|
|
</span>
|
|
<Badge variant={c.badgeVariant ?? 'info-light'} size="sm" className="shrink-0">
|
|
{c.badge}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-muted-foreground mt-1.5 text-[13px] text-pretty">{c.desc}</p>
|
|
<p className="text-muted-foreground mt-2 truncate font-mono text-[11px]">{c.meta}</p>
|
|
</>
|
|
)
|
|
const cls = 'bg-muted/25 w-full rounded-lg border p-3.5 text-left'
|
|
return (
|
|
<li key={c.id} className="min-w-0">
|
|
{c.onSelect ? (
|
|
<button
|
|
type="button"
|
|
onClick={c.onSelect}
|
|
aria-label={`${c.title}: ${c.badge}`}
|
|
className={cn(cls, 'hover:bg-muted/50 focus-visible:ring-ring transition-colors outline-none focus-visible:ring-2')}
|
|
>
|
|
{body}
|
|
</button>
|
|
) : (
|
|
<div className={cls}>{body}</div>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</FramePanel>
|
|
</Frame>
|
|
)
|
|
}
|
|
|
|
const DOT: Record<Tone, string> = {
|
|
ok: 'bg-success',
|
|
wn: 'bg-warning',
|
|
er: 'bg-destructive',
|
|
mu: 'bg-muted-foreground',
|
|
in: 'bg-chart-4',
|
|
}
|
|
|
|
/**
|
|
* Лента событий как на доске «Изменения»: время, точка важности с линией, плашка, заголовок
|
|
* и строка «кто · источник · IP».
|
|
*/
|
|
export function ActivityFeed({
|
|
title,
|
|
description,
|
|
items,
|
|
action,
|
|
}: {
|
|
title: string
|
|
description: string
|
|
items: Array<{ t: string; title: string; text: string; label: string; tone: Tone }>
|
|
action?: ReactNode
|
|
}) {
|
|
return (
|
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full min-w-0">
|
|
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
<FrameTitle>{title}</FrameTitle>
|
|
<FrameDescription className="text-pretty">{description}</FrameDescription>
|
|
</div>
|
|
{action}
|
|
</FrameHeader>
|
|
<FramePanel className="py-1.5">
|
|
<ol>
|
|
{items.map((e) => (
|
|
<li key={`${e.t}-${e.title}`} className="grid grid-cols-[3.9rem_1rem_minmax(0,1fr)] items-start gap-2.5 px-4 py-2 text-[13px]">
|
|
<span className="text-muted-foreground pt-px font-mono text-xs tabular-nums">{e.t}</span>
|
|
<span aria-hidden className="flex h-full flex-col items-center">
|
|
<i className={cn('mt-1 size-[9px] rounded-full', DOT[e.tone])} />
|
|
<u className="bg-border mt-0.5 min-h-4.5 w-px flex-1 no-underline" />
|
|
</span>
|
|
<span className="min-w-0">
|
|
<span className="mr-2 inline-block align-middle">
|
|
<StatusBadge tone={e.tone}>{e.label}</StatusBadge>
|
|
</span>
|
|
<b className="font-medium">{e.title}</b>
|
|
<span className="text-muted-foreground mt-0.5 block text-xs">{e.text}</span>
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</FramePanel>
|
|
</Frame>
|
|
)
|
|
}
|