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,10 +1,10 @@
import React from 'react';
import { RiAddLine, RiBrainAi3Line, RiCloseLine, RiSearchLine, RiStarFill, RiTimeLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { isIMECompositionEvent } from '@/lib/ime';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -79,7 +79,7 @@ export const ModelChip: React.FC<{
onClick={onRemove}
className="text-muted-foreground hover:text-foreground ml-0.5"
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
);
@@ -349,7 +349,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
setIsOpen(!isOpen);
}}
>
<RiAddLine className="h-3.5 w-3.5 mr-1" />
<Icon name="add" className="h-3.5 w-3.5 mr-1" />
{addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
</Button>
@@ -426,7 +426,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{/* Search input */}
<div className="p-2 border-b border-border/40">
<div className="relative">
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
ref={searchInputRef}
type="text"
@@ -455,7 +455,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{filteredFavorites.length > 0 && (
<>
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
<RiStarFill className="h-4 w-4 text-primary" />
<Icon name="star-fill" className="h-4 w-4 text-primary" />
{t('multirun.modelMultiSelect.sections.favorites')}
</div>
{filteredFavorites.map(({ model, providerID, modelID }) => {
@@ -470,7 +470,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<>
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
<RiTimeLine className="h-4 w-4" />
<Icon name="time" className="h-4 w-4" />
{t('multirun.modelMultiSelect.sections.recent')}
</div>
{filteredRecents.map(({ model, providerID, modelID }) => {
@@ -554,7 +554,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
size="chip"
className="px-2 gap-1.5 rounded-md !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 typography-meta font-medium text-foreground"
>
<RiBrainAi3Line
<Icon name="brain-ai-3"
className={cn(
'h-3.5 w-3.5 flex-shrink-0',
variantValue === DEFAULT_VARIANT_VALUE ? 'text-muted-foreground' : 'text-[color:var(--status-info)]'
@@ -1,5 +1,4 @@
import React from 'react';
import { RiAddLine, RiArrowDownSLine, RiAttachment2, RiCloseLine, RiFileImageLine, RiFileLine, RiFolderLine, RiInformationLine, RiTerminalLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -21,6 +20,7 @@ import { BranchSelector, useBranchOptions } from './BranchSelector';
import { AgentSelector } from './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 { isDesktopShell } from '@/lib/desktop';
import { useTabletStandalonePwaRuntime } from '@/lib/device';
import { useThemeSystem } from '@/contexts/useThemeSystem';
@@ -60,7 +60,7 @@ const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Tooltip>
<TooltipTrigger asChild>
<button type="button" tabIndex={-1} className="inline-flex items-center justify-center text-muted-foreground/50 hover:text-muted-foreground transition-colors">
<RiInformationLine className="h-3.5 w-3.5" />
<Icon name="information" className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[240px]">
@@ -168,7 +168,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
iconColor: currentTheme.colors.surface.foreground,
},
);
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
const iconColor = project.color ? PROJECT_COLOR_MAP[project.color] : undefined;
return (
@@ -180,10 +180,10 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
>
<img src={imageUrl} alt="" className="h-full w-full object-contain" draggable={false} />
</span>
) : ProjectIcon ? (
<ProjectIcon className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
) : projectIconName ? (
<Icon name={projectIconName} className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
) : (
<RiFolderLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
<Icon name="folder" className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined}/>
)}
<span className="truncate">{displayLabel}</span>
</span>
@@ -615,7 +615,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
aria-label={t('multirun.launcher.actions.closeEsc')}
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
>
<RiCloseLine className="h-5 w-5" />
<Icon name="close" className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
@@ -716,7 +716,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
{/* ── Setup commands (collapsible, full width) ── */}
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
<CollapsibleTrigger className="w-full flex items-center gap-2 py-1.5 px-2 -mx-2 rounded-lg hover:bg-[var(--interactive-hover)]/50 transition-colors group">
<RiTerminalLine className="h-3.5 w-3.5 text-muted-foreground/70" />
<Icon name="terminal" className="h-3.5 w-3.5 text-muted-foreground/70" />
<span className="typography-meta font-medium text-muted-foreground group-hover:text-foreground transition-colors">
{t('multirun.launcher.setupCommands.label')}
</span>
@@ -733,7 +733,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
{configuredSetupCount}
</span>
)}
<RiArrowDownSLine className={cn(
<Icon name="arrow-down-s" className={cn(
'h-3.5 w-3.5 text-muted-foreground/50 transition-transform duration-200 ml-auto',
isSetupCommandsOpen && 'rotate-180'
)} />
@@ -765,7 +765,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
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('multirun.launcher.setupCommands.removeCommandAria')}
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
</div>
))}
@@ -774,7 +774,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
onClick={() => setSetupCommands([...setupCommands, ''])}
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors px-1"
>
<RiAddLine className="h-3 w-3" />
<Icon name="add" className="h-3 w-3" />
{t('multirun.launcher.setupCommands.addCommand')}
</button>
</>
@@ -854,7 +854,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center gap-1 h-6 px-2 rounded-md typography-micro text-muted-foreground hover:text-foreground hover:bg-[var(--interactive-hover)]/50 transition-colors"
>
<RiAttachment2 className="h-3 w-3" />
<Icon name="attachment-2" className="h-3 w-3" />
{t('multirun.launcher.attachments.attach')}
</button>
</TooltipTrigger>
@@ -871,9 +871,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
}}
>
{file.mimeType.startsWith('image/') ? (
<RiFileImageLine className="h-3 w-3 text-muted-foreground" />
<Icon name="file-image" className="h-3 w-3 text-muted-foreground" />
) : (
<RiFileLine className="h-3 w-3 text-muted-foreground" />
<Icon name="file" className="h-3 w-3 text-muted-foreground" />
)}
<span className="truncate max-w-[100px]" title={file.filename}>
{file.filename}
@@ -883,7 +883,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
onClick={() => handleRemoveFile(file.id)}
className="text-muted-foreground hover:text-destructive"
>
<RiCloseLine className="h-3 w-3" />
<Icon name="close" className="h-3 w-3" />
</button>
</div>
))}