Refactor dropdown menu components in various files to prevent event propagation, enhancing user interaction. Update CountedLineTabs component documentation with preview links for better clarity. Improve styling and layout in resource page and user columns for consistency and visual appeal.
Build and Push Telemt Panel Docker Image / build-and-push (push) Successful in 2m8s
Build and Push Telemt Panel Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-08-04 22:06:15 +07:00
parent 3c065e8de1
commit ee3e96647e
5 changed files with 115 additions and 75 deletions
+18 -3
View File
@@ -17,7 +17,11 @@ interface CountedLineTabsProps {
children?: ReactNode children?: ReactNode
} }
/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */ /**
* Line tabs with count pills — ReUI PRO DNA.
* Preview: https://reui.io/preview/base/components/c-tabs-2
* Lists: https://reui.io/preview/base/data-grid-filtering-2
*/
export function CountedLineTabs({ export function CountedLineTabs({
tabs, tabs,
value, value,
@@ -28,12 +32,23 @@ export function CountedLineTabs({
}: CountedLineTabsProps) { }: CountedLineTabsProps) {
return ( return (
<Tabs value={value} onValueChange={onValueChange} className={className}> <Tabs value={value} onValueChange={onValueChange} className={className}>
<TabsList variant="line" className={cn('gap-5', listClassName)}> <TabsList
variant="line"
className={cn(
'h-auto w-full justify-start gap-5 rounded-none bg-transparent p-0',
listClassName,
)}
>
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabsTrigger <TabsTrigger
key={tab.id} key={tab.id}
value={tab.id} value={tab.id}
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0" className={cn(
'text-muted-foreground hover:text-foreground h-auto flex-none gap-2 rounded-none px-0 pb-3 shadow-none',
'data-active:bg-transparent data-active:text-foreground data-active:shadow-none',
'dark:data-active:border-transparent dark:data-active:bg-transparent',
'after:bottom-0 after:bg-foreground',
)}
> >
<span>{tab.label}</span> <span>{tab.label}</span>
{tab.count !== undefined ? ( {tab.count !== undefined ? (
@@ -251,26 +251,32 @@ function RowActions({
onOpen: (row: TelemtErrorRow) => void onOpen: (row: TelemtErrorRow) => void
}) { }) {
return ( return (
<DropdownMenu> <div
<DropdownMenuTrigger className="flex items-center justify-center"
render={ onClick={(event) => event.stopPropagation()}
<Button onPointerDown={(event) => event.stopPropagation()}
size="icon" >
variant="ghost" <DropdownMenu>
className="size-7" <DropdownMenuTrigger
aria-label="Действия" render={
/> <Button
} size="icon"
> variant="ghost"
<MoreHorizontalIcon aria-hidden /> className="size-7"
</DropdownMenuTrigger> aria-label="Действия"
<DropdownMenuContent align="end" className="w-40"> />
<DropdownMenuItem onClick={() => onOpen(row.original)}> }
<EyeIcon className="size-4" aria-hidden /> >
Открыть <MoreHorizontalIcon aria-hidden />
</DropdownMenuItem> </DropdownMenuTrigger>
</DropdownMenuContent> <DropdownMenuContent align="end" className="w-40">
</DropdownMenu> <DropdownMenuItem onClick={() => onOpen(row.original)}>
<EyeIcon className="size-4" aria-hidden />
Открыть
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) )
} }
@@ -570,14 +570,13 @@ function ResourcePageFiltered<T extends object>({
<FramePanel className="p-0 shadow-none!"> <FramePanel className="p-0 shadow-none!">
{countedTabs.length > 0 ? ( {countedTabs.length > 0 ? (
<> <>
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)"> <div className="border-border flex flex-col gap-0 border-b px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
<CountedLineTabs <CountedLineTabs
tabs={countedTabs} tabs={countedTabs}
value={activeTab} value={activeTab}
onValueChange={handleTabChange} onValueChange={handleTabChange}
/> />
</div> </div>
<Separator />
</> </>
) : null} ) : null}
@@ -1269,7 +1269,21 @@ function DataGridTableBodyRow<TData>({
data-depth={row.depth || undefined} data-depth={row.depth || undefined}
data-row-pinned={isRowPinned || undefined} data-row-pinned={isRowPinned || undefined}
data-row-pinned-boundary={pinnedBoundary} data-row-pinned-boundary={pinnedBoundary}
onClick={() => props.onRowClick && props.onRowClick(row.original)} onClick={(event) => {
if (!props.onRowClick) return
// Interactive controls (actions menu, pin, checkbox, links) must not
// open the row detail — same contract as DataGridTableRowPin/Select.
const target = event.target
if (
target instanceof Element &&
target.closest(
'button, a, input, textarea, select, [role="button"], [role="menuitem"], [role="checkbox"], [data-slot="dropdown-menu-trigger"], [data-slot="checkbox"]'
)
) {
return
}
props.onRowClick(row.original)
}}
className={cn( className={cn(
"hover:bg-muted/40 data-[state=selected]:bg-muted/50", "hover:bg-muted/40 data-[state=selected]:bg-muted/50",
props.onRowClick && "cursor-pointer", props.onRowClick && "cursor-pointer",
+55 -49
View File
@@ -199,55 +199,61 @@ export function ActionsCell({
return ( return (
<> <>
<DropdownMenu> <div
<DropdownMenuTrigger className="flex items-center justify-center"
render={ onClick={(event) => event.stopPropagation()}
<Button onPointerDown={(event) => event.stopPropagation()}
size="icon" >
variant="ghost" <DropdownMenu>
className="size-7" <DropdownMenuTrigger
aria-label="Действия строки" render={
/> <Button
} size="icon"
> variant="ghost"
<MoreHorizontalIcon aria-hidden="true" /> className="size-7"
</DropdownMenuTrigger> aria-label="Действия строки"
<DropdownMenuContent side="bottom" align="start" className="w-44"> />
<DropdownMenuGroup> }
<DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}> >
{isPinned ? ( <MoreHorizontalIcon aria-hidden="true" />
<PinOffIcon className="size-4" aria-hidden="true" /> </DropdownMenuTrigger>
) : ( <DropdownMenuContent side="bottom" align="start" className="w-44">
<Pin className="size-4" aria-hidden="true" /> <DropdownMenuGroup>
)} <DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}>
{isPinned ? 'Открепить' : 'Закрепить'} {isPinned ? (
</DropdownMenuItem> <PinOffIcon className="size-4" aria-hidden="true" />
<DropdownMenuItem onClick={() => onOpen(row.original)}> ) : (
<EyeIcon className="size-4" aria-hidden="true" /> <Pin className="size-4" aria-hidden="true" />
Открыть )}
</DropdownMenuItem> {isPinned ? 'Открепить' : 'Закрепить'}
<DropdownMenuItem </DropdownMenuItem>
onClick={() => { <DropdownMenuItem onClick={() => onOpen(row.original)}>
copyToClipboard(row.original.username) <EyeIcon className="size-4" aria-hidden="true" />
toast.success('Имя скопировано', { Открыть
description: row.original.username, </DropdownMenuItem>
}) <DropdownMenuItem
}} onClick={() => {
> copyToClipboard(row.original.username)
<CopyIcon className="size-4" aria-hidden="true" /> toast.success('Имя скопировано', {
Копировать имя description: row.original.username,
</DropdownMenuItem> })
<DropdownMenuSeparator /> }}
<DropdownMenuItem >
variant="destructive" <CopyIcon className="size-4" aria-hidden="true" />
onClick={() => setDeleteOpen(true)} Копировать имя
> </DropdownMenuItem>
<Trash2Icon className="size-4" aria-hidden="true" /> <DropdownMenuSeparator />
Удалить <DropdownMenuItem
</DropdownMenuItem> variant="destructive"
</DropdownMenuGroup> onClick={() => setDeleteOpen(true)}
</DropdownMenuContent> >
</DropdownMenu> <Trash2Icon className="size-4" aria-hidden="true" />
Удалить
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}> <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<AlertDialogContent size="sm"> <AlertDialogContent size="sm">