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:
@@ -7,31 +7,6 @@ import { useCommandsStore } from '@/stores/useCommandsStore';
|
||||
import { useMcpConfigStore } from '@/stores/useMcpConfigStore';
|
||||
import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import {
|
||||
RiAiAgentLine,
|
||||
RiAiGenerate2,
|
||||
RiArrowLeftSLine,
|
||||
RiBarChart2Line,
|
||||
RiBookLine,
|
||||
RiBookOpenLine,
|
||||
RiChatAi3Line,
|
||||
RiChatHistoryLine,
|
||||
RiCloseLine,
|
||||
RiCommandLine,
|
||||
RiCloudLine,
|
||||
RiFoldersLine,
|
||||
RiGitBranchLine,
|
||||
RiGlobalLine,
|
||||
RiMicLine,
|
||||
RiListUnordered,
|
||||
RiNotification3Line,
|
||||
RiPaletteLine,
|
||||
RiRobot2Line,
|
||||
RiRestartLine,
|
||||
RiServerLine,
|
||||
RiSlashCommands2,
|
||||
RiBrainLine,
|
||||
} from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
import { AgentsSidebar } from '@/components/sections/agents/AgentsSidebar';
|
||||
@@ -56,10 +31,11 @@ import { MagicPromptsPage } from '@/components/sections/magic-prompts/MagicPromp
|
||||
import { GitPage } from '@/components/sections/git-identities/GitPage';
|
||||
import type { OpenChamberSection } from '@/components/sections/openchamber/types';
|
||||
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import type { IconName } from "@/components/icon/icons";
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import {
|
||||
SETTINGS_PAGE_METADATA,
|
||||
@@ -125,54 +101,54 @@ function isPageAvailable(page: SettingsPageMeta, ctx: SettingsRuntimeContext): b
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<{ className?: string }> | null {
|
||||
export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
|
||||
switch (slug) {
|
||||
case 'projects':
|
||||
return RiFoldersLine;
|
||||
return 'folders';
|
||||
case 'remote-instances':
|
||||
return RiServerLine;
|
||||
return 'server';
|
||||
case 'appearance':
|
||||
return RiPaletteLine;
|
||||
return 'palette';
|
||||
case 'chat':
|
||||
return RiChatAi3Line;
|
||||
return 'chat-ai-3';
|
||||
case 'magic-prompts':
|
||||
return RiAiGenerate2;
|
||||
return 'ai-generate-2';
|
||||
case 'notifications':
|
||||
return RiNotification3Line;
|
||||
return 'notification-3';
|
||||
case 'shortcuts':
|
||||
return RiCommandLine;
|
||||
return 'command';
|
||||
case 'sessions':
|
||||
return RiChatHistoryLine;
|
||||
return 'chat-history';
|
||||
|
||||
case 'providers':
|
||||
return RiCloudLine;
|
||||
return 'cloud';
|
||||
case 'agents':
|
||||
return RiAiAgentLine;
|
||||
return 'ai-agent';
|
||||
case 'behavior':
|
||||
return RiBrainLine;
|
||||
return 'brain';
|
||||
case 'commands':
|
||||
return RiSlashCommands2;
|
||||
return 'slash-commands-2';
|
||||
case 'mcp':
|
||||
return McpIcon;
|
||||
return 'plug-2';
|
||||
|
||||
case 'skills.installed':
|
||||
return RiBookOpenLine;
|
||||
return 'book-open';
|
||||
case 'skills.catalog':
|
||||
return RiBookLine;
|
||||
return 'book';
|
||||
|
||||
case 'git':
|
||||
return RiGitBranchLine;
|
||||
return 'git-branch';
|
||||
|
||||
case 'usage':
|
||||
return RiBarChart2Line;
|
||||
return 'bar-chart-2';
|
||||
case 'voice':
|
||||
return RiMicLine;
|
||||
return 'mic';
|
||||
case 'tunnel':
|
||||
return RiGlobalLine;
|
||||
return 'global';
|
||||
case 'home':
|
||||
return null;
|
||||
default:
|
||||
return RiRobot2Line;
|
||||
return 'robot-2';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,8 +560,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<div className="flex flex-col gap-0.5 pt-4 pb-2 px-2">
|
||||
{sortedFilteredPages.map((page) => {
|
||||
const selected = settingsSlug === page.slug;
|
||||
const Icon = getSettingsNavIcon(page.slug);
|
||||
if (!Icon) return null;
|
||||
const iconName = getSettingsNavIcon(page.slug);
|
||||
if (!iconName) return null;
|
||||
|
||||
return (
|
||||
<Tooltip key={page.slug}>
|
||||
@@ -601,7 +577,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
: 'text-foreground hover:bg-interactive-hover'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<Icon name={iconName} className="h-4 w-4 shrink-0" />
|
||||
<span className="flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150 opacity-100">
|
||||
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
|
||||
{(page.slug === 'voice' || page.slug === 'tunnel') && (
|
||||
@@ -633,7 +609,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
)}
|
||||
onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] })}
|
||||
>
|
||||
<RiRestartLine className="h-4 w-4 shrink-0" />
|
||||
<Icon name="restart" className="h-4 w-4 shrink-0" />
|
||||
<span>{t('settings.view.actions.reloadOpenCode')}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -734,7 +710,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
aria-label={showBackButton ? t('settings.view.actions.backToSettings') : t('settings.view.actions.closeSettings')}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiArrowLeftSLine className="h-5 w-5" />
|
||||
<Icon name="arrow-left-s" className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div className="min-w-0 flex-1 typography-ui-label font-medium text-foreground truncate">
|
||||
@@ -750,7 +726,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
aria-label={t('settings.view.actions.openSectionList')}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiListUnordered className="h-5 w-5" />
|
||||
<Icon name="list-unordered" className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -762,7 +738,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
<Icon name="close" className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -776,7 +752,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
aria-label={t('settings.view.actions.back')}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiArrowLeftSLine className="h-5 w-5" />
|
||||
<Icon name="arrow-left-s" className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -790,7 +766,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
<Icon name="close" className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user