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
@@ -6,7 +6,7 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RiArrowDownSLine, RiFolderLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { useProjectsStore } from '@/stores/useProjectsStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { cn } from '@/lib/utils';
@@ -59,9 +59,9 @@ export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ clas
'flex items-center gap-1.5 text-left'
)}
>
<RiFolderLine className="h-4 w-4 opacity-70" />
<Icon name="folder" className="h-4 w-4 opacity-70" />
<span className="min-w-0 flex-1 truncate typography-ui-label font-medium">{label}</span>
<RiArrowDownSLine className="size-4 opacity-50" />
<Icon name="arrow-down-s" className="size-4 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-auto">
@@ -1,6 +1,6 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { RiAddLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { useDeviceInfo } from '@/lib/device';
import { cn } from '@/lib/utils';
@@ -54,7 +54,7 @@ export const SettingsSidebarHeader: React.FC<SettingsSidebarHeaderProps> = ({
onClick={onAdd}
aria-label={addButtonLabel}
>
<RiAddLine className="size-4" />
<Icon name="add" className="size-4" />
</Button>
)}
</div>
@@ -6,14 +6,14 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RiMore2Line } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
export interface SettingsSidebarItemAction {
/** Label shown in dropdown menu */
label: string;
/** Icon component to show before label */
icon?: React.ComponentType<{ className?: string }>;
icon?: string;
/** Callback when action is clicked */
onClick: () => void;
/** If true, uses destructive styling (red text) */
@@ -47,10 +47,10 @@ interface SettingsSidebarItemProps {
* metadata={agent.description}
* selected={selectedId === agent.id}
* onSelect={() => setSelectedId(agent.id)}
* icon={<RiRobotLine className="h-4 w-4" />}
* icon={<'robot' className="h-4 w-4" />}
* actions={[
* { label: 'Duplicate', icon: RiFileCopyLine, onClick: handleDuplicate },
* { label: 'Delete', icon: RiDeleteBinLine, onClick: handleDelete, destructive: true },
* { label: 'Duplicate', icon: 'file-copy', onClick: handleDuplicate },
* { label: 'Delete', icon: 'delete-bin', onClick: handleDelete, destructive: true },
* ]}
* />
*/
@@ -103,12 +103,12 @@ export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
variant="ghost"
className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
>
<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="w-fit min-w-20">
{actions.map((action) => {
const Icon = action.icon;
const iconName = action.icon;
return (
<DropdownMenuItem
key={action.label}
@@ -120,7 +120,7 @@ export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
action.destructive && 'text-destructive focus:text-destructive'
)}
>
{Icon && <Icon className="h-4 w-4 mr-px" />}
{iconName && <Icon name={iconName} className="h-4 w-4 mr-px" />}
{action.label}
</DropdownMenuItem>
);
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { RiArrowDownSLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { Icon } from "@/components/icon/Icon";
interface SidebarGroupProps {
/** Group display label (e.g. "business", "automation-ai") */
@@ -64,7 +64,7 @@ export const SidebarGroup: React.FC<SidebarGroupProps> = ({
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
)}
>
<RiArrowDownSLine
<Icon name="arrow-down-s"
className={cn(
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-200',
!expanded && '-rotate-90',