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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user