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
+14 -15
View File
@@ -1,5 +1,4 @@
import React from 'react';
import { RiArrowDownSLine, RiArrowRightSLine, RiEditLine, RiGitCommitLine, RiLoader4Line, RiTextWrap } from '@remixicon/react';
import { useUIStore } from '@/stores/useUIStore';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
@@ -26,6 +25,7 @@ import type { DiffViewMode } from '@/components/chat/message/types';
import { PierreDiffViewer } from './PierreDiffViewer';
import { useDeviceInfo } from '@/lib/device';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { Icon } from "@/components/icon/Icon";
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
import { sessionEvents } from '@/lib/sessionEvents';
import { useI18n } from '@/lib/i18n';
@@ -232,7 +232,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
) : (
<span className="text-muted-foreground">{t('diffView.selector.selectFile')}</span>
)}
<RiArrowDownSLine className="size-4 opacity-50" />
<Icon name="arrow-down-s" className="size-4 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[70vh] min-w-[320px] overflow-y-auto">
@@ -297,7 +297,7 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
<span className="min-w-0 truncate typography-meta">
{t(currentOption.labelKey)}
</span>
<RiArrowDownSLine className="size-4 opacity-50" />
<Icon name="arrow-down-s" className="size-4 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[140px]">
@@ -788,9 +788,9 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
<div className="relative flex min-w-0 flex-1 items-center gap-2">
<span className="flex size-5 items-center justify-center opacity-70 group-hover/header:opacity-100">
{isExpanded ? (
<RiArrowDownSLine className="size-4" />
<Icon name="arrow-down-s" className="size-4" />
) : (
<RiArrowRightSLine className="size-4" />
<Icon name="arrow-right-s" className="size-4" />
)}
</span>
<span
@@ -856,9 +856,9 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
disabled={isOpeningInEditor}
>
{isOpeningInEditor ? (
<RiLoader4Line className="size-3.5 animate-spin" />
<Icon name="loader-4" className="size-3.5 animate-spin" />
) : (
<RiEditLine className="size-3.5" />
<Icon name="edit" className="size-3.5" />
)}
</Button>
) : null}
@@ -895,7 +895,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
) : null}
{isLoading && !diffData && !diffLoadError ? (
<div className="flex items-center justify-center gap-2 px-4 py-8 text-sm text-muted-foreground">
<RiLoader4Line size={16} className="animate-spin" />
<Icon name="loader-4" className="animate-spin" />
Loading diff…
</div>
) : null}
@@ -987,7 +987,6 @@ export const DiffView: React.FC<DiffViewProps> = ({
const pendingScrollFrameRef = React.useRef<number | null>(null);
const shouldPinAfterAlignRef = React.useRef(false);
React.useEffect(() => {
if (!pinSelectedFileHeaderToTopOnNavigate || !isStackedView || !pinnedStackedTarget) {
return;
@@ -1630,7 +1629,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
if (isLoadingStatus && !status) {
return (
<div className="flex flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
<RiLoader4Line size={16} className="animate-spin" />
<Icon name="loader-4" className="animate-spin" />
{t('diffView.state.loadingRepositoryStatus')}
</div>
);
@@ -1682,7 +1681,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
</div>
) : (
<>
<RiLoader4Line size={16} className="animate-spin" />
<Icon name="loader-4" className="animate-spin" />
{t('diffView.state.loadingDiff')}
</>
)}
@@ -1697,7 +1696,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
<div className="flex items-center gap-3 px-3 py-2 bg-background">
{!isMobile && (
<div className="flex items-center gap-1 rounded-md px-2 py-1 text-muted-foreground shrink-0">
<RiGitCommitLine size={16} />
<Icon name="git-commit" className="h-4 w-4" />
<span className="typography-ui-label font-semibold text-foreground">
{isLoadingStatus && !status
? t('diffView.state.loadingChanges')
@@ -1734,7 +1733,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
)}
title={diffWrapLines ? t('diffView.actions.disableLineWrap') : t('diffView.actions.enableLineWrap')}
>
<RiTextWrap className="size-4" />
<Icon name="text-wrap" className="size-4" />
</Button>
)}
{showOpenInEditorAction && selectedFileEntry && !isStackedView && (
@@ -1749,9 +1748,9 @@ export const DiffView: React.FC<DiffViewProps> = ({
title={t('diffView.actions.openFileAtFirstChangedLine')}
>
{isOpeningSelectedInEditor ? (
<RiLoader4Line className="size-3.5 animate-spin" />
<Icon name="loader-4" className="size-3.5 animate-spin" />
) : (
<RiEditLine className="size-3.5" />
<Icon name="edit" className="size-3.5" />
)}
</Button>
)}