perf(ui): migrate icons to SVG sprite system

Replace @remixicon/react with a shared Icon component that renders
via <use href> references to a single hidden SVG sprite. This reduces
DOM node count by replacing inline SVGs with lightweight references.

- Create Icon component with sprite injection (packages/ui/src/components/icon/)
- Migrate all 164 files from @remixicon/react to Icon component
- Auto-generate sprite data from remixicon bundle (scripts/generate-icon-sprite.mjs)
- Add bun run icons:generate to package.json
- Move @remixicon/react to devDependencies
- Add icon usage instructions to theme-system skill
This commit is contained in:
Bohdan Triapitsyn
2026-05-13 13:26:35 +03:00
parent b4cbd7f0d6
commit 14357257ae
173 changed files with 2342 additions and 2227 deletions
@@ -1,7 +1,7 @@
import React from 'react';
import { RiArrowDownLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface AIHighlightsBoxProps {
@@ -35,7 +35,7 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
onClick={handleInsert}
aria-label={t('gitView.commit.aiHighlights.insertAria')}
>
<RiArrowDownLine className="size-4" />
<Icon name="arrow-down" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>
@@ -1,12 +1,4 @@
import React from 'react';
import {
RiGitMergeLine,
RiGitBranchLine,
RiLoader4Line,
RiArrowDownSLine,
RiCheckLine,
RiCloseLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -31,6 +23,7 @@ import {
CommandSeparator,
} from '@/components/ui/command';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
@@ -188,13 +181,13 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
<div key={index} className="flex items-start gap-2">
<div className="mt-0.5 shrink-0">
{log.status === 'running' && (
<RiLoader4Line className="size-3.5 animate-spin text-primary" />
<Icon name="loader-4" className="size-3.5 animate-spin text-primary" />
)}
{log.status === 'done' && (
<RiCheckLine className="size-3.5 text-success" />
<Icon name="check" className="size-3.5 text-success" />
)}
{log.status === 'error' && (
<RiCloseLine className="size-3.5 text-destructive" />
<Icon name="close" className="size-3.5 text-destructive" />
)}
{log.status === 'pending' && (
<div className="size-3.5 rounded-full border border-muted-foreground/30" />
@@ -251,7 +244,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
)}
>
<div className="flex items-center gap-2">
<RiGitMergeLine
<Icon name="git-merge"
className={cn('size-4', operation === 'merge' ? 'text-primary' : 'text-muted-foreground')}
/>
<span
@@ -279,7 +272,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
)}
>
<div className="flex items-center gap-2">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('size-4', operation === 'rebase' ? 'text-primary' : 'text-muted-foreground')}
/>
<span
@@ -311,7 +304,7 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
<span className={cn('truncate', !selectedBranch && 'text-muted-foreground')}>
{selectedBranch || t('gitView.branch.selectBranch')}
</span>
<RiArrowDownSLine className="size-4 opacity-60 shrink-0" />
<Icon name="arrow-down-s" className="size-4 opacity-60 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -388,12 +381,12 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
>
{operation === 'merge' ? (
<>
<RiGitMergeLine className="size-4" />
<Icon name="git-merge" className="size-4" />
{t('gitView.operation.merge')}
</>
) : (
<>
<RiGitBranchLine className="size-4" />
<Icon name="git-branch" className="size-4" />
{t('gitView.operation.rebase')}
</>
)}
@@ -444,9 +437,9 @@ export const BranchIntegrationSection: React.FC<BranchIntegrationSectionProps> =
disabled={isDisabled}
>
{isOperating ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiGitMergeLine className="size-4" />
<Icon name="git-merge" className="size-4" />
)}
<span>{t('gitView.branch.mergeRebase')}</span>
</Button>
@@ -1,12 +1,4 @@
import React from 'react';
import {
RiGitBranchLine,
RiArrowDownSLine,
RiAddLine,
RiCloseLine,
RiLoader4Line,
RiArrowLeftLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -23,6 +15,7 @@ import {
CommandSeparator,
} from '@/components/ui/command';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import type { GitRemote } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
@@ -177,11 +170,11 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
className="h-8 min-w-0 max-w-full justify-start gap-1.5 px-2 py-1"
disabled={disabled}
>
<RiGitBranchLine className="size-4 text-primary" />
<Icon name="git-branch" className="size-4 text-primary" />
<span className="min-w-0 truncate font-medium text-left">
{currentBranch || t('gitView.branch.detachedHead')}
</span>
<RiArrowDownSLine className="size-4 opacity-60" />
<Icon name="arrow-down-s" className="size-4 opacity-60" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -214,7 +207,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
disabled={isCreating}
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
>
<RiArrowLeftLine className="size-4" />
<Icon name="arrow-left" className="size-4" />
</button>
<span className="typography-meta text-muted-foreground">
{t('gitView.branch.pushToPrefix')} <span className="text-foreground font-medium">{sanitizedNewBranch}</span> {t('gitView.branch.pushToSuffix')}
@@ -231,7 +224,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
>
<span className="typography-ui-label text-foreground">
{isCreating ? (
<RiLoader4Line className="inline size-3 mr-1.5 animate-spin" />
<Icon name="loader-4" className="inline size-3 mr-1.5 animate-spin" />
) : null}
{remote.name}
</span>
@@ -244,7 +237,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
</div>
) : !showCreate ? (
<CommandItem onSelect={handleShowCreate}>
<RiAddLine className="size-4" />
<Icon name="add" className="size-4" />
<span>{t('gitView.branch.create')}</span>
</CommandItem>
) : (
@@ -274,9 +267,9 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
>
{isCreating ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiAddLine className="size-4" />
<Icon name="add" className="size-4" />
)}
</button>
<button
@@ -285,7 +278,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
disabled={isCreating}
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</div>
)}
@@ -1,11 +1,8 @@
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 { Icon } from "@/components/icon/Icon";
import type { GitStatus } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
@@ -160,9 +157,9 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
aria-label={t('gitView.changes.revertFileAria', { path: file.path })}
>
{isReverting ? (
<RiLoader4Line className="size-3.5 animate-spin" />
<Icon name="loader-4" className="size-3.5 animate-spin" />
) : (
<RiArrowGoBackLine className="size-3.5" />
<Icon name="arrow-go-back" className="size-3.5" />
)}
</button>
</TooltipTrigger>
@@ -1,6 +1,5 @@
import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { RiArchiveStackLine, RiFolder3Fill, RiFolderOpenFill } from '@remixicon/react';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import {
@@ -13,6 +12,7 @@ import {
} from '@/components/ui/dialog';
import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar';
import { Icon } from "@/components/icon/Icon";
import { ChangeRow } from './ChangeRow';
import type { GitStatus } from '@/lib/api/types';
import { cn } from '@/lib/utils';
@@ -405,9 +405,9 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
: t('gitView.changes.expandDirectoryAria', { path: directory.path })}
>
{isExpanded ? (
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
<Icon name="folder-open-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
) : (
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
<Icon name="folder-3-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
)}
<span className="min-w-0 flex-1 truncate typography-ui-label text-foreground" title={directory.path}>
{directory.name}
@@ -475,7 +475,7 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
aria-label={t('gitView.stashes.title')}
title={t('gitView.stashes.title')}
>
<RiArchiveStackLine className="size-4" />
<Icon name="archive-stack" className="size-4" />
</Button>
) : null}
</div>
@@ -1,15 +1,9 @@
import {
RiGitCommitLine,
RiArrowUpLine,
RiAiGenerate2,
RiLoader4Line,
RiEmotionHappyLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { CommitInput } from './CommitInput';
import { AIHighlightsBox } from './AIHighlightsBox';
import { useDeviceInfo } from '@/lib/device';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
type CommitAction = 'commit' | 'commitAndPush' | null;
@@ -87,7 +81,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
className="w-fit"
type="button"
>
<RiEmotionHappyLine className="size-4" />
<Icon name="emotion-happy" className="size-4" />
{t('gitView.commit.addGitmoji')}
</Button>
)}
@@ -107,9 +101,9 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
className="commit-actions__btn"
>
{isGeneratingMessage ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiAiGenerate2 className="size-4 text-primary" />
<Icon name="ai-generate-2" className="size-4 text-primary" />
)}
<span className="commit-actions__label">{t('gitView.commit.generate')}</span>
</Button>
@@ -126,12 +120,12 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
>
{commitAction === 'commit' ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
<span className="commit-actions__label">{t('gitView.commit.committing')}</span>
</>
) : (
<>
<RiGitCommitLine className="size-4" />
<Icon name="git-commit" className="size-4" />
<span className="commit-actions__label">{t('gitView.commit.commit')}</span>
</>
)}
@@ -149,9 +143,9 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
aria-label={t('gitView.commit.pushAria')}
>
{commitAction === 'commitAndPush' ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiArrowUpLine className="size-3.5" />
<Icon name="arrow-up" className="size-3.5" />
)}
</Button>
</TooltipTrigger>
@@ -170,12 +164,12 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
>
{commitAction === 'commitAndPush' ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
<span className="commit-actions__label">{t('gitView.commit.pushing')}</span>
</>
) : (
<>
<RiArrowUpLine className="size-3.5" />
<Icon name="arrow-up" className="size-3.5" />
<span className="commit-actions__label">{t('gitView.commit.push')}</span>
</>
)}
@@ -8,11 +8,11 @@ import {
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { RiAlertLine, RiLoader4Line, RiChat1Line, RiAddLine } from '@remixicon/react';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { getConflictDetails, type MergeConflictDetails } from '@/lib/gitApi';
import { renderMagicPrompt } from '@/lib/magicPrompts';
import { useI18n } from '@/lib/i18n';
@@ -173,7 +173,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
<div className="flex flex-col gap-4 overflow-hidden">
<DialogHeader>
<div className="flex items-center gap-2">
<RiAlertLine className="size-5 shrink-0 text-[var(--status-warning)]" />
<Icon name="alert" className="size-5 shrink-0 text-[var(--status-warning)]" />
<DialogTitle>{t('gitView.conflict.detectedTitle', { operation: operationLabel })}</DialogTitle>
</div>
<DialogDescription>
@@ -183,7 +183,7 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
{isLoading && (
<div className="flex items-center justify-center gap-2 py-4 text-muted-foreground">
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
<span className="typography-meta">{t('gitView.conflict.loading')}</span>
</div>
)}
@@ -237,9 +237,9 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
className="w-full gap-2"
>
{isLoading ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiAddLine className="size-4" />
<Icon name="add" className="size-4" />
)}
{t('gitView.conflict.resolveNewSession')}
</Button>
@@ -250,9 +250,9 @@ export const ConflictDialog: React.FC<ConflictDialogProps> = ({
className="w-full gap-2"
>
{isLoading ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiChat1Line className="size-4" />
<Icon name="chat-1" className="size-4" />
)}
{t('gitView.conflict.resolveCurrentSession')}
</Button>
@@ -1,6 +1,6 @@
import React from 'react';
import { RiArchiveStackLine, RiGitCommitLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface GitEmptyStateProps {
@@ -11,7 +11,7 @@ export const GitEmptyState: React.FC<GitEmptyStateProps> = ({ onOpenStashes }) =
const { t } = useI18n();
return (
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
<RiGitCommitLine className="size-10 text-muted-foreground/70 mb-4" />
<Icon name="git-commit" className="size-10 text-muted-foreground/70 mb-4" />
<p className="typography-ui-label font-semibold text-foreground mb-1">
{t('gitView.empty.cleanTitle')}
</p>
@@ -27,7 +27,7 @@ export const GitEmptyState: React.FC<GitEmptyStateProps> = ({ onOpenStashes }) =
onClick={onOpenStashes}
className="gap-1.5"
>
<RiArchiveStackLine className="size-4" />
<Icon name="archive-stack" className="size-4" />
{t('gitView.stashes.title')}
</Button>
) : null}
@@ -1,17 +1,4 @@
import React from 'react';
import {
RiArrowDownSLine,
RiCheckLine,
RiLoader4Line,
RiGitBranchLine,
RiBriefcaseLine,
RiHomeLine,
RiGraduationCapLine,
RiCodeLine,
RiHeartLine,
RiHistoryLine,
RiUser3Line,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip';
import {
@@ -21,6 +8,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { BranchSelector } from './BranchSelector';
import { WorktreeBranchDisplay } from './WorktreeBranchDisplay';
import { SyncActions } from './SyncActions';
@@ -54,17 +43,14 @@ interface GitHeaderProps {
onSelectActionTab?: (tabID: string) => void;
}
const IDENTITY_ICON_MAP: Record<
string,
React.ComponentType<React.ComponentProps<typeof RiGitBranchLine>>
> = {
branch: RiGitBranchLine,
briefcase: RiBriefcaseLine,
house: RiHomeLine,
graduation: RiGraduationCapLine,
code: RiCodeLine,
heart: RiHeartLine,
user: RiUser3Line,
const IDENTITY_ICON_MAP: Record<string, IconName> = {
branch: 'git-branch',
briefcase: 'briefcase',
house: 'home',
graduation: 'graduation-cap',
code: 'code',
heart: 'heart',
user: 'user-3',
};
const IDENTITY_COLOR_MAP: Record<string, string> = {
@@ -92,9 +78,10 @@ interface IdentityIconProps {
}
const IdentityIcon: React.FC<IdentityIconProps> = ({ icon, className, colorToken }) => {
const IconComponent = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? RiUser3Line;
const iconName = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? 'user-3';
return (
<IconComponent
<Icon
name={iconName}
className={className}
style={{ color: getIdentityColor(colorToken) }}
/>
@@ -132,7 +119,7 @@ export const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
disabled={isDisabled}
>
{isApplying ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<IdentityIcon
icon={activeProfile?.icon}
@@ -145,7 +132,7 @@ export const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
{activeProfile?.name || t('gitView.header.noIdentity')}
</span>
)}
<RiArrowDownSLine className="size-4 opacity-60" />
<Icon name="arrow-down-s" className="size-4 opacity-60" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -178,7 +165,7 @@ export const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
</span>
</span>
{isSelected ? (
<RiCheckLine className="ml-auto size-4 text-foreground" />
<Icon name="check" className="ml-auto size-4 text-foreground" />
) : null}
</span>
</DropdownMenuItem>
@@ -230,7 +217,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
className="h-8 w-8 px-0"
onClick={onOpenHistory}
>
<RiHistoryLine className="size-4" />
<Icon name="history" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>{t('gitView.history.title')}</TooltipContent>
@@ -1,7 +1,7 @@
import React from 'react';
import { RiLoader4Line, RiFileCopyLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
@@ -99,7 +99,7 @@ export const HistoryCommitRow = React.memo(({
onCopyHash(entry.hash);
}}
>
<RiFileCopyLine className="size-3" />
<Icon name="file-copy" className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={8}>{t('gitView.history.copySha')}</TooltipContent>
@@ -112,7 +112,7 @@ export const HistoryCommitRow = React.memo(({
<div className="px-3 pb-2 pl-8 border-t border-border/40">
{isLoadingFiles ? (
<div className="flex items-center gap-2 py-2">
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" />
<span className="typography-micro text-muted-foreground">{t('gitView.history.loadingFiles')}</span>
</div>
) : files.length === 0 ? (
@@ -1,5 +1,4 @@
import React from 'react';
import { RiArrowDownSLine, RiArrowUpLine, RiArrowUpSLine } from '@remixicon/react';
import {
Collapsible,
CollapsibleContent,
@@ -13,6 +12,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Icon } from "@/components/icon/Icon";
import { HistoryCommitRow } from './HistoryCommitRow';
import type { GitLogEntry, CommitFileEntry } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
@@ -78,8 +78,8 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
const bottomEntries = hasDivider ? log.all.slice(branchDivider.insertBeforeIndex) : [];
const dividerIcon = branchDivider?.direction === 'down'
? <RiArrowDownSLine className="size-3.5" />
: <RiArrowUpLine className="size-3.5" />;
? <Icon name="arrow-down-s" className="size-3.5" />
: <Icon name="arrow-up" className="size-3.5" />;
const renderCommitList = (entries: GitLogEntry[]) => (
<ul className="divide-y divide-border/60">
@@ -182,9 +182,9 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
</div>
)}
{isOpen ? (
<RiArrowUpSLine className="size-4 text-muted-foreground" />
<Icon name="arrow-up-s" className="size-4 text-muted-foreground" />
) : (
<RiArrowDownSLine className="size-4 text-muted-foreground" />
<Icon name="arrow-down-s" className="size-4 text-muted-foreground" />
)}
</div>
</CollapsibleTrigger>
@@ -1,13 +1,6 @@
import React from 'react';
import {
RiGitMergeLine,
RiGitBranchLine,
RiLoader4Line,
RiCheckLine,
RiCloseLine,
RiSparklingLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import type { GitMergeInProgress, GitRebaseInProgress } from '@/lib/api/types';
import { useI18n } from '@/lib/i18n';
@@ -63,7 +56,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
const isProcessing = processingAction !== null;
const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase');
const OperationIcon = operation === 'merge' ? RiGitMergeLine : RiGitBranchLine;
const operationIconName = operation === 'merge' ? 'git-merge' : 'git-branch';
// Build description
let description = '';
@@ -81,7 +74,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
<div className="bg-[var(--status-warning-bg)] border border-[var(--status-warning)] rounded-lg p-3 mx-3 mt-3">
<div className="flex flex-col gap-3">
<div className="flex items-start gap-2 min-w-0">
<OperationIcon className="size-4 text-[var(--status-warning)] shrink-0" />
<Icon name={operationIconName} className="size-4 text-[var(--status-warning)] shrink-0" />
<div className="min-w-0">
<p className="typography-label text-[var(--status-warning)]">
{t('gitView.operation.inProgressTitle', { operation: operationLabel })}
@@ -103,7 +96,7 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
disabled={isProcessing || isLoading}
className="gap-1.5"
>
<RiSparklingLine className="size-4" />
<Icon name="sparkling" className="size-4" />
{t('gitView.operation.resolveWithAi')}
</Button>
)}
@@ -117,9 +110,9 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
className="gap-1.5"
>
{processingAction === 'abort' ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
)}
{t('gitView.operation.abort')}
</Button>
@@ -134,9 +127,9 @@ export const InProgressOperationBanner: React.FC<InProgressOperationBannerProps>
className="gap-1.5"
>
{processingAction === 'continue' ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
)}
{t('gitView.operation.continue')}
</Button>
@@ -1,5 +1,4 @@
import * as React from 'react';
import { RiArrowDownSLine, RiLoader4Line, RiSplitCellsHorizontal, RiSparklingLine } from '@remixicon/react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -15,6 +14,7 @@ import {
CommandList,
} from '@/components/ui/command';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { useUIStore } from '@/stores/useUIStore';
@@ -344,7 +344,7 @@ export const IntegrateCommitsSection: React.FC<{
<section className={containerClassName}>
<div className={headerClassName}>
<div className="flex items-center gap-2 min-w-0">
<RiSplitCellsHorizontal className="size-4 text-muted-foreground" />
<Icon name="split-cells-horizontal" className="size-4 text-muted-foreground" />
<h3 className="typography-ui-header font-semibold text-foreground truncate">{t('gitView.integrate.title')}</h3>
{ui.kind === 'ready' && ui.plan.commits.length > 0 ? (
<span className="typography-meta text-muted-foreground truncate">
@@ -354,7 +354,7 @@ export const IntegrateCommitsSection: React.FC<{
</div>
<div className="flex items-center gap-2">
{ui.kind === 'loading' || ui.kind === 'running' ? (
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
<Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" />
) : null}
</div>
</div>
@@ -375,7 +375,7 @@ export const IntegrateCommitsSection: React.FC<{
<Button variant="outline" size="sm" className="gap-1.5">
{t('gitView.integrate.target')}
<span className="max-w-[160px] truncate font-mono text-xs text-muted-foreground">{targetBranch}</span>
<RiArrowDownSLine className="size-4 opacity-60" />
<Icon name="arrow-down-s" className="size-4 opacity-60" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -495,7 +495,7 @@ export const IntegrateCommitsSection: React.FC<{
disabled={!currentSessionId}
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, false)}
>
<RiSparklingLine className="size-3.5" />
<Icon name="sparkling" className="size-3.5" />
{t('gitView.integrate.currentSession')}
</Button>
<Button
@@ -504,7 +504,7 @@ export const IntegrateCommitsSection: React.FC<{
className="typography-meta gap-1"
onClick={() => void handleResolveWithAi({ state: ui.state, details: ui.details }, true)}
>
<RiSparklingLine className="size-3.5" />
<Icon name="sparkling" className="size-3.5" />
{t('gitView.integrate.newSession')}
</Button>
<Button size="sm" className="typography-meta" onClick={() => void handleContinue()}>
@@ -1,23 +1,4 @@
import React from 'react';
import {
RiChat4Line,
RiCheckLine,
RiCheckboxCircleLine,
RiAiGenerate2,
RiArrowDownSLine,
RiArrowRightSLine,
RiCloseLine,
RiEditLine,
RiErrorWarningLine,
RiExternalLinkLine,
RiGitClosePullRequestLine,
RiGitMergeLine,
RiGitPrDraftLine,
RiGitPullRequestLine,
RiInformationLine,
RiLoader4Line,
RiRefreshLine,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
@@ -45,6 +26,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useDeviceInfo } from '@/lib/device';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { Icon } from "@/components/icon/Icon";
import { useUIStore } from '@/stores/useUIStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store';
@@ -770,7 +752,7 @@ export const PullRequestSection: React.FC<{
{run.detailsUrl ? (
<Button variant="outline" size="sm" asChild className="flex-shrink-0">
<a href={run.detailsUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
<Icon name="external-link" className="size-4" />
Open
</a>
</Button>
@@ -859,7 +841,7 @@ export const PullRequestSection: React.FC<{
(isFail ? 'bg-destructive/10 text-destructive' : 'text-muted-foreground')
}
>
{stepExpanded ? <RiArrowDownSLine className="size-4" /> : <RiArrowRightSLine className="size-4" />}
{stepExpanded ? <Icon name="arrow-down-s" className="size-4" /> : <Icon name="arrow-right-s" className="size-4" />}
<span className="truncate">{step.name}</span>
{step.conclusion ? <span className="ml-auto flex-shrink-0">{step.conclusion}</span> : null}
</button>
@@ -1385,13 +1367,13 @@ export const PullRequestSection: React.FC<{
const shouldShowConnectionNotice = githubAuthChecked && status?.connected === false;
const prVisualState = getPrVisualState(status);
const prColorVar = prVisualState ? `var(--pr-${prVisualState})` : 'var(--status-info)';
const PrStateIcon = prVisualState === 'draft'
? RiGitPrDraftLine
const prStateIconName = prVisualState === 'draft'
? 'git-pr-draft'
: prVisualState === 'merged'
? RiGitMergeLine
? 'git-merge'
: prVisualState === 'closed'
? RiGitClosePullRequestLine
: RiGitPullRequestLine;
? 'git-close-pull-request'
: 'git-pull-request';
const prStatusText = pr
? [
`${pr.state}${pr.draft ? ' (draft)' : ''}`,
@@ -1424,13 +1406,13 @@ export const PullRequestSection: React.FC<{
onClick={() => void openExternal(pr.url)}
aria-label={t('gitView.pr.actions.openOnGitHubAria')}
>
<PrStateIcon className="size-4 shrink-0" style={{ color: prColorVar }} />
<Icon name={prStateIconName} className="size-4 shrink-0" style={{ color: prColorVar }} />
</button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.openOnGitHub')}</p></TooltipContent>
</Tooltip>
) : (
<PrStateIcon className="size-4 shrink-0" style={{ color: 'var(--surface-muted-foreground)' }} />
<Icon name={prStateIconName} className="size-4 shrink-0" style={{ color: 'var(--surface-muted-foreground)' }} />
)}
<h3 className="typography-ui-header font-semibold text-foreground truncate">{t('gitView.pullRequest.title')}</h3>
{pr ? (
@@ -1438,7 +1420,7 @@ export const PullRequestSection: React.FC<{
) : null}
</div>
<div className="flex shrink-0 items-center gap-1">
{isLoading ? <RiLoader4Line className="size-4 animate-spin text-muted-foreground" /> : null}
{isLoading ? <Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" /> : null}
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -1448,7 +1430,7 @@ export const PullRequestSection: React.FC<{
onClick={() => void refresh({ force: true })}
aria-label={t('gitView.pr.actions.refreshAria')}
>
<RiRefreshLine className="size-3.5 text-muted-foreground" />
<Icon name="refresh" className="size-3.5 text-muted-foreground" />
</button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.refresh')}</p></TooltipContent>
@@ -1493,7 +1475,7 @@ export const PullRequestSection: React.FC<{
{repoUrl ? (
<Button variant="outline" size="sm" asChild className="w-fit">
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
<Icon name="external-link" className="size-4" />
Open Repo
</a>
</Button>
@@ -1503,7 +1485,7 @@ export const PullRequestSection: React.FC<{
{!pr && !isInitialStatusResolved && !error && !shouldShowConnectionNotice ? (
<div className="flex items-center gap-2 typography-micro text-muted-foreground">
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('gitView.pr.checkingStatus')}
</div>
) : pr ? (
@@ -1574,7 +1556,7 @@ export const PullRequestSection: React.FC<{
disabled={isUpdating}
aria-label={t('gitView.pr.actions.cancelEditingAria')}
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.cancelEditing')}</p></TooltipContent>
@@ -1588,7 +1570,7 @@ export const PullRequestSection: React.FC<{
disabled={isUpdating || !editTitle.trim()}
aria-label={t('gitView.pr.actions.savePrAria')}
>
{isUpdating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckLine className="size-4" />}
{isUpdating ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="check" className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.savePr')}</p></TooltipContent>
@@ -1604,7 +1586,7 @@ export const PullRequestSection: React.FC<{
onClick={() => setIsEditingPr(true)}
aria-label={t('gitView.pr.actions.editPrAria')}
>
<RiEditLine className="size-4" />
<Icon name="edit" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.editPr')}</p></TooltipContent>
@@ -1623,7 +1605,7 @@ export const PullRequestSection: React.FC<{
disabled={isLoadingCheckDetails}
aria-label={t('gitView.pr.actions.openChecksAria')}
>
{isLoadingCheckDetails ? <RiLoader4Line className="size-4 animate-spin" /> : <RiInformationLine className="size-4" />}
{isLoadingCheckDetails ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="information" className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.openChecks')}</p></TooltipContent>
@@ -1640,7 +1622,7 @@ export const PullRequestSection: React.FC<{
onClick={sendFailedChecksToChat}
aria-label={t('gitView.pr.actions.resolveFailedChecksAria')}
>
<RiErrorWarningLine className="size-4" />
<Icon name="error-warning" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.resolveFailedChecks')}</p></TooltipContent>
@@ -1656,7 +1638,7 @@ export const PullRequestSection: React.FC<{
onClick={openCommentsDialog}
aria-label={t('gitView.pr.actions.openCommentsAria')}
>
<RiChat4Line className="size-4" />
<Icon name="chat-4" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.openComments')}</p></TooltipContent>
@@ -1671,7 +1653,7 @@ export const PullRequestSection: React.FC<{
onClick={sendCommentsToChat}
aria-label={t('gitView.pr.actions.shareCommentsAria')}
>
<RiAiGenerate2 className="size-4" />
<Icon name="ai-generate-2" className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.shareComments')}</p></TooltipContent>
@@ -1688,7 +1670,7 @@ export const PullRequestSection: React.FC<{
disabled={isMarkingReady || isMerging || isUpdating || isEditingPr}
aria-label={t('gitView.pr.actions.markReadyAria')}
>
{isMarkingReady ? <RiLoader4Line className="size-4 animate-spin" /> : <RiCheckboxCircleLine className="size-4" />}
{isMarkingReady ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="checkbox-circle" className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.markReady')}</p></TooltipContent>
@@ -1722,7 +1704,7 @@ export const PullRequestSection: React.FC<{
disabled={isMerging || isMarkingReady || pr.state !== 'open' || pr.draft || isUpdating || isEditingPr}
aria-label={t('gitView.pr.actions.mergePrAria')}
>
{isMerging ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitMergeLine className="size-4" />}
{isMerging ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="git-merge" className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.mergePr')}</p></TooltipContent>
@@ -1745,7 +1727,7 @@ export const PullRequestSection: React.FC<{
{repoUrl ? (
<Button variant="outline" size="sm" asChild>
<a href={repoUrl} target="_blank" rel="noopener noreferrer">
<RiExternalLinkLine className="size-4" />
<Icon name="external-link" className="size-4" />
{t('gitView.pr.actions.repo')}
</a>
</Button>
@@ -1906,7 +1888,7 @@ export const PullRequestSection: React.FC<{
onClick={generateDescription}
disabled={isGenerating || isCreating}
>
{isGenerating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiAiGenerate2 className="size-4 text-primary" />}
{isGenerating ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="ai-generate-2" className="size-4 text-primary" />}
{t('gitView.commit.generate')}
</Button>
<div className="flex-1" />
@@ -1917,7 +1899,7 @@ export const PullRequestSection: React.FC<{
disabled={isCreating || !isConnected || !targetBaseBranch.trim() || (!useDetectedUpstream && targetBaseBranch.trim() === branch)}
>
<span className="inline-flex size-4 items-center justify-center">
{isCreating ? <RiLoader4Line className="size-4 animate-spin" /> : <RiGitPullRequestLine className="size-4" />}
{isCreating ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="git-pull-request" className="size-4" />}
</span>
<span>{t('gitView.pr.actions.createPr')}</span>
</Button>
@@ -1930,7 +1912,7 @@ export const PullRequestSection: React.FC<{
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col min-h-0">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiGitPullRequestLine className="h-5 w-5" />
<Icon name="git-pull-request" className="h-5 w-5" />
{t('gitView.pr.checkDetails.title')}
</DialogTitle>
<DialogDescription>
@@ -1941,7 +1923,7 @@ export const PullRequestSection: React.FC<{
<div className="flex-1 min-h-0 overflow-y-auto mt-2">
{isLoadingCheckDetails ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('gitView.loading.loading')}
</div>
) : null}
@@ -1971,7 +1953,7 @@ export const PullRequestSection: React.FC<{
<DialogContent className="max-w-2xl max-h-[82vh] min-h-[38rem] flex flex-col gap-2">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiGitPullRequestLine className="h-5 w-5" />
<Icon name="git-pull-request" className="h-5 w-5" />
{t('gitView.pr.comments.title')}
{pr ? (
<span className="typography-meta text-muted-foreground">{t('gitView.pr.numberLabel', { number: pr.number })}</span>
@@ -1982,7 +1964,7 @@ export const PullRequestSection: React.FC<{
<ScrollShadow className="mt-2 max-h-[66vh] overflow-y-auto overlay-scrollbar-target overlay-scrollbar-container">
{isLoadingCommentsDetails ? (
<div className="text-center text-muted-foreground py-8 flex items-center justify-center gap-2">
<RiLoader4Line className="h-4 w-4 animate-spin" />
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
{t('gitView.loading.loading')}
</div>
) : null}
@@ -2023,7 +2005,7 @@ export const PullRequestSection: React.FC<{
}}
aria-label={t('gitView.pr.actions.sendCommentToAgentAria')}
>
<RiAiGenerate2 className="size-3.5" />
<Icon name="ai-generate-2" className="size-3.5" />
{t('gitView.pr.actions.sendToAgent')}
</Button>
</TooltipTrigger>
@@ -10,7 +10,7 @@ import {
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/components/ui';
import { RiAlertLine, RiLoader4Line } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface StashDialogProps {
@@ -59,7 +59,7 @@ export const StashDialog: React.FC<StashDialogProps> = ({
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center gap-2">
<RiAlertLine className="size-5 text-[var(--status-warning)]" />
<Icon name="alert" className="size-5 text-[var(--status-warning)]" />
<DialogTitle>{t('gitView.stash.title')}</DialogTitle>
</div>
<DialogDescription>
@@ -115,7 +115,7 @@ export const StashDialog: React.FC<StashDialogProps> = ({
>
{isProcessing ? (
<>
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
{t('gitView.common.processing')}
</>
) : (
@@ -1,10 +1,10 @@
import React from 'react';
import { RiArchiveStackLine, RiDeleteBinLine, RiInboxArchiveLine, RiInboxUnarchiveFill, RiInboxUnarchiveLine, RiLoader4Line, RiSearchLine } from '@remixicon/react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import type { GitStashEntry } from '@/lib/api/types';
@@ -124,7 +124,7 @@ export const StashesDialog: React.FC<StashesDialogProps> = ({
<DialogContent className="max-w-2xl max-h-[70vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiArchiveStackLine className="h-5 w-5" />
<Icon name="archive-stack" className="h-5 w-5" />
{t('gitView.stashes.title')}
</DialogTitle>
<DialogDescription>{t('gitView.stashes.description')}</DialogDescription>
@@ -140,7 +140,7 @@ export const StashesDialog: React.FC<StashesDialogProps> = ({
className="flex-1"
/>
<Button onClick={handleCreate} disabled={!hasUncommittedChanges || isOperating || !directory}>
{operation === 'create' ? <RiLoader4Line className="size-4 animate-spin" /> : <RiInboxArchiveLine className="size-4" />}
{operation === 'create' ? <Icon name="loader-4" className="size-4 animate-spin" /> : <Icon name="inbox-archive" className="size-4" />}
{t('gitView.stashes.actions.stashCurrentWithCount', { count: uncommittedFileCount })}
</Button>
</div>
@@ -148,13 +148,13 @@ export const StashesDialog: React.FC<StashesDialogProps> = ({
</div>
<div className="relative mt-2">
<RiSearchLine className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Icon name="search" className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={t('gitView.stashes.searchPlaceholder')} className="pl-9" />
</div>
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="flex items-center justify-center py-10 text-muted-foreground"><RiLoader4Line className="size-5 animate-spin" /></div>
<div className="flex items-center justify-center py-10 text-muted-foreground"><Icon name="loader-4" className="size-5 animate-spin" /></div>
) : filtered.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">{query ? t('gitView.stashes.empty.search') : t('gitView.stashes.empty.list')}</div>
) : filtered.map((stash, index) => (
@@ -172,9 +172,9 @@ export const StashesDialog: React.FC<StashesDialogProps> = ({
</p>
</div>
<div className="mr-2 flex shrink-0 items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100">
<StashIconButton label={t('gitView.stashes.actions.apply')} loading={operation === `apply:${stash.ref}`} onClick={() => runStashAction(stash, 'apply')} disabled={isOperating}><RiInboxUnarchiveLine className="size-4" /></StashIconButton>
<StashIconButton label={t('gitView.stashes.actions.pop')} loading={operation === `pop:${stash.ref}`} onClick={() => runStashAction(stash, 'pop')} disabled={isOperating}><RiInboxUnarchiveFill className="size-4" /></StashIconButton>
<StashIconButton label={t('gitView.stashes.actions.drop')} loading={operation === `drop:${stash.ref}`} onClick={() => runStashAction(stash, 'drop')} disabled={isOperating} destructive><RiDeleteBinLine className="size-4" /></StashIconButton>
<StashIconButton label={t('gitView.stashes.actions.apply')} loading={operation === `apply:${stash.ref}`} onClick={() => runStashAction(stash, 'apply')} disabled={isOperating}><Icon name="inbox-unarchive" className="size-4" /></StashIconButton>
<StashIconButton label={t('gitView.stashes.actions.pop')} loading={operation === `pop:${stash.ref}`} onClick={() => runStashAction(stash, 'pop')} disabled={isOperating}><Icon name="inbox-unarchive-fill" className="size-4" /></StashIconButton>
<StashIconButton label={t('gitView.stashes.actions.drop')} loading={operation === `drop:${stash.ref}`} onClick={() => runStashAction(stash, 'drop')} disabled={isOperating} destructive><Icon name="delete-bin" className="size-4" /></StashIconButton>
</div>
</div>
))}
@@ -188,7 +188,7 @@ const StashIconButton: React.FC<{ label: string; loading: boolean; disabled: boo
<Tooltip>
<TooltipTrigger asChild>
<button type="button" aria-label={label} aria-busy={loading || undefined} className={cn('flex h-6 w-6 items-center justify-center transition-colors disabled:opacity-50', destructive ? 'text-[var(--status-error)] hover:text-[var(--status-error)]' : 'text-muted-foreground hover:text-foreground')} onClick={onClick} disabled={disabled}>
{loading ? <RiLoader4Line className="size-4 animate-spin" /> : children}
{loading ? <Icon name="loader-4" className="size-4 animate-spin" /> : children}
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{label}</TooltipContent>
@@ -1,10 +1,4 @@
import React from 'react';
import {
RiArrowDownSLine,
RiCloseLine,
RiLoader4Line,
RiRefreshLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
@@ -13,6 +7,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Icon } from "@/components/icon/Icon";
import type { GitRemote } from '@/lib/gitApi';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
@@ -90,9 +85,9 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
aria-label={t('gitView.sync.syncChanges')}
>
{syncAction === 'sync' ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiRefreshLine className="size-4" />
<Icon name="refresh" className="size-4" />
)}
<span className="whitespace-nowrap tabular-nums">{primaryLabel}</span>
</button>
@@ -111,7 +106,7 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
disabled={isDropdownDisabled}
aria-label={t('gitView.sync.moreActionsAria')}
>
<RiArrowDownSLine className="size-4" />
<Icon name="arrow-down-s" className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" alignOffset={-40} className="w-[min(360px,calc(100vw-2rem))] max-h-[320px] overflow-y-auto">
@@ -128,7 +123,7 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
}}
>
<div className="flex w-full items-center gap-2">
<RiRefreshLine className="size-4 text-muted-foreground" />
<Icon name="refresh" className="size-4 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="flex flex-col">
<span className="typography-ui-label text-foreground">
@@ -161,9 +156,9 @@ export const SyncActions: React.FC<SyncActionsProps> = ({
title={t('gitView.header.removeRemoteTitle', { name: remote.name })}
>
{removingRemoteName === remote.name ? (
<RiLoader4Line className="size-3.5 animate-spin" />
<Icon name="loader-4" className="size-3.5 animate-spin" />
) : (
<RiCloseLine className="size-3.5" />
<Icon name="close" className="size-3.5" />
)}
</Button>
) : null}
@@ -1,6 +1,6 @@
import React from 'react';
import { RiGitBranchLine, RiEditLine, RiCheckLine, RiCloseLine, RiLoader4Line } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface WorktreeBranchDisplayProps {
@@ -86,7 +86,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
handleSaveEdit();
}}
>
<RiGitBranchLine className="size-4 text-primary" />
<Icon name="git-branch" className="size-4 text-primary" />
<input
ref={inputRef}
value={editBranchName}
@@ -104,9 +104,9 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
>
{isRenaming ? (
<RiLoader4Line className="size-4 animate-spin" />
<Icon name="loader-4" className="size-4 animate-spin" />
) : (
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
)}
</button>
<button
@@ -117,7 +117,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
title={t('gitView.branch.renameCancel')}
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</form>
</div>
@@ -126,7 +126,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
return (
<div className="flex w-full min-w-0 items-center gap-1.5 px-2 py-1 h-8">
<RiGitBranchLine className="size-4 text-primary shrink-0" />
<Icon name="git-branch" className="size-4 text-primary shrink-0" />
<div className="inline-flex min-w-0 max-w-full items-center gap-1">
<span className="truncate typography-ui-label font-normal text-foreground">
{currentBranch || t('gitView.branch.detachedHead')}
@@ -139,7 +139,7 @@ export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
onClick={handleStartEdit}
title={t('gitView.branch.renameTitle')}
>
<RiEditLine className="size-4" />
<Icon name="edit" className="size-4" />
</Button>
)}
</div>