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,15 +1,7 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowLeftLongLine,
RiArrowRightSLine,
RiDeleteBinLine,
RiGitBranchLine,
} from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { sessionEvents } from '@/lib/sessionEvents';
import type { MainTab } from '@/stores/useUIStore';
@@ -536,7 +528,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center gap-1 leading-none align-middle">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -544,7 +536,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
{prIndicator.url ? (
@@ -568,7 +560,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -593,12 +585,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : group.isArchivedBucket ? (
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiArchiveLine className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<Icon name="archive" className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<span className={cn(
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -606,7 +598,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : (!group.isMain || group.worktree) ? (
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -614,7 +606,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -626,16 +618,15 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{showBranchSubtitle && statusLine ? (
<span className="inline-flex min-w-0 items-center gap-1.5 leading-tight">
{group.isArchivedBucket ? (
<RiArchiveLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="archive" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
) : (!group.isMain || isGitProject) ? (
showInlinePrTitle && prIndicator ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
<RiGitBranchLine
className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} align="start" className="max-w-sm">
@@ -645,7 +636,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -666,10 +657,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
) : (
<RiGitBranchLine
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
)
) : null}
<span
@@ -698,7 +688,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteArchivedInGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteArchivedSessions')}</p></TooltipContent>
@@ -722,7 +712,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteWorktree')}</p></TooltipContent>
@@ -745,7 +735,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
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.group.actions.newDraftInGroupAria', { label: group.label })}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.project.actions.newDraftSession')}</p></TooltipContent>