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,19 +1,8 @@
import React from 'react';
import {
RiFolderLine,
RiFolderOpenLine,
RiArrowRightSLine,
RiArrowDownSLine,
RiPencilAiLine,
RiDeleteBinLine,
RiCheckLine,
RiCloseLine,
RiAddLine,
RiFolderAddLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
interface SessionFolderItemProps<TSessionNode> {
folder: SessionFolder;
@@ -143,7 +132,7 @@ const SessionFolderItemBase = <TSessionNode,>({
};
}, [isRenaming]);
const FolderIcon = isCollapsed ? RiFolderLine : RiFolderOpenLine;
const folderIconName = isCollapsed ? 'folder' : 'folder-open';
const isSubFolder = depth > 0;
return (
@@ -179,7 +168,7 @@ const SessionFolderItemBase = <TSessionNode,>({
? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
: '',
)}>
<FolderIcon className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
<Icon name={folderIconName} className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
{renaming ? (
<form
@@ -220,7 +209,7 @@ const SessionFolderItemBase = <TSessionNode,>({
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
>
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
</button>
<button
type="button"
@@ -232,7 +221,7 @@ const SessionFolderItemBase = <TSessionNode,>({
onMouseDown={(event) => event.stopPropagation()}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</form>
) : (
@@ -244,9 +233,9 @@ const SessionFolderItemBase = <TSessionNode,>({
• {sessions.length}
</span>
{isCollapsed ? (
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-right-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
) : (
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
)}
</div>
)}
@@ -274,7 +263,7 @@ const SessionFolderItemBase = <TSessionNode,>({
aria-label={t('sessions.sidebar.folderItem.newSessionAria', { folderName: folder.name })}
title={t('sessions.sidebar.project.actions.newSession')}
>
<RiAddLine className="h-3.5 w-3.5" />
<Icon name="add" className="h-3.5 w-3.5" />
</button>
) : null}
{/* Only allow sub-folders at depth 0 (one level deep max) */}
@@ -289,7 +278,7 @@ const SessionFolderItemBase = <TSessionNode,>({
aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })}
title={t('sessions.sidebar.folderItem.newSubfolder')}
>
<RiFolderAddLine className="h-3.5 w-3.5" />
<Icon name="folder-add" className="h-3.5 w-3.5" />
</button>
) : null}
{!archivedBucket ? (
@@ -302,7 +291,7 @@ const SessionFolderItemBase = <TSessionNode,>({
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.folderItem.renameAria', { folderName: folder.name })}
>
<RiPencilAiLine className="h-3.5 w-3.5" />
<Icon name="pencil-ai" className="h-3.5 w-3.5" />
</button>
) : null}
<button
@@ -316,7 +305,7 @@ const SessionFolderItemBase = <TSessionNode,>({
? t('sessions.sidebar.folderItem.deleteArchivedInFolderAria', { folderName: folder.name })
: t('sessions.sidebar.folderItem.deleteFolderAria', { folderName: folder.name })}
>
<RiDeleteBinLine className="h-3.5 w-3.5" />
<Icon name="delete-bin" className="h-3.5 w-3.5" />
</button>
</div>
</div>