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,19 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Checkbox } from '@/components/ui/checkbox';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { toast } from '@/components/ui';
import {
RiLoader4Line,
RiPlayLine,
RiEdit2Line,
RiDeleteBinLine,
RiAddLine,
RiFolderLine,
RiTimerLine,
RiHistoryLine,
RiCheckboxCircleLine,
RiErrorWarningLine,
RiPulseLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { useUIStore } from '@/stores/useUIStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
@@ -159,14 +147,14 @@ const STATUS_META: Record<
ScheduledTaskStatus,
{
tone: StatusTone;
Icon: React.ComponentType<{ className?: string }>;
Icon: string;
spin?: boolean;
}
> = {
success: { tone: 'success', Icon: RiCheckboxCircleLine },
error: { tone: 'error', Icon: RiErrorWarningLine },
running: { tone: 'warning', Icon: RiLoader4Line, spin: true },
idle: { tone: 'muted', Icon: RiPulseLine },
success: { tone: 'success', Icon: 'checkbox-circle' },
error: { tone: 'error', Icon: 'error-warning' },
running: { tone: 'warning', Icon: 'loader-4', spin: true },
idle: { tone: 'muted', Icon: 'pulse' },
};
const toneStyle = (tone: StatusTone): React.CSSProperties => {
@@ -213,7 +201,7 @@ export function ScheduledTasksDialog() {
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 (
@@ -225,10 +213,10 @@ export function ScheduledTasksDialog() {
>
<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>
@@ -423,7 +411,7 @@ export function ScheduledTasksDialog() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{projectSelector}
<Button onClick={openNewTaskEditor} disabled={!selectedProjectID}>
<RiAddLine className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
<Icon name="add" className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
</Button>
</div>
) : (
@@ -433,7 +421,7 @@ export function ScheduledTasksDialog() {
<div className="min-h-[280px]">
{loading ? (
<div className="flex items-center gap-2 typography-meta text-muted-foreground">
<RiLoader4Line className="h-4 w-4 animate-spin" /> {t('sessions.scheduledTasks.dialog.loading')}
<Icon name="loader-4" className="h-4 w-4 animate-spin" /> {t('sessions.scheduledTasks.dialog.loading')}
</div>
) : tasks.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-4 typography-meta text-muted-foreground">
@@ -474,7 +462,7 @@ export function ScheduledTasksDialog() {
<div className="mt-3 flex flex-wrap items-center gap-x-5 gap-y-1 typography-micro text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<RiTimerLine className="h-3.5 w-3.5" />
<Icon name="timer" className="h-3.5 w-3.5" />
<span className="font-medium text-foreground">{t('sessions.scheduledTasks.dialog.nextRun.label')}</span>
{nextAt ? (
<>
@@ -487,14 +475,14 @@ export function ScheduledTasksDialog() {
)}
</span>
<span className="inline-flex items-center gap-1.5">
<RiHistoryLine className="h-3.5 w-3.5" />
<Icon name="history" className="h-3.5 w-3.5" />
<span className="font-medium text-foreground">{t('sessions.scheduledTasks.dialog.lastRun.label')}</span>
{status === 'running' ? (
<span
className="inline-flex items-center gap-1"
style={{ color: 'var(--status-warning)' }}
>
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
<Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" />
{t('sessions.scheduledTasks.dialog.lastRun.runningNow')}
</span>
) : lastAt ? (
@@ -504,7 +492,7 @@ export function ScheduledTasksDialog() {
className="inline-flex items-center gap-1"
style={{ color: `var(--status-${meta.tone})` }}
>
<meta.Icon className="h-3.5 w-3.5" />
<Icon name={meta.Icon} className="h-3.5 w-3.5" />
{statusLabel}
</span>
) : null}
@@ -522,7 +510,7 @@ export function ScheduledTasksDialog() {
className="mt-3 flex items-start gap-2 rounded-md border p-2 typography-micro"
style={toneStyle('error')}
>
<RiErrorWarningLine className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<Icon name="error-warning" className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span className="min-w-0 break-words">{task.state.lastError}</span>
</div>
) : null}
@@ -553,7 +541,7 @@ export function ScheduledTasksDialog() {
onClick={() => void handleRunNow(task)}
disabled={isBusy}
>
<RiPlayLine className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.runNow')}
<Icon name="play" className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.runNow')}
</Button>
<Button
variant="outline"
@@ -565,7 +553,7 @@ export function ScheduledTasksDialog() {
disabled={isBusy}
aria-label={t('sessions.scheduledTasks.dialog.actions.editAria', { taskName: task.name })}
>
<RiEdit2Line className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.edit')}
<Icon name="edit-2" className="h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.edit')}
</Button>
<Button
variant="destructive"
@@ -574,7 +562,7 @@ export function ScheduledTasksDialog() {
disabled={isBusy}
aria-label={t('sessions.scheduledTasks.dialog.actions.deleteAria', { taskName: task.name })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</Button>
</div>
</div>
@@ -612,7 +600,7 @@ export function ScheduledTasksDialog() {
onClick={openNewTaskEditor}
disabled={!selectedProjectID}
>
<RiAddLine className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
<Icon name="add" className="mr-1 h-4 w-4" /> {t('sessions.scheduledTasks.dialog.actions.newTask')}
</Button>
)}
>