perf: trim team page render overhead
This commit is contained in:
parent
ef129f8931
commit
f05aefe097
7 changed files with 132 additions and 148 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
|
||||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount';
|
import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount';
|
||||||
|
|
@ -178,7 +177,7 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex w-full cursor-pointer flex-col justify-center border-b px-2 py-1.5 text-left transition-colors hover:bg-surface-raised ${unreadBackgroundClass} ${task.teamDeleted ? 'opacity-50' : ''}`}
|
className={`sidebar-task-item flex w-full cursor-pointer flex-col justify-center border-b px-2 py-1.5 text-left transition-colors hover:bg-surface-raised ${unreadBackgroundClass} ${task.teamDeleted ? 'opacity-50' : ''}`}
|
||||||
style={{ borderColor: 'var(--color-border)' }}
|
style={{ borderColor: 'var(--color-border)' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isRenaming) {
|
if (!isRenaming) {
|
||||||
|
|
@ -225,37 +224,29 @@ export const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<span
|
||||||
<TooltipTrigger asChild>
|
className="line-clamp-2 text-[13px] font-medium leading-tight"
|
||||||
|
style={{ color: 'var(--color-text-muted)' }}
|
||||||
|
title={displaySubject}
|
||||||
|
>
|
||||||
|
<StatusIcon className={cn('mr-1.5 inline-block align-[-1px]', statusIconClassName)} />
|
||||||
|
{unreadCount > 0 &&
|
||||||
|
(unreadCount === 1 ? (
|
||||||
|
<span className="mr-1 inline-block size-1.5 rounded-full bg-blue-400 align-middle" />
|
||||||
|
) : (
|
||||||
|
<span className="mr-1 inline-flex size-3.5 items-center justify-center rounded-full bg-blue-500 align-middle text-[8px] font-bold leading-none text-white">
|
||||||
|
{unreadCount > 9 ? '9+' : unreadCount}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{displaySubject}
|
||||||
|
{isTeamTaskNeedsFixActionable(task) && (
|
||||||
<span
|
<span
|
||||||
className="line-clamp-2 text-[13px] font-medium leading-tight"
|
className={`ml-1.5 inline-block rounded-full px-1.5 py-0.5 align-middle text-[10px] font-medium leading-none ${REVIEW_STATE_DISPLAY.needsFix.bg} ${REVIEW_STATE_DISPLAY.needsFix.text}`}
|
||||||
style={{ color: 'var(--color-text-muted)' }}
|
|
||||||
>
|
>
|
||||||
<StatusIcon
|
{tCommon('tasks.reviewState.needsFix')}
|
||||||
className={cn('mr-1.5 inline-block align-[-1px]', statusIconClassName)}
|
|
||||||
/>
|
|
||||||
{unreadCount > 0 &&
|
|
||||||
(unreadCount === 1 ? (
|
|
||||||
<span className="mr-1 inline-block size-1.5 rounded-full bg-blue-400 align-middle" />
|
|
||||||
) : (
|
|
||||||
<span className="mr-1 inline-flex size-3.5 items-center justify-center rounded-full bg-blue-500 align-middle text-[8px] font-bold leading-none text-white">
|
|
||||||
{unreadCount > 9 ? '9+' : unreadCount}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{displaySubject}
|
|
||||||
{isTeamTaskNeedsFixActionable(task) && (
|
|
||||||
<span
|
|
||||||
className={`ml-1.5 inline-block rounded-full px-1.5 py-0.5 align-middle text-[10px] font-medium leading-none ${REVIEW_STATE_DISPLAY.needsFix.bg} ${REVIEW_STATE_DISPLAY.needsFix.text}`}
|
|
||||||
>
|
|
||||||
{tCommon('tasks.reviewState.needsFix')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
)}
|
||||||
<TooltipContent side="right" sideOffset={6}>
|
</span>
|
||||||
{displaySubject}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,12 @@ const ActiveTeamCard = ({
|
||||||
const launchMode: TeamLaunchDialogMode = status === 'offline' ? 'launch' : 'relaunch';
|
const launchMode: TeamLaunchDialogMode = status === 'offline' ? 'launch' : 'relaunch';
|
||||||
const launchLabel =
|
const launchLabel =
|
||||||
launchMode === 'relaunch' ? t('list.actions.relaunchTeam') : t('list.actions.launchTeam');
|
launchMode === 'relaunch' ? t('list.actions.relaunchTeam') : t('list.actions.launchTeam');
|
||||||
|
const launchTitle =
|
||||||
|
launchingTeamName === team.teamName ? t('list.actions.launching') : launchLabel;
|
||||||
|
const stopTitle =
|
||||||
|
stoppingTeamName === team.teamName ? t('list.actions.stopping') : t('list.actions.stopTeam');
|
||||||
|
const copyTitle = t('list.actions.copyTeam');
|
||||||
|
const deleteTitle = t('list.actions.deleteTeam');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -400,78 +406,51 @@ const ActiveTeamCard = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 gap-1">
|
<div className="flex shrink-0 gap-1">
|
||||||
{canLaunch ? (
|
{canLaunch ? (
|
||||||
<Tooltip>
|
<button
|
||||||
<TooltipTrigger asChild>
|
type="button"
|
||||||
<button
|
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-emerald-500/10 hover:text-emerald-300 disabled:opacity-50 group-hover:opacity-100"
|
||||||
type="button"
|
onClick={(event) =>
|
||||||
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-emerald-500/10 hover:text-emerald-300 disabled:opacity-50 group-hover:opacity-100"
|
onLaunchTeam(team.teamName, team.projectPath ?? undefined, launchMode, event)
|
||||||
onClick={(event) =>
|
}
|
||||||
onLaunchTeam(
|
disabled={launchingTeamName === team.teamName}
|
||||||
team.teamName,
|
aria-label={launchTitle}
|
||||||
team.projectPath ?? undefined,
|
title={launchTitle}
|
||||||
launchMode,
|
>
|
||||||
event
|
<Play size={14} fill="currentColor" />
|
||||||
)
|
</button>
|
||||||
}
|
|
||||||
disabled={launchingTeamName === team.teamName}
|
|
||||||
aria-label={launchLabel}
|
|
||||||
>
|
|
||||||
<Play size={14} fill="currentColor" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
{launchingTeamName === team.teamName
|
|
||||||
? t('list.actions.launching')
|
|
||||||
: launchLabel}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
) : null}
|
||||||
{status === 'active' || status === 'idle' ? (
|
{status === 'active' || status === 'idle' ? (
|
||||||
<Tooltip>
|
<button
|
||||||
<TooltipTrigger asChild>
|
type="button"
|
||||||
<button
|
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-amber-500/10 hover:text-amber-300 disabled:opacity-50 group-hover:opacity-100"
|
||||||
type="button"
|
onClick={(event) => onStopTeam(team.teamName, event)}
|
||||||
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-amber-500/10 hover:text-amber-300 disabled:opacity-50 group-hover:opacity-100"
|
disabled={stoppingTeamName === team.teamName}
|
||||||
onClick={(event) => onStopTeam(team.teamName, event)}
|
aria-label={stopTitle}
|
||||||
disabled={stoppingTeamName === team.teamName}
|
title={stopTitle}
|
||||||
aria-label={t('list.actions.stopTeam')}
|
>
|
||||||
>
|
<Square size={14} fill="currentColor" />
|
||||||
<Square size={14} fill="currentColor" />
|
</button>
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
{stoppingTeamName === team.teamName
|
|
||||||
? t('list.actions.stopping')
|
|
||||||
: t('list.actions.stopTeam')}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
) : null}
|
||||||
{!team.pendingCreate ? (
|
{!team.pendingCreate ? (
|
||||||
<Tooltip>
|
<button
|
||||||
<TooltipTrigger asChild>
|
type="button"
|
||||||
<button
|
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-blue-500/10 hover:text-blue-300 group-hover:opacity-100"
|
||||||
type="button"
|
onClick={(event) => onCopyTeam(team.teamName, event)}
|
||||||
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-blue-500/10 hover:text-blue-300 group-hover:opacity-100"
|
aria-label={copyTitle}
|
||||||
onClick={(event) => onCopyTeam(team.teamName, event)}
|
title={copyTitle}
|
||||||
>
|
>
|
||||||
<Copy size={14} />
|
<Copy size={14} />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">{t('list.actions.copyTeam')}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Tooltip>
|
<button
|
||||||
<TooltipTrigger asChild>
|
type="button"
|
||||||
<button
|
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-red-500/10 hover:text-red-300 group-hover:opacity-100"
|
||||||
type="button"
|
onClick={(event) => onDeleteTeam(team.teamName, !!team.pendingCreate, event)}
|
||||||
className="shrink-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-red-500/10 hover:text-red-300 group-hover:opacity-100"
|
aria-label={deleteTitle}
|
||||||
onClick={(event) => onDeleteTeam(team.teamName, !!team.pendingCreate, event)}
|
title={deleteTitle}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">{t('list.actions.deleteTeam')}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,7 @@ import { AttachmentDisplay } from '@renderer/components/team/attachments/Attachm
|
||||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||||
import { TaskTooltip } from '@renderer/components/team/TaskTooltip';
|
import { TaskTooltip } from '@renderer/components/team/TaskTooltip';
|
||||||
import { ExpandableContent } from '@renderer/components/ui/ExpandableContent';
|
import { ExpandableContent } from '@renderer/components/ui/ExpandableContent';
|
||||||
import {
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@renderer/components/ui/tooltip';
|
|
||||||
import {
|
import {
|
||||||
CARD_BG,
|
CARD_BG,
|
||||||
CARD_BG_ZEBRA,
|
CARD_BG_ZEBRA,
|
||||||
|
|
@ -1310,7 +1305,7 @@ export const ActivityItem = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className="group overflow-hidden rounded-md"
|
className="activity-timeline-card group overflow-hidden rounded-md"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: isSlashCommandResult ? 26 : isUserSent ? 15 : undefined,
|
marginLeft: isSlashCommandResult ? 26 : isUserSent ? 15 : undefined,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
|
|
@ -1424,27 +1419,14 @@ export const ActivityItem = memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider delayDuration={1000}>
|
<div title={compactPreviewTooltipText}>
|
||||||
<Tooltip>
|
<CompactMarkdownPreview
|
||||||
<TooltipTrigger asChild>
|
content={compactPreviewMarkdown}
|
||||||
<div>
|
className="mt-1 line-clamp-2 w-full min-w-0 max-w-full break-words text-[11px] leading-4"
|
||||||
<CompactMarkdownPreview
|
teamColorByName={teamColorByName}
|
||||||
content={compactPreviewMarkdown}
|
onTeamClick={onTeamClick}
|
||||||
className="mt-1 line-clamp-2 w-full min-w-0 max-w-full break-words text-[11px] leading-4"
|
/>
|
||||||
teamColorByName={teamColorByName}
|
</div>
|
||||||
onTeamClick={onTeamClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
className="max-w-sm whitespace-normal break-words"
|
|
||||||
>
|
|
||||||
{compactPreviewTooltipText}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
) : !isExpanded ? (
|
) : !isExpanded ? (
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|
@ -1506,27 +1488,14 @@ export const ActivityItem = memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider delayDuration={1000}>
|
<div title={compactPreviewTooltipText}>
|
||||||
<Tooltip>
|
<CompactMarkdownPreview
|
||||||
<TooltipTrigger asChild>
|
content={compactPreviewMarkdown}
|
||||||
<div>
|
className="mt-1 line-clamp-2 w-full min-w-0 max-w-full break-words text-[11px] leading-4"
|
||||||
<CompactMarkdownPreview
|
teamColorByName={teamColorByName}
|
||||||
content={compactPreviewMarkdown}
|
onTeamClick={onTeamClick}
|
||||||
className="mt-1 line-clamp-2 w-full min-w-0 max-w-full break-words text-[11px] leading-4"
|
/>
|
||||||
teamColorByName={teamColorByName}
|
</div>
|
||||||
onTeamClick={onTeamClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="bottom"
|
|
||||||
align="start"
|
|
||||||
className="max-w-sm whitespace-normal break-words"
|
|
||||||
>
|
|
||||||
{compactPreviewTooltipText}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ export interface TimelineViewport {
|
||||||
scrollMargin?: number;
|
scrollMargin?: number;
|
||||||
/** Enable virtualization (wired in a follow-up; ignored for now). */
|
/** Enable virtualization (wired in a follow-up; ignored for now). */
|
||||||
virtualizationEnabled?: boolean;
|
virtualizationEnabled?: boolean;
|
||||||
|
/** Optional row-count gate for compact hosts that need virtualization earlier. */
|
||||||
|
virtualizationRowThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActivityTimelineProps {
|
interface ActivityTimelineProps {
|
||||||
|
|
@ -692,7 +694,7 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({
|
||||||
const shouldVirtualize =
|
const shouldVirtualize =
|
||||||
viewport?.virtualizationEnabled === true &&
|
viewport?.virtualizationEnabled === true &&
|
||||||
viewport.scrollElementRef != null &&
|
viewport.scrollElementRef != null &&
|
||||||
renderRows.length >= VIRTUALIZATION_ROW_THRESHOLD;
|
renderRows.length >= (viewport.virtualizationRowThreshold ?? VIRTUALIZATION_ROW_THRESHOLD);
|
||||||
|
|
||||||
// DOM-measured distance from the scroll container's scroll origin to the
|
// DOM-measured distance from the scroll container's scroll origin to the
|
||||||
// timeline root. We avoid re-measuring on every scroll: the offset only
|
// timeline root. We avoid re-measuring on every scroll: the offset only
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,29 @@ export const AnimatedHeightReveal = ({
|
||||||
style,
|
style,
|
||||||
containerRef,
|
containerRef,
|
||||||
children,
|
children,
|
||||||
|
}: AnimatedHeightRevealProps): JSX.Element => {
|
||||||
|
if (!animate && !className && !style && !containerRef) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedHeightRevealInner
|
||||||
|
animate={animate}
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
containerRef={containerRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AnimatedHeightRevealInner>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnimatedHeightRevealInner = ({
|
||||||
|
animate,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
containerRef,
|
||||||
|
children,
|
||||||
}: AnimatedHeightRevealProps): JSX.Element => {
|
}: AnimatedHeightRevealProps): JSX.Element => {
|
||||||
const [shouldAnimateOnMount] = useState(() => Boolean(animate));
|
const [shouldAnimateOnMount] = useState(() => Boolean(animate));
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -363,8 +363,9 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
// path for short lists and only switches to the windowed path once
|
// path for short lists and only switches to the windowed path once
|
||||||
// the row count crosses its internal threshold.
|
// the row count crosses its internal threshold.
|
||||||
virtualizationEnabled: true,
|
virtualizationEnabled: true,
|
||||||
|
virtualizationRowThreshold: position === 'sidebar' ? 24 : undefined,
|
||||||
};
|
};
|
||||||
}, [activeScrollContainerRef]);
|
}, [activeScrollContainerRef, position]);
|
||||||
const handleExpandContent = useCallback(() => {
|
const handleExpandContent = useCallback(() => {
|
||||||
// no-op: user is reading expanded content, not composing
|
// no-op: user is reading expanded content, not composing
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -1470,6 +1470,25 @@ a[href],
|
||||||
--row-zebra-bg: color-mix(in srgb, var(--color-surface-raised) 99%, var(--color-text) 1%);
|
--row-zebra-bg: color-mix(in srgb, var(--color-surface-raised) 99%, var(--color-text) 1%);
|
||||||
--row-zebra-hover-bg: color-mix(in srgb, var(--row-zebra-bg) 97%, var(--color-text) 3%);
|
--row-zebra-hover-bg: color-mix(in srgb, var(--row-zebra-bg) 97%, var(--color-text) 3%);
|
||||||
background-color: var(--row-card-bg);
|
background-color: var(--row-card-bg);
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-row-zebra-card {
|
||||||
|
contain-intrinsic-size: auto 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-row-zebra-card {
|
||||||
|
contain-intrinsic-size: auto 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-card {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-task-item {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-row-zebra-card:hover,
|
.team-row-zebra-card:hover,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue