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:
@@ -1,20 +1,4 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiFileAddLine,
|
||||
RiFileCopyLine,
|
||||
RiFolder3Fill,
|
||||
RiFolderAddLine,
|
||||
RiFolderOpenFill,
|
||||
RiFolderReceivedLine,
|
||||
RiLoader4Line,
|
||||
RiMore2Fill,
|
||||
RiRefreshLine,
|
||||
RiSearchLine,
|
||||
RiDownloadLine,
|
||||
} from '@remixicon/react';
|
||||
|
||||
import { toast } from '@/components/ui';
|
||||
import {
|
||||
@@ -48,6 +32,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { cn, getRevealLabelKey } from '@/lib/utils';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -221,9 +206,9 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
>
|
||||
{isDir ? (
|
||||
isExpanded ? (
|
||||
<RiFolderOpenFill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
<Icon name="folder-open-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
) : (
|
||||
<RiFolder3Fill className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
<Icon name="folder-3-fill" className="h-4 w-4 flex-shrink-0 text-primary/60" />
|
||||
)
|
||||
) : (
|
||||
getFileIcon(node.path, node.extension)
|
||||
@@ -252,13 +237,13 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
className="h-6 w-6"
|
||||
onClick={handleMenuButtonClick}
|
||||
>
|
||||
<RiMore2Fill className="h-4 w-4" />
|
||||
<Icon name="more-2-fill" className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="bottom" onCloseAutoFocus={() => setContextMenuPath(null)}>
|
||||
{canRename && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('rename', node); }}>
|
||||
<RiEditLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.rename')}
|
||||
<Icon name="edit" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.rename')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
@@ -271,19 +256,19 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
toast.error(t('sidebarFilesTree.toast.copyFailed'));
|
||||
});
|
||||
}}>
|
||||
<RiFileCopyLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.copyPath')}
|
||||
<Icon name="file-copy" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.copyPath')}
|
||||
</DropdownMenuItem>
|
||||
{!isDir && downloadFile && (
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void downloadFile(node.path);
|
||||
}}>
|
||||
<RiDownloadLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.save')}
|
||||
<Icon name="download" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.save')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canReveal && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onRevealPath(node.path); }}>
|
||||
<RiFolderReceivedLine className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())}
|
||||
<Icon name="folder-received" className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isDir && (canCreateFile || canCreateFolder) && (
|
||||
@@ -291,12 +276,12 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
<DropdownMenuSeparator />
|
||||
{canCreateFile && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFile', node); }}>
|
||||
<RiFileAddLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFile')}
|
||||
<Icon name="file-add" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFile')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canCreateFolder && (
|
||||
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onOpenDialog('createFolder', node); }}>
|
||||
<RiFolderAddLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFolder')}
|
||||
<Icon name="folder-add" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.newFolder')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
@@ -308,7 +293,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.delete')}
|
||||
<Icon name="delete-bin" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.delete')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -817,7 +802,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
<section className="flex h-full min-h-0 flex-col overflow-hidden bg-sidebar">
|
||||
<div className="flex items-center gap-2 border-b border-border/40 px-3 py-2">
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<RiSearchLine className="pointer-events-none absolute left-2 top-2 h-4 w-4 text-muted-foreground" />
|
||||
<Icon name="search" className="pointer-events-none absolute left-2 top-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchQuery}
|
||||
@@ -835,7 +820,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
<Icon name="close" className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -847,7 +832,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
title={t('sidebarFilesTree.actions.newFileTitle')}
|
||||
>
|
||||
<RiFileAddLine className="h-4 w-4" />
|
||||
<Icon name="file-add" className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{canCreateFolder && (
|
||||
@@ -858,11 +843,11 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
title={t('sidebarFilesTree.actions.newFolderTitle')}
|
||||
>
|
||||
<RiFolderAddLine className="h-4 w-4" />
|
||||
<Icon name="folder-add" className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={() => void refreshRoot()} className="h-8 w-8 p-0 flex-shrink-0" title={t('sidebarFilesTree.actions.refreshTitle')}>
|
||||
<RiRefreshLine className="h-4 w-4" />
|
||||
<Icon name="refresh" className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -870,7 +855,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
<ul className="flex flex-col">
|
||||
{searching ? (
|
||||
<li className="flex items-center gap-1.5 px-2 py-1 typography-meta text-muted-foreground">
|
||||
<RiLoader4Line className="h-4 w-4 animate-spin" />
|
||||
<Icon name="loader-4" className="h-4 w-4 animate-spin" />
|
||||
{t('sidebarFilesTree.state.searching')}
|
||||
</li>
|
||||
) : searchResults.length > 0 ? (
|
||||
@@ -956,7 +941,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
onClick={() => void handleDialogSubmit()}
|
||||
disabled={isDialogSubmitting || (activeDialog !== 'delete' && !dialogInputValue.trim())}
|
||||
>
|
||||
{isDialogSubmitting ? <RiLoader4Line className="animate-spin" /> : (
|
||||
{isDialogSubmitting ? <Icon name="loader-4" className="animate-spin" /> : (
|
||||
activeDialog === 'delete' ? t('sidebarFilesTree.dialog.delete.confirm') : t('sidebarFilesTree.dialog.confirm')
|
||||
)}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user