Files
EvoBGP/apps/web/src/components/reui-kit/quick-action-grid.tsx
T
DenozordecandCursor b04b27c215
CI / changes (push) Successful in 5s
CI / openapi (push) Successful in 39s
CI / web (push) Successful in 55s
CI / go (push) Successful in 2m40s
CI / bird2 (push) Successful in 16s
CI / commitlint (push) Skipped
CI / release (push) Successful in 18s
refactor(web): align Frame KPI settings chrome and a11y
Выровнять KPI на IconTile, вынести settings из settings-7, Sheet/AlertDialog и skip-link в общем chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 15:36:54 +07:00

160 lines
4.7 KiB
TypeScript

import type { KeyboardEvent, ReactNode } from 'react'
import { Link } from '@tanstack/react-router'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { Badge } from '@/components/reui/badge'
import { IconTile } from '@/components/reui/icon-tile'
import { cn } from '@evobgp/ui/lib/utils'
import { kpiCols } from './kpi-cols'
type QuickActionBase = {
id: string
title: string
description: string
icon?: ReactNode
iconClassName?: string
/** Override badge label (default: «Перейти» for links, «Выполнить» for onClick). */
badge?: string
disabled?: boolean
}
export type QuickActionItem = QuickActionBase &
(
| { to: string; search?: Record<string, unknown>; onClick?: never }
| { onClick: () => void; to?: never; search?: never }
)
interface QuickActionGridProps {
actions: QuickActionItem[]
title?: string
description?: string
className?: string
}
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
function resolveBadge(action: QuickActionItem): string {
if (action.badge) return action.badge
return action.onClick ? 'Выполнить' : 'Перейти'
}
function handleActionKeyDown(onActivate: () => void, event: KeyboardEvent<HTMLDivElement>) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onActivate()
}
}
function QuickActionBody({ action }: { action: QuickActionItem }) {
return (
<div className="relative z-10 flex h-full items-start gap-3">
{action.icon ? (
<IconTile
variant="elevated"
aria-hidden="true"
className={cn('size-10.5', action.iconClassName ?? DEFAULT_ICON_CLASS)}
>
{action.icon}
</IconTile>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-start justify-between gap-2">
<span className="text-foreground text-sm font-medium">{action.title}</span>
<Badge variant="outline" size="sm" className="shrink-0">
{resolveBadge(action)}
</Badge>
</div>
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
{action.description}
</p>
</div>
</div>
)
}
function panelClassName(disabled?: boolean) {
return cn(
'relative isolate flex h-full flex-col transition-colors',
disabled
? 'cursor-not-allowed opacity-60'
: 'hover:bg-muted/40 focus-within:ring-ring cursor-pointer focus-within:ring-2',
)
}
/**
* KPI-like quick actions strip (horizontal Frame tiles).
* Preview: https://reui.io/preview/base/stats-12
*/
export function QuickActionGrid({
actions,
title = 'Быстрые действия',
description,
className,
}: QuickActionGridProps) {
if (actions.length === 0) return null
return (
<Frame dense spacing="sm" className={cn('@container w-full', className)}>
{(title || description) && (
<FrameHeader>
{title ? <FrameTitle>{title}</FrameTitle> : null}
{description ? <FrameDescription>{description}</FrameDescription> : null}
</FrameHeader>
)}
<div className={cn('grid gap-2', kpiCols(actions.length))}>
{actions.map((action) => {
const label = `${action.title}: ${action.description}`
if ('to' in action && action.to) {
return (
<FramePanel key={action.id} className={panelClassName(action.disabled)}>
{action.disabled ? (
<div aria-disabled aria-label={label}>
<QuickActionBody action={action} />
</div>
) : (
<Link
to={action.to}
search={action.search}
className="focus-visible:outline-none"
aria-label={label}
>
<QuickActionBody action={action} />
</Link>
)}
</FramePanel>
)
}
const onClick = action.onClick
const onActivate = () => {
if (action.disabled || !onClick) return
onClick()
}
return (
<FramePanel
key={action.id}
className={panelClassName(action.disabled)}
role="button"
tabIndex={action.disabled ? -1 : 0}
aria-disabled={action.disabled || undefined}
aria-label={label}
onClick={onActivate}
onKeyDown={(e) => handleActionKeyDown(onActivate, e)}
>
<QuickActionBody action={action} />
</FramePanel>
)
})}
</div>
</Frame>
)
}