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
@@ -15,26 +15,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
RiAddLine,
RiCheckLine,
RiCloudOffLine,
RiEarthLine,
RiLoader4Line,
RiMore2Line,
RiPencilLine,
RiPlug2Line,
RiRefreshLine,
RiServerLine,
RiSettings3Line,
RiShieldKeyholeLine,
RiStarFill,
RiStarLine,
RiDeleteBinLine,
RiWindowLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { isTauriShell, isDesktopShell } from '@/lib/desktop';
import { useUIStore } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
@@ -117,11 +100,11 @@ const statusLabelKey = (status: HostProbeResult['status'] | null):
};
const statusIcon = (status: HostProbeResult['status'] | null) => {
if (status === 'ok') return <RiCheckLine className="h-4 w-4" />;
if (status === 'auth') return <RiShieldKeyholeLine className="h-4 w-4" />;
if (status === 'wrong-service') return <RiCloudOffLine className="h-4 w-4" />;
if (status === 'unreachable') return <RiCloudOffLine className="h-4 w-4" />;
return <RiEarthLine className="h-4 w-4" />;
if (status === 'ok') return <Icon name="check" className="h-4 w-4" />;
if (status === 'auth') return <Icon name="shield-keyhole" className="h-4 w-4" />;
if (status === 'wrong-service') return <Icon name="cloud-off" className="h-4 w-4" />;
if (status === 'unreachable') return <Icon name="cloud-off" className="h-4 w-4" />;
return <Icon name="earth" className="h-4 w-4" />;
};
const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
@@ -732,14 +715,14 @@ export function DesktopHostSwitcherDialog({
disabled={!tauriAvailable || isLoading || isProbing}
aria-label={t('desktopHostSwitcher.actions.refreshInstancesAria')}
>
<RiRefreshLine className={cn('h-4 w-4', isProbing && 'animate-spin')} />
<Icon name="refresh" className={cn('h-4 w-4', isProbing && 'animate-spin')} />
</button>
</div>
</div>
) : (
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<RiServerLine className="h-5 w-5" />
<Icon name="server" className="h-5 w-5" />
{t('desktopHostSwitcher.title')}
</DialogTitle>
<DialogDescription>
@@ -764,7 +747,7 @@ export function DesktopHostSwitcherDialog({
onClick={() => void probeAll(allHosts)}
disabled={!tauriAvailable || isLoading || isProbing}
>
<RiRefreshLine className={cn('h-4 w-4', isProbing && 'animate-spin')} />
<Icon name="refresh" className={cn('h-4 w-4', isProbing && 'animate-spin')} />
{t('desktopHostSwitcher.actions.refresh')}
</Button>
</div>
@@ -775,7 +758,7 @@ export function DesktopHostSwitcherDialog({
<div className="flex-shrink-0 flex items-center justify-between gap-2 px-2.5 py-1.5">
<span className="typography-micro text-muted-foreground">{t('desktopHostSwitcher.ssh.needInstancesHint')}</span>
<Button type="button" variant="ghost" size="sm" onClick={openRemoteInstancesSettings}>
<RiSettings3Line className="h-4 w-4" />
<Icon name="settings-3" className="h-4 w-4" />
{t('desktopHostSwitcher.actions.remoteSsh')}
</Button>
</div>
@@ -868,7 +851,7 @@ export function DesktopHostSwitcherDialog({
disabled={isSaving}
onClick={(e) => e.stopPropagation()}
>
<RiMore2Line className="h-4 w-4" />
<Icon name="more-2" className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit min-w-28">
@@ -879,7 +862,7 @@ export function DesktopHostSwitcherDialog({
}}
disabled={isSaving}
>
<RiPencilLine className="h-4 w-4 mr-1" />
<Icon name="pencil" className="h-4 w-4 mr-1" />
{t('desktopHostSwitcher.actions.edit')}
</DropdownMenuItem>
<DropdownMenuItem
@@ -890,7 +873,7 @@ export function DesktopHostSwitcherDialog({
className="text-destructive focus:text-destructive"
disabled={isSaving}
>
<RiDeleteBinLine className="h-4 w-4 mr-1" />
<Icon name="delete-bin" className="h-4 w-4 mr-1" />
{t('desktopHostSwitcher.actions.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -917,7 +900,7 @@ export function DesktopHostSwitcherDialog({
void connectSshHostInPlace(host);
}}
>
{switchingHostId === host.id ? <RiLoader4Line className="h-3.5 w-3.5 animate-spin" /> : <RiPlug2Line className="h-3.5 w-3.5" />}
{switchingHostId === host.id ? <Icon name="loader-4" className="h-3.5 w-3.5 animate-spin" /> : <Icon name="plug-2" className="h-3.5 w-3.5" />}
{t('desktopHostSwitcher.actions.connect')}
</Button>
) : (
@@ -942,7 +925,7 @@ export function DesktopHostSwitcherDialog({
aria-label={isDefault ? t('desktopHostSwitcher.actions.defaultInstanceAria') : t('desktopHostSwitcher.actions.setAsDefaultAria')}
disabled={isSaving || (!isDefault && (statusKind === 'unreachable' || statusKind === 'wrong-service'))}
>
{isDefault ? <RiStarFill className="h-4 w-4" /> : <RiStarLine className="h-4 w-4" />}
{isDefault ? <Icon name="star-fill" className="h-4 w-4" /> : <Icon name="star" className="h-4 w-4" />}
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>
@@ -967,7 +950,7 @@ export function DesktopHostSwitcherDialog({
disabled={statusKind === 'unreachable' || statusKind === 'wrong-service'}
aria-label={t('desktopHostSwitcher.actions.openInNewWindowAria')}
>
<RiWindowLine className="h-4 w-4" />
<Icon name="window" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>
@@ -993,7 +976,7 @@ export function DesktopHostSwitcherDialog({
{t('desktopHostSwitcher.actions.cancel')}
</Button>
<Button type="button" size="sm" onClick={() => void commitEdit()} disabled={isSaving}>
{isSaving ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
{isSaving ? <Icon name="loader-4" className="h-4 w-4 animate-spin" /> : null}
{t('desktopHostSwitcher.actions.save')}
</Button>
</div>
@@ -1025,7 +1008,7 @@ export function DesktopHostSwitcherDialog({
onClick={() => setIsAddFormOpen(true)}
disabled={!tauriAvailable || isSaving}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
<span className="typography-ui-label">{t('desktopHostSwitcher.actions.addInstance')}</span>
</button>
</div>
@@ -1056,7 +1039,7 @@ export function DesktopHostSwitcherDialog({
onClick={() => void addHost()}
disabled={!tauriAvailable || isSaving || !newUrl.trim()}
>
{isSaving ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
{isSaving ? <Icon name="loader-4" className="h-4 w-4 animate-spin" /> : null}
{t('desktopHostSwitcher.actions.add')}
</Button>
</div>
@@ -1104,7 +1087,7 @@ export function DesktopHostSwitcherDialog({
<DialogContent className="w-[min(28rem,calc(100vw-2rem))] max-w-none">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RiLoader4Line className={cn('h-4 w-4', !sshSwitchModal.error && 'animate-spin')} />
<Icon name="loader-4" className={cn('h-4 w-4', !sshSwitchModal.error && 'animate-spin')} />
{t('desktopHostSwitcher.ssh.connectingTo', { host: sshSwitchModal.hostLabel || t('desktopHostSwitcher.ssh.instanceFallback') })}
</DialogTitle>
<DialogDescription>
@@ -1337,7 +1320,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
data-oc-host-switcher
className={cn(headerIconButtonClass, 'relative w-auto px-3')}
>
<RiServerLine className="h-5 w-5" />
<Icon name="server" className="h-5 w-5" />
<span className="hidden sm:inline typography-ui-label font-medium text-muted-foreground truncate max-w-[11rem]">
{safeEffectiveLabel}
</span>
@@ -1397,7 +1380,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
onClick={retryStartupSsh}
disabled={startupSshModal.connecting || !startupSshModal.hostId}
>
{startupSshModal.connecting ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
{startupSshModal.connecting ? <Icon name="loader-4" className="h-4 w-4 animate-spin" /> : null}
{t('desktopHostSwitcher.actions.retry')}
</Button>
</div>
@@ -1425,7 +1408,7 @@ export function DesktopHostSwitcherInline() {
className="w-full justify-center"
onClick={() => setOpen(true)}
>
<RiServerLine className="h-4 w-4" />
<Icon name="server" className="h-4 w-4" />
{t('desktopHostSwitcher.actions.switchInstance')}
</Button>
<DesktopHostSwitcherDialog open={open} onOpenChange={setOpen} />
@@ -7,12 +7,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { copyTextToClipboard } from '@/lib/clipboard';
import { cn } from '@/lib/utils';
import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop';
import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps';
import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore';
import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react';
import { useI18n } from '@/lib/i18n';
const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
@@ -167,7 +167,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
)}
aria-label={t('openInApp.actions.chooseAppAria')}
>
<RiArrowDownSLine className="h-4 w-4" />
<Icon name="arrow-down-s" className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -176,7 +176,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
style={{ translate: '-30px 0' }}
>
<DropdownMenuItem className="flex items-center gap-2" onClick={() => void handleCopyPath()}>
<RiFileCopyLine className="h-4 w-4" />
<Icon name="file-copy" className="h-4 w-4" />
<span className="typography-ui-label text-foreground">{t('openInApp.actions.copyPath')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
@@ -195,7 +195,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
/>
<span className="typography-ui-label text-foreground">{app.label}</span>
{selectedApp.id === app.id ? (
<RiCheckLine className="ml-auto h-4 w-4 text-primary" />
<Icon name="check" className="ml-auto h-4 w-4 text-primary" />
) : null}
</DropdownMenuItem>
);
@@ -205,7 +205,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
className="flex items-center gap-2"
onClick={() => void loadInstalledApps(true)}
>
<RiRefreshLine className="h-4 w-4" />
<Icon name="refresh" className="h-4 w-4" />
<span className="typography-ui-label text-foreground">{t('openInApp.actions.refreshApps')}</span>
</DropdownMenuItem>
) : null}