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).
This commit is contained in:
Bohdan Triapitsyn
2026-04-30 22:41:33 +03:00
parent 24533dfe32
commit 63da6e6292
57 changed files with 305 additions and 314 deletions
@@ -38,36 +38,47 @@ export const MultiRunWindow: React.FC<MultiRunWindowProps> = ({
}}
>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
<Dialog.Popup
aria-describedby={descriptionId}
<Dialog.Backdrop
className={cn(
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
'w-[90vw] max-w-[720px] h-[680px] max-h-[85vh]',
'flex flex-col rounded-xl border shadow-none overflow-hidden',
'bg-background'
'fixed inset-0 z-50 bg-black/50 dark:bg-black/75',
'transition-opacity duration-150 ease-out',
'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',
)}
>
<div className="absolute right-0.5 top-0.5 z-50">
<button
type="button"
onClick={() => onOpenChange(false)}
aria-label={t('multiRun.window.actions.closeAria')}
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiCloseLine className="h-5 w-5" />
</button>
</div>
<Dialog.Description id={descriptionId} className="sr-only">
{t('multiRun.window.description')}
</Dialog.Description>
<MultiRunLauncher
initialPrompt={initialPrompt}
onCreated={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
isWindowed
/>
</Dialog.Popup>
/>
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
<Dialog.Popup
aria-describedby={descriptionId}
className={cn(
'relative pointer-events-auto',
'w-[90vw] max-w-[720px] h-[680px] max-h-[85vh]',
'flex flex-col rounded-xl border shadow-none overflow-hidden origin-center',
'bg-background',
'transition-all duration-150 ease-out',
'data-[starting-style]:opacity-0 data-[starting-style]:scale-[0.98]',
'data-[ending-style]:opacity-0 data-[ending-style]:scale-[0.98]',
)}
>
<div className="absolute right-0.5 top-0.5 z-50">
<button
type="button"
onClick={() => onOpenChange(false)}
aria-label={t('multiRun.window.actions.closeAria')}
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiCloseLine className="h-5 w-5" />
</button>
</div>
<Dialog.Description id={descriptionId} className="sr-only">
{t('multiRun.window.description')}
</Dialog.Description>
<MultiRunLauncher
initialPrompt={initialPrompt}
onCreated={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
isWindowed
/>
</Dialog.Popup>
</div>
</Dialog.Portal>
</Dialog.Root>
);
@@ -628,7 +628,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
{resolvedPath ? (
<div className="flex items-center gap-1">
<DropdownMenu>
<Tooltip delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
@@ -657,7 +657,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<Tooltip delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
@@ -27,7 +27,7 @@ export const PreviewToggleButton: React.FC<PreviewToggleButtonProps> = ({
const tooltipText = isPreview ? 'Edit' : 'Preview';
return (
<Tooltip delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -554,7 +554,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
if (!Icon) return null;
return (
<Tooltip key={page.slug} delayDuration={600}>
<Tooltip key={page.slug}>
<TooltipTrigger asChild>
<button
type="button"
@@ -587,7 +587,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
<div className="overflow-hidden transition-opacity duration-150 opacity-100">
<div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5">
{!runtimeCtx.isVSCode && (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
@@ -36,21 +36,32 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
}}
>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 dark:bg-black/75" />
<Dialog.Popup
aria-describedby={descriptionId}
<Dialog.Backdrop
className={cn(
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
'w-[90vw] max-w-[1200px] h-[85vh] max-h-[900px]',
'rounded-xl border shadow-none overflow-hidden',
'bg-background'
'fixed inset-0 z-50 bg-black/50 dark:bg-black/75',
'transition-opacity duration-150 ease-out',
'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',
)}
>
<Dialog.Description id={descriptionId} className="sr-only">
{t('settings.window.description')}
</Dialog.Description>
<SettingsView onClose={() => onOpenChange(false)} isWindowed />
</Dialog.Popup>
/>
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
<Dialog.Popup
aria-describedby={descriptionId}
className={cn(
'relative pointer-events-auto',
'w-[90vw] max-w-[1200px] h-[85vh] max-h-[900px]',
'rounded-xl border shadow-none overflow-hidden origin-center',
'bg-background',
'transition-all duration-150 ease-out',
'data-[starting-style]:opacity-0 data-[starting-style]:scale-[0.98]',
'data-[ending-style]:opacity-0 data-[ending-style]:scale-[0.98]',
)}
>
<Dialog.Description id={descriptionId} className="sr-only">
{t('settings.window.description')}
</Dialog.Description>
<SettingsView onClose={() => onOpenChange(false)} isWindowed />
</Dialog.Popup>
</div>
</Dialog.Portal>
</Dialog.Root>
);
@@ -26,7 +26,7 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
<div className="space-y-2 rounded-xl border border-border/60 bg-transparent px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="typography-micro text-muted-foreground">{t('gitView.commit.aiHighlights.title')}</p>
<Tooltip delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -414,7 +414,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
return (
<>
<Tooltip delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -40,7 +40,6 @@ interface BranchSelectorProps {
onCreate: (name: string, remote?: GitRemote) => Promise<void>;
remotes?: GitRemote[];
disabled?: boolean;
tooltipDelayMs?: number;
}
const sanitizeBranchNameInput = (value: string): string => {
@@ -65,7 +64,6 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
onCreate,
remotes = [],
disabled = false,
tooltipDelayMs = 1000,
}) => {
const { t } = useI18n();
const [isOpen, setIsOpen] = React.useState(false);
@@ -170,7 +168,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<Tooltip delayDuration={tooltipDelayMs}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
@@ -150,7 +150,7 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
<span className="text-muted-foreground mx-0.5">/</span>
<span style={{ color: 'var(--status-error)' }}>-{deletions}</span>
</span>
<Tooltip delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
@@ -104,7 +104,6 @@ interface IdentityDropdownProps {
identities: GitIdentityProfile[];
onSelect: (profile: GitIdentityProfile) => void;
isApplying: boolean;
tooltipDelayMs?: number;
iconOnly?: boolean;
}
@@ -113,7 +112,6 @@ const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
identities,
onSelect,
isApplying,
tooltipDelayMs = 1000,
iconOnly = false,
}) => {
const { t } = useI18n();
@@ -121,7 +119,7 @@ const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
return (
<DropdownMenu>
<Tooltip delayDuration={tooltipDelayMs}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
@@ -224,7 +222,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
const managementButtons = (
<div className="flex items-center gap-1 shrink-0">
{onOpenHistory ? (
<Tooltip delayDuration={useTwoRowHeader ? 300 : 1000}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -252,7 +250,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
removingRemoteName={removingRemoteName}
disabled={!status}
iconOnly={true}
tooltipDelayMs={useTwoRowHeader ? 300 : 1000}
aheadCount={status.ahead}
behindCount={status.behind}
/>
@@ -264,7 +262,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
identities={availableIdentities}
onSelect={onSelectIdentity}
isApplying={isApplyingIdentity}
tooltipDelayMs={useTwoRowHeader ? 300 : 1000}
iconOnly={false}
/>
);
@@ -287,7 +285,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
onCheckout={onCheckoutBranch}
onCreate={onCreateBranch}
remotes={remotes}
tooltipDelayMs={useTwoRowHeader ? 300 : 1000}
/>
)}
</div>
@@ -88,7 +88,7 @@ export const HistoryCommitRow = React.memo(({
<code className="shrink-0 font-mono">
{entry.hash.slice(0, 8)}
</code>
<Tooltip delayDuration={500}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -1408,7 +1408,7 @@ export const PullRequestSection: React.FC<{
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
{pr ? (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
@@ -1431,7 +1431,7 @@ export const PullRequestSection: React.FC<{
</div>
<div className="flex items-center gap-2">
{isLoading ? <RiLoader4Line className="size-4 animate-spin text-muted-foreground" /> : null}
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
@@ -1612,7 +1612,7 @@ export const PullRequestSection: React.FC<{
{pr.state === 'open' ? (
isEditingPr ? (
<>
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1631,7 +1631,7 @@ export const PullRequestSection: React.FC<{
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.cancelEditing')}</p></TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
@@ -1647,7 +1647,7 @@ export const PullRequestSection: React.FC<{
</Tooltip>
</>
) : (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1665,7 +1665,7 @@ export const PullRequestSection: React.FC<{
) : null}
{checks ? (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1683,7 +1683,7 @@ export const PullRequestSection: React.FC<{
) : null}
{checks?.failure ? (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1699,7 +1699,7 @@ export const PullRequestSection: React.FC<{
</Tooltip>
) : null}
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1714,7 +1714,7 @@ export const PullRequestSection: React.FC<{
<TooltipContent><p>{t('gitView.pr.actions.openComments')}</p></TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1730,7 +1730,7 @@ export const PullRequestSection: React.FC<{
</Tooltip>
{canMerge && pr.draft && pr.state === 'open' ? (
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
@@ -1765,7 +1765,7 @@ export const PullRequestSection: React.FC<{
<SelectItem value="rebase">{t('gitView.pr.mergeMethod.rebase')}</SelectItem>
</SelectContent>
</Select>
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
@@ -2064,7 +2064,7 @@ export const PullRequestSection: React.FC<{
{comment.authorLogin && comment.authorLogin !== comment.authorName ? ` · @${comment.authorLogin}` : ''}
</span>
{comment.createdAt ? <span className="whitespace-nowrap">{formatTimestamp(comment.createdAt)}</span> : null}
<Tooltip delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
@@ -29,7 +29,6 @@ interface SyncActionsProps {
disabled: boolean;
removingRemoteName?: string | null;
iconOnly?: boolean;
tooltipDelayMs?: number;
aheadCount?: number;
behindCount?: number;
}
@@ -44,7 +43,6 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
disabled,
removingRemoteName = null,
iconOnly = false,
tooltipDelayMs = 1000,
aheadCount = 0,
behindCount = 0,
}) => {
@@ -108,7 +106,7 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
);
return (
<Tooltip delayDuration={tooltipDelayMs}>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent sideOffset={8}>{tooltipText}</TooltipContent>
</Tooltip>
@@ -126,7 +124,7 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
) => {
return (
<DropdownMenu>
<Tooltip delayDuration={tooltipDelayMs}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button