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,12 +1,4 @@
import React from 'react';
import {
RiGitBranchLine,
RiArrowDownSLine,
RiCheckLine,
RiMore2Line,
RiFileCopyLine,
RiLoader4Line,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { copyTextToClipboard } from '@/lib/clipboard';
@@ -31,6 +23,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface AgentGroupDetailProps {
@@ -163,7 +156,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h1 className="typography-heading-lg text-foreground truncate">{group.name}</h1>
{groupBusy && <RiLoader4Line className="h-4 w-4 animate-spin text-amber-500 flex-shrink-0" />}
{groupBusy && <Icon name="loader-4" className="h-4 w-4 animate-spin text-amber-500 flex-shrink-0" />}
</div>
<div className="flex items-center gap-2 mt-1 typography-meta text-muted-foreground">
<span>
@@ -173,7 +166,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
</span>
<span>·</span>
<span className="flex items-center gap-1">
<RiGitBranchLine className="h-3.5 w-3.5" />
<Icon name="git-branch" className="h-3.5 w-3.5" />
{selectedSession?.worktreeMetadata?.label || selectedSession?.branch || t('agentManager.detail.header.noBranch')}
</span>
</div>
@@ -209,7 +202,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
</>
)}
</div>
<RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[var(--anchor-width)]">
@@ -237,13 +230,13 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
</div>
{session.branch && (
<div className="flex items-center gap-1 typography-micro text-muted-foreground/60">
<RiGitBranchLine className="h-3 w-3" />
<Icon name="git-branch" className="h-3 w-3" />
<span className="truncate">{session.worktreeMetadata?.label || session.branch}</span>
</div>
)}
</div>
{selectedSession?.id === session.id && (
<RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />
<Icon name="check" className="h-4 w-4 text-primary flex-shrink-0" />
)}
</DropdownMenuItem>
))}
@@ -254,7 +247,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="flex-shrink-0" aria-label={t('agentManager.detail.actions.worktreeActionsAria')}>
<RiMore2Line className="h-4 w-4" />
<Icon name="more-2" className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[220px]">
@@ -278,7 +271,7 @@ export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
}}
disabled={!selectedSession?.path}
>
<RiFileCopyLine className="h-4 w-4 mr-px" />
<Icon name="file-copy" className="h-4 w-4 mr-px" />
{t('agentManager.detail.actions.copyWorktreePath')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -1,15 +1,4 @@
import React from 'react';
import {
RiAddCircleLine,
RiAddLine,
RiArrowDownSLine,
RiCloseLine,
RiFileImageLine,
RiFileLine,
RiGitBranchLine,
RiHourglassFill,
RiSendPlane2Line,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { cn } from '@/lib/utils';
import { Input } from '@/components/ui/input';
@@ -22,6 +11,7 @@ import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSe
import { AgentSelector } from '@/components/multirun/AgentSelector';
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
import { Icon } from "@/components/icon/Icon";
import { isIMECompositionEvent } from '@/lib/ime';
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
import { useThemeSystem } from '@/contexts/useThemeSystem';
@@ -438,7 +428,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
{/* Branch Selection */}
<div className="space-y-1.5">
<label className="typography-ui-label font-medium text-foreground flex items-center gap-1.5">
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
<Icon name="git-branch" className="h-4 w-4 text-muted-foreground" />
{t('agentManager.empty.baseBranch.label')}
</label>
<BranchSelector
@@ -465,7 +455,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
) : null;
})()}
</p>
<RiArrowDownSLine className={cn(
<Icon name="arrow-down-s" className={cn(
'h-4 w-4 text-muted-foreground transition-transform duration-200',
isSetupCommandsOpen && 'rotate-180'
)} />
@@ -500,7 +490,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('agentManager.empty.setupCommands.removeCommandAria')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</div>
))}
@@ -509,7 +499,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onClick={() => setSetupCommands([...setupCommands, ''])}
className="flex items-center gap-1.5 typography-meta text-muted-foreground hover:text-foreground transition-colors"
>
<RiAddLine className="h-3.5 w-3.5" />
<Icon name="add" className="h-3.5 w-3.5" />
{t('agentManager.empty.setupCommands.addCommand')}
</button>
</div>
@@ -583,9 +573,9 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
className="inline-flex items-center gap-1.5 px-2 py-1 bg-muted/30 border border-border/30 rounded-md typography-meta"
>
{file.mimeType.startsWith('image/') ? (
<RiFileImageLine className="h-3.5 w-3.5 text-muted-foreground" />
<Icon name="file-image" className="h-3.5 w-3.5 text-muted-foreground" />
) : (
<RiFileLine className="h-3.5 w-3.5 text-muted-foreground" />
<Icon name="file" className="h-3.5 w-3.5 text-muted-foreground" />
)}
<span className="truncate max-w-[120px]" title={file.filename}>
{file.filename}
@@ -595,7 +585,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
onClick={() => handleRemoveFile(file.id)}
className="text-muted-foreground hover:text-destructive ml-0.5"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
))}
@@ -620,7 +610,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground transition-colors"
aria-label={t('agentManager.empty.prompt.addAttachmentAria')}
>
<RiAddCircleLine className="h-[18px] w-[18px]" />
<Icon name="add-circle" className="h-[18px] w-[18px]" />
</button>
</div>
@@ -645,9 +635,9 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
aria-label={t('agentManager.empty.actions.startAgentGroupAria')}
>
{isSubmittingOrCreating ? (
<RiHourglassFill className="h-[18px] w-[18px] animate-spin" />
<Icon name="hourglass-fill" className="h-[18px] w-[18px] animate-spin" />
) : (
<RiSendPlane2Line className="h-[18px] w-[18px]" />
<Icon name="send-plane-2" className="h-[18px] w-[18px]" />
)}
</button>
</div>
@@ -1,12 +1,4 @@
import React from 'react';
import {
RiAddLine,
RiArrowDownSLine,
RiMore2Line,
RiSearchLine,
RiGitBranchLine,
RiLoader4Line,
} from '@remixicon/react';
import { toast } from '@/components/ui';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@@ -25,6 +17,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useAgentGroupsStore, type AgentGroup } from '@/stores/useAgentGroupsStore';
import { useAllSessionStatuses } from '@/sync/sync-context';
@@ -92,11 +85,11 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
<span className="truncate typography-ui-label font-normal text-foreground">
{group.name}
</span>
{isBusy && <RiLoader4Line className="h-3 w-3 animate-spin text-amber-500 flex-shrink-0" />}
{isBusy && <Icon name="loader-4" className="h-3 w-3 animate-spin text-amber-500 flex-shrink-0" />}
</div>
<div className="flex items-center gap-2">
<span className="typography-micro text-muted-foreground/60 flex items-center gap-1">
<RiGitBranchLine className="h-3 w-3" />
<Icon name="git-branch" className="h-3 w-3" />
{group.sessionCount === 1
? t('agentManager.sidebar.item.modelCountSingle', { count: group.sessionCount })
: t('agentManager.sidebar.item.modelCountPlural', { count: group.sessionCount })}
@@ -126,7 +119,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, isBu
aria-label={t('agentManager.sidebar.item.groupMenuAria')}
onClick={(e) => e.stopPropagation()}
>
<RiMore2Line className="h-3.5 w-3.5" />
<Icon name="more-2" className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[140px]">
@@ -218,7 +211,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
{/* Search Input */}
<div className="px-2.5 pt-3 pb-2">
<div className="relative">
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@@ -235,14 +228,14 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
className="w-full justify-start gap-2 h-8"
onClick={onNewAgent}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
<span className="typography-ui-label">{t('agentManager.sidebar.actions.newAgentGroup')}</span>
</Button>
</div>
{/* Agent Groups Section Header */}
<div className="px-2.5 py-1.5 flex items-center gap-1">
<RiArrowDownSLine className="h-4 w-4 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-4 w-4 text-muted-foreground" />
<span className="typography-micro font-medium text-muted-foreground uppercase tracking-wider">
{t('agentManager.sidebar.section.agentGroups')}
</span>