Files
openchamber/packages/ui/src/components/views/git/ChangeRow.tsx
T
Bohdan Triapitsyn 63da6e6292 feat(ui): unify overlay animations and trim tooltip delays
Use base-ui's transition-status pattern (data-starting-style /
data-ending-style) for Dialog, DropdownMenu, Select, Tooltip and Popover
so open/close animate consistently at 150ms ease-out without flicker.
Wrap dialog popups in a centered flex container so the scale animation
no longer fights with translate-based positioning, and switch
PendingChangesBar to a real Popover so it actually animates closed.

Convert ScheduledTaskEditorDialog from raw Radix to the shared Dialog
wrapper, give ScheduledTasksDialog a stable min-height to prevent layout
shift mid-animation, and reset NewWorktreeDialog form state on open
instead of close so fields don't empty during the close transition.

Drop tooltip delay from 700ms to 300ms globally and remove all
per-component overrides except the model/variant/agent selectors in the
chat input (600ms).
2026-04-30 22:41:33 +03:00

174 lines
6.0 KiB
TypeScript

import React, { useCallback, useMemo } from 'react';
import {
RiArrowGoBackLine,
RiLoader4Line,
} from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Checkbox } from '@/components/ui/checkbox';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import type { GitStatus } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
type ChangeDescriptor = {
code: string;
color: string;
description: string;
};
const CHANGE_DESCRIPTORS: Record<string, ChangeDescriptor> = {
'?': { code: '?', color: 'var(--status-info)', description: 'Untracked file' },
A: { code: 'A', color: 'var(--status-success)', description: 'New file' },
D: { code: 'D', color: 'var(--status-error)', description: 'Deleted file' },
R: { code: 'R', color: 'var(--status-info)', description: 'Renamed file' },
C: { code: 'C', color: 'var(--status-info)', description: 'Copied file' },
M: { code: 'M', color: 'var(--status-warning)', description: 'Modified file' },
};
const DEFAULT_DESCRIPTOR = CHANGE_DESCRIPTORS.M;
function getChangeSymbol(file: GitStatus['files'][number]): string {
const indexCode = file.index?.trim();
const workingCode = file.working_dir?.trim();
if (indexCode && indexCode !== '?') return indexCode.charAt(0);
if (workingCode) return workingCode.charAt(0);
return indexCode?.charAt(0) || workingCode?.charAt(0) || 'M';
}
function describeChange(file: GitStatus['files'][number]): ChangeDescriptor {
const symbol = getChangeSymbol(file);
return CHANGE_DESCRIPTORS[symbol] ?? DEFAULT_DESCRIPTOR;
}
interface ChangeRowProps {
file: GitStatus['files'][number];
checked: boolean;
onToggle: () => void;
onViewDiff: () => void;
onRevert: () => void;
isReverting: boolean;
stats?: { insertions: number; deletions: number };
rowPaddingClassName?: string;
indentPx?: number;
}
export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
file,
checked,
onToggle,
onViewDiff,
onRevert,
isReverting,
stats,
rowPaddingClassName,
indentPx = 0,
}) {
const descriptor = useMemo(() => describeChange(file), [file]);
const { t } = useI18n();
const indicatorLabel = descriptor.description;
const insertions = stats?.insertions ?? 0;
const deletions = stats?.deletions ?? 0;
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === ' ') {
event.preventDefault();
onToggle();
} else if (event.key === 'Enter') {
event.preventDefault();
onViewDiff();
}
},
[onToggle, onViewDiff]
);
const handleRevertClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onRevert();
},
[onRevert]
);
return (
<div
className={`group flex items-center gap-2 py-1.5 hover:bg-sidebar/40 cursor-pointer ${rowPaddingClassName ?? 'px-3'}`}
role="button"
tabIndex={0}
onClick={onViewDiff}
onKeyDown={handleKeyDown}
style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined}
>
<div className="flex size-5 shrink-0 items-center justify-center" onClick={(e) => { e.stopPropagation(); }}>
<Checkbox
size="sm"
checked={checked}
onChange={() => onToggle()}
ariaLabel={t('gitView.changes.selectFileAria', { path: file.path })}
/>
</div>
<span
className="typography-micro font-semibold w-4 text-center uppercase"
style={{ color: descriptor.color }}
title={indicatorLabel}
aria-label={indicatorLabel}
>
{descriptor.code}
</span>
<FileTypeIcon filePath={file.path} className="h-3.5 w-3.5 shrink-0" />
{(() => {
const lastSlash = file.path.lastIndexOf('/');
if (lastSlash === -1) {
return (
<span
className="flex-1 min-w-0 truncate typography-ui-label text-foreground"
style={{ direction: 'rtl', textAlign: 'left', unicodeBidi: 'plaintext' }}
title={file.path}
>
{file.path}
</span>
);
}
const dir = file.path.slice(0, lastSlash);
const name = file.path.slice(lastSlash);
return (
<span className="flex-1 min-w-0 flex items-baseline overflow-hidden" title={file.path}>
<span
className="min-w-0 truncate typography-ui-label text-muted-foreground"
style={{ direction: 'rtl', textAlign: 'left', unicodeBidi: 'plaintext' }}
>
{dir}
</span>
<span className="flex-shrink-0 typography-ui-label"><span className="text-muted-foreground">/</span><span className="text-foreground">{name.slice(1)}</span></span>
</span>
);
})()}
<span className="shrink-0 typography-micro">
<span style={{ color: 'var(--status-success)' }}>+{insertions}</span>
<span className="text-muted-foreground mx-0.5">/</span>
<span style={{ color: 'var(--status-error)' }}>-{deletions}</span>
</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleRevertClick}
disabled={isReverting}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
aria-label={t('gitView.changes.revertFileAria', { path: file.path })}
>
{isReverting ? (
<RiLoader4Line className="size-3.5 animate-spin" />
) : (
<RiArrowGoBackLine className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>{t('gitView.changes.revertFileTooltip')}</TooltipContent>
</Tooltip>
</div>
);
});