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
@@ -11,9 +11,9 @@ import remend from 'remend';
import { FadeInOnReveal } from './message/FadeInOnReveal';
import type { Part } from '@opencode-ai/sdk/v2';
import { cn } from '@/lib/utils';
import { RiFileCopyLine, RiCheckLine, RiDownloadLine, RiEyeLine, RiCodeLine } from '@remixicon/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
@@ -228,7 +228,7 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.table.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-none">
@@ -286,7 +286,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.table.actions.downloadTitle')}
>
<RiDownloadLine className="size-3.5" />
<Icon name="download" className="size-3.5" />
</button>
{showMenu && (
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-none">
@@ -425,7 +425,7 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -449,7 +449,7 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copyTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -472,14 +472,14 @@ const MermaidBlock: React.FC<{ source: string; mode: 'svg' | 'ascii' }> = ({ sou
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.copySourceTitle')}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
<button
onClick={handleDownloadSvg}
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
title={t('markdownRenderer.mermaid.actions.downloadSvgTitle')}
>
{downloaded ? <RiCheckLine className="size-3.5" /> : <RiDownloadLine className="size-3.5" />}
{downloaded ? <Icon name="check" className="size-3.5" /> : <Icon name="download" className="size-3.5" />}
</button>
</div>
</div>
@@ -793,7 +793,7 @@ const MarkdownCodeBlock: React.FC<{
aria-pressed={viewMode === 'preview'}
aria-label={viewMode === 'preview' ? 'Show code' : 'Preview HTML'}
>
{viewMode === 'preview' ? <RiCodeLine className="size-3.5" /> : <RiEyeLine className="size-3.5" />}
{viewMode === 'preview' ? <Icon name="code" className="size-3.5" /> : <Icon name="eye" className="size-3.5" />}
</button>
) : null}
{canPreview ? (
@@ -804,7 +804,7 @@ const MarkdownCodeBlock: React.FC<{
title="Download HTML"
aria-label="Download HTML"
>
<RiDownloadLine className="size-3.5" />
<Icon name="download" className="size-3.5" />
</button>
) : null}
<button
@@ -814,7 +814,7 @@ const MarkdownCodeBlock: React.FC<{
title={copied ? 'Copied' : 'Copy code'}
aria-label={copied ? 'Copied' : 'Copy code'}
>
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
{copied ? <Icon name="check" className="size-3.5" /> : <Icon name="file-copy" className="size-3.5" />}
</button>
</div>
</div>
@@ -959,7 +959,7 @@ const buildMarkdownComponents = ({
title={previewTitle ?? previewLabel ?? 'Open preview pane'}
data-loopback-preview-trigger="true"
>
<RiEyeLine className="size-3" aria-hidden="true" />
<Icon name="eye" className="size-3" aria-hidden="true"/>
<span className="font-medium">{previewLabel ?? 'Preview'}</span>
</button>
) : null}