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
@@ -7,21 +7,9 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiCheckLine,
RiCheckboxMultipleLine,
RiChatNewLine,
RiEqualizer2Line,
RiFolderAddLine,
RiLayoutLeftLine,
RiSearchLine,
RiCloseLine,
RiContractUpDownLine,
RiExpandUpDownLine,
RiCalendarScheduleLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { Icon } from "@/components/icon/Icon";
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useI18n } from '@/lib/i18n';
@@ -118,7 +106,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50"
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
<Icon name="layout-left" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.closeSessions')}</p></TooltipContent>
@@ -132,7 +120,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.addProject')}
>
<RiFolderAddLine className={headerActionIconClass} />
<Icon name="folder-add" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.addProject')}</p></TooltipContent>
@@ -145,7 +133,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.newSession')}
>
<RiChatNewLine className={headerActionIconClass} />
<Icon name="chat-new" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.newSession')}</p></TooltipContent>
@@ -176,7 +164,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.scheduledTasks')}
>
<RiCalendarScheduleLine className={headerActionIconClass} />
<Icon name="calendar-schedule" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.scheduledTasks')}</p></TooltipContent>
@@ -191,7 +179,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
aria-label={t('sessions.sidebar.header.actions.searchSessions')}
aria-expanded={isSessionSearchOpen}
>
<RiSearchLine className={headerActionIconClass} />
<Icon name="search" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.searchSessions')}</p></TooltipContent>
@@ -208,7 +196,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
: t('sessions.sidebar.header.actions.selectSessions')}
aria-pressed={selectionModeEnabled}
>
<RiCheckboxMultipleLine className={headerActionIconClass} />
<Icon name="checkbox-multiple" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
@@ -227,7 +215,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.sessionDisplayMode')}
>
<RiEqualizer2Line className={headerActionIconClass} />
<Icon name="equalizer-2" className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -239,14 +227,14 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'default' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'minimal' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -254,15 +242,15 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.showRecent')}</span>
{showRecentSection ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{showRecentSection ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={collapseAllProjects} className="flex items-center gap-2">
<RiContractUpDownLine className="h-4 w-4" />
<Icon name="contract-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.collapseAll')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={expandAllProjects} className="flex items-center gap-2">
<RiExpandUpDownLine className="h-4 w-4" />
<Icon name="expand-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.expandAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -281,7 +269,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
<span>{t('sessions.sidebar.header.search.escapeHint')}</span>
</div>
<div className="relative">
<RiSearchLine className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Icon name="search" className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
ref={sessionSearchInputRef}
value={sessionSearchQuery}
@@ -306,7 +294,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="absolute right-1 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.header.search.clear')}
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
) : null}
</div>