perf(renderer): lazy mount closed menu controls

This commit is contained in:
777genius 2026-05-31 02:59:56 +03:00
parent f06a50f859
commit 808e5f73e4
4 changed files with 194 additions and 184 deletions

View file

@ -68,12 +68,12 @@ export const MoreMenu = ({
openTeamsTab, openTeamsTab,
} = useStore( } = useStore(
useShallow((s) => ({ useShallow((s) => ({
openCommandPalette: () => s.openCommandPalette(), openCommandPalette: s.openCommandPalette,
openExtensionsTab: () => s.openExtensionsTab(), openExtensionsTab: s.openExtensionsTab,
openSessionReport: (tabId: string) => s.openSessionReport(tabId), openSessionReport: s.openSessionReport,
openSchedulesTab: () => s.openSchedulesTab(), openSchedulesTab: s.openSchedulesTab,
openSettingsTab: () => s.openSettingsTab(), openSettingsTab: s.openSettingsTab,
openTeamsTab: () => s.openTeamsTab(), openTeamsTab: s.openTeamsTab,
})) }))
); );

View file

@ -1,4 +1,4 @@
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer'; import { useAppTranslation } from '@features/localization/renderer';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
@ -46,6 +46,7 @@ export const KanbanFilterPopover = ({
onFilterChange, onFilterChange,
}: KanbanFilterPopoverProps): React.JSX.Element => { }: KanbanFilterPopoverProps): React.JSX.Element => {
const { t } = useAppTranslation('team'); const { t } = useAppTranslation('team');
const [open, setOpen] = useState(false);
const activeCount = useMemo(() => { const activeCount = useMemo(() => {
let count = 0; let count = 0;
if (filter.sessionId !== null) count += 1; if (filter.sessionId !== null) count += 1;
@ -83,7 +84,7 @@ export const KanbanFilterPopover = ({
}; };
return ( return (
<Popover> <Popover open={open} onOpenChange={setOpen}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -104,111 +105,113 @@ export const KanbanFilterPopover = ({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom">{t('kanban.filter.title')}</TooltipContent> <TooltipContent side="bottom">{t('kanban.filter.title')}</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent align="end" className="w-72 p-0"> {open ? (
{/* Session section */} <PopoverContent align="end" className="w-72 p-0">
<div className="border-b border-[var(--color-border)] p-3"> {/* Session section */}
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]"> <div className="border-b border-[var(--color-border)] p-3">
{t('kanban.filter.session')} <p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
</p> {t('kanban.filter.session')}
<div className="max-h-40 space-y-0.5 overflow-y-auto"> </p>
<button <div className="max-h-40 space-y-0.5 overflow-y-auto">
type="button" <button
className={`w-full rounded-md px-2 py-1.5 text-left text-xs transition-colors ${ type="button"
filter.sessionId === null className={`w-full rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
? 'bg-blue-500/15 text-blue-300' filter.sessionId === null
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]' ? 'bg-blue-500/15 text-blue-300'
}`} : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
onClick={() => handleSessionSelect(null)} }`}
> onClick={() => handleSessionSelect(null)}
{t('kanban.filter.allSessions')} >
</button> {t('kanban.filter.allSessions')}
{sessions.map((session) => { </button>
const isLead = session.id === leadSessionId; {sessions.map((session) => {
const isSelected = filter.sessionId === session.id; const isLead = session.id === leadSessionId;
const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8); const isSelected = filter.sessionId === session.id;
return ( const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8);
<button return (
key={session.id} <button
type="button" key={session.id}
className={`flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors ${ type="button"
isSelected className={`flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors ${
? 'bg-blue-500/15 text-blue-300' isSelected
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]' ? 'bg-blue-500/15 text-blue-300'
}`} : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
onClick={() => handleSessionSelect(isSelected ? null : session.id)} }`}
onClick={() => handleSessionSelect(isSelected ? null : session.id)}
>
{isLead && <Crown size={11} className="shrink-0 text-blue-400" />}
<span className="truncate">{label}</span>
</button>
);
})}
</div>
</div>
{/* Teammate section */}
<div className="border-b border-[var(--color-border)] p-3">
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
{t('kanban.filter.teammate')}
</p>
<div className="space-y-1.5">
{members.map((member) => (
<label
key={member.name}
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]"
> >
{isLead && <Crown size={11} className="shrink-0 text-blue-400" />} <Checkbox
<span className="truncate">{label}</span> checked={filter.selectedOwners.has(member.name)}
</button> onCheckedChange={() => handleOwnerToggle(member.name)}
); />
})} {displayMemberName(member.name)}
</div> </label>
</div> ))}
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- Radix Checkbox renders a button, not a native input */}
{/* Teammate section */} <label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs italic text-[var(--color-text-muted)] hover:bg-[var(--color-surface-raised)]">
<div className="border-b border-[var(--color-border)] p-3">
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
{t('kanban.filter.teammate')}
</p>
<div className="space-y-1.5">
{members.map((member) => (
<label
key={member.name}
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]"
>
<Checkbox <Checkbox
checked={filter.selectedOwners.has(member.name)} checked={filter.selectedOwners.has(UNASSIGNED_OWNER)}
onCheckedChange={() => handleOwnerToggle(member.name)} onCheckedChange={() => handleOwnerToggle(UNASSIGNED_OWNER)}
/> />
{displayMemberName(member.name)} {t('kanban.filter.unassigned')}
</label> </label>
))} </div>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- Radix Checkbox renders a button, not a native input */}
<label className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs italic text-[var(--color-text-muted)] hover:bg-[var(--color-surface-raised)]">
<Checkbox
checked={filter.selectedOwners.has(UNASSIGNED_OWNER)}
onCheckedChange={() => handleOwnerToggle(UNASSIGNED_OWNER)}
/>
{t('kanban.filter.unassigned')}
</label>
</div> </div>
</div>
{/* Column section */} {/* Column section */}
<div className="border-b border-[var(--color-border)] p-3"> <div className="border-b border-[var(--color-border)] p-3">
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]"> <p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
{t('kanban.filter.column')} {t('kanban.filter.column')}
</p> </p>
<div className="space-y-1.5"> <div className="space-y-1.5">
{KANBAN_COLUMNS.map((col) => ( {KANBAN_COLUMNS.map((col) => (
<label <label
key={col.id} key={col.id}
className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs hover:bg-[var(--color-surface-raised)]" className="flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs hover:bg-[var(--color-surface-raised)]"
style={{ color: col.color }} style={{ color: col.color }}
> >
<Checkbox <Checkbox
checked={filter.columns.has(col.id)} checked={filter.columns.has(col.id)}
onCheckedChange={() => handleColumnToggle(col.id)} onCheckedChange={() => handleColumnToggle(col.id)}
/> />
{t(col.labelKey)} {t(col.labelKey)}
</label> </label>
))} ))}
</div>
</div> </div>
</div>
{/* Footer */} {/* Footer */}
<div className="flex justify-end p-2"> <div className="flex justify-end p-2">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]" className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
disabled={activeCount === 0} disabled={activeCount === 0}
onClick={handleClearAll} onClick={handleClearAll}
> >
{t('kanban.filter.clearAll')} {t('kanban.filter.clearAll')}
</Button> </Button>
</div> </div>
</PopoverContent> </PopoverContent>
) : null}
</Popover> </Popover>
); );
}; };

View file

@ -1,3 +1,5 @@
import { useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer'; import { useAppTranslation } from '@features/localization/renderer';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
@ -53,10 +55,11 @@ export const KanbanSortPopover = ({
onSortChange, onSortChange,
}: KanbanSortPopoverProps): React.JSX.Element => { }: KanbanSortPopoverProps): React.JSX.Element => {
const { t } = useAppTranslation('team'); const { t } = useAppTranslation('team');
const [open, setOpen] = useState(false);
const isNonDefault = sort.field !== 'updatedAt'; const isNonDefault = sort.field !== 'updatedAt';
return ( return (
<Popover> <Popover open={open} onOpenChange={setOpen}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -77,66 +80,68 @@ export const KanbanSortPopover = ({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom">{t('kanban.sort.title')}</TooltipContent> <TooltipContent side="bottom">{t('kanban.sort.title')}</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent align="end" className="w-56 p-0"> {open ? (
<div className="p-3"> <PopoverContent align="end" className="w-56 p-0">
<p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]"> <div className="p-3">
{t('kanban.sort.sortBy')} <p className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
</p> {t('kanban.sort.sortBy')}
<div className="space-y-0.5"> </p>
{SORT_OPTIONS.map((option) => { <div className="space-y-0.5">
const isSelected = sort.field === option.field; {SORT_OPTIONS.map((option) => {
return ( const isSelected = sort.field === option.field;
<button return (
key={option.field} <button
type="button" key={option.field}
className={cn( type="button"
'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors',
isSelected
? 'bg-blue-500/15 text-blue-300'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
)}
onClick={() => onSortChange({ field: option.field })}
>
<span
className={cn( className={cn(
'shrink-0', 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-xs transition-colors',
isSelected ? 'text-blue-400' : 'text-[var(--color-text-muted)]' isSelected
? 'bg-blue-500/15 text-blue-300'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
)} )}
onClick={() => onSortChange({ field: option.field })}
> >
{option.icon} <span
</span>
<div className="min-w-0">
<div className="font-medium">{t(option.labelKey)}</div>
<div
className={cn( className={cn(
'text-[10px]', 'shrink-0',
isSelected ? 'text-blue-300/70' : 'text-[var(--color-text-muted)]' isSelected ? 'text-blue-400' : 'text-[var(--color-text-muted)]'
)} )}
> >
{t(option.descriptionKey)} {option.icon}
</span>
<div className="min-w-0">
<div className="font-medium">{t(option.labelKey)}</div>
<div
className={cn(
'text-[10px]',
isSelected ? 'text-blue-300/70' : 'text-[var(--color-text-muted)]'
)}
>
{t(option.descriptionKey)}
</div>
</div> </div>
</div> {isSelected && (
{isSelected && ( <ArrowDownUp size={12} className="ml-auto shrink-0 text-blue-400" />
<ArrowDownUp size={12} className="ml-auto shrink-0 text-blue-400" /> )}
)} </button>
</button> );
); })}
})} </div>
</div> </div>
</div> {isNonDefault && (
{isNonDefault && ( <div className="flex justify-end border-t border-[var(--color-border)] p-2">
<div className="flex justify-end border-t border-[var(--color-border)] p-2"> <Button
<Button variant="ghost"
variant="ghost" size="sm"
size="sm" className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
className="h-6 px-2 text-[11px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]" onClick={() => onSortChange({ field: 'updatedAt' })}
onClick={() => onSortChange({ field: 'updatedAt' })} >
> {t('kanban.sort.reset')}
{t('kanban.sort.reset')} </Button>
</Button> </div>
</div> )}
)} </PopoverContent>
</PopoverContent> ) : null}
</Popover> </Popover>
); );
}; };

View file

@ -206,32 +206,34 @@ const CancelTaskButton = ({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top">{t('kanban.taskCard.cancel')}</TooltipContent> <TooltipContent side="top">{t('kanban.taskCard.cancel')}</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent {open ? (
className="w-56 p-3" <PopoverContent
side="top" className="w-56 p-3"
align="start" side="top"
onClick={(e) => e.stopPropagation()} align="start"
> onClick={(e) => e.stopPropagation()}
<p className="mb-3 text-xs text-[var(--color-text-secondary)]"> >
{t('kanban.taskCard.moveBackToTodoConfirm')} <p className="mb-3 text-xs text-[var(--color-text-secondary)]">
</p> {t('kanban.taskCard.moveBackToTodoConfirm')}
<div className="flex gap-2"> </p>
<Button <div className="flex gap-2">
variant="destructive" <Button
size="sm" variant="destructive"
className="flex-1" size="sm"
onClick={() => { className="flex-1"
setOpen(false); onClick={() => {
onConfirm(taskId); setOpen(false);
}} onConfirm(taskId);
> }}
{t('kanban.taskCard.confirm')} >
</Button> {t('kanban.taskCard.confirm')}
<Button variant="outline" size="sm" className="flex-1" onClick={() => setOpen(false)}> </Button>
{t('kanban.taskCard.keep')} <Button variant="outline" size="sm" className="flex-1" onClick={() => setOpen(false)}>
</Button> {t('kanban.taskCard.keep')}
</div> </Button>
</PopoverContent> </div>
</PopoverContent>
) : null}
</Popover> </Popover>
); );
}; };