Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m13s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
Co-authored-by: Cursor <[email protected]>
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
"use client"
|
|
|
|
import type { KeyboardEvent, ReactNode } from "react"
|
|
import Link from "next/link"
|
|
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 "@/lib/utils"
|
|
import { kpiCols } from "./kpi-cols"
|
|
|
|
type QuickActionBase = {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
icon?: ReactNode
|
|
iconClassName?: string
|
|
badge?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
export type QuickActionItem = QuickActionBase &
|
|
(
|
|
| { href: string; onClick?: never }
|
|
| { onClick: () => void; href?: 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 · https://reui.io/preview/base/card-12
|
|
* Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/icon-tile
|
|
*/
|
|
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 ("href" in action && action.href) {
|
|
return (
|
|
<FramePanel key={action.id} className={panelClassName(action.disabled)}>
|
|
{action.disabled ? (
|
|
<div aria-disabled aria-label={label}>
|
|
<QuickActionBody action={action} />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<QuickActionBody action={action} />
|
|
<Link
|
|
href={action.href}
|
|
className="absolute inset-0 z-20 focus-visible:outline-none"
|
|
aria-label={label}
|
|
/>
|
|
</>
|
|
)}
|
|
</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>
|
|
)
|
|
}
|