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:
@@ -12,8 +12,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { toast } from '@/components/ui';
|
||||
import { RiStackLine, RiToolsLine, RiBrainAi3Line, RiFileImageLine, RiArrowDownSLine, RiCheckLine, RiSearchLine, RiInformationLine, RiEyeLine, RiEyeOffLine } from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
@@ -511,7 +511,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<RiStackLine className="mx-auto mb-3 h-12 w-12 opacity-50" />
|
||||
<Icon name="stack" className="mx-auto mb-3 h-12 w-12 opacity-50" />
|
||||
<p className="typography-body">{t('settings.providers.page.empty.noProvidersDetected')}</p>
|
||||
<p className="typography-meta mt-1 opacity-75">{t('settings.providers.page.empty.checkOpenCodeConfiguration')}</p>
|
||||
</div>
|
||||
@@ -561,7 +561,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
: t('settings.providers.page.connect.selectProviderPlaceholder')}
|
||||
</span>
|
||||
</span>
|
||||
<RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground/50" />
|
||||
<Icon name="arrow-down-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground/50" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@@ -573,7 +573,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
className="flex items-center gap-2 border-b border-[var(--surface-subtle)] px-3 py-2"
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<RiSearchLine className="h-4 w-4 text-muted-foreground" />
|
||||
<Icon name="search" className="h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={providerSearchQuery}
|
||||
@@ -608,7 +608,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
<span className="truncate">{provider.name || provider.id}</span>
|
||||
</span>
|
||||
{candidateProviderId === provider.id && (
|
||||
<RiCheckLine className="h-4 w-4 text-[var(--primary-base)]" />
|
||||
<Icon name="check" className="h-4 w-4 text-[var(--primary-base)]" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
));
|
||||
@@ -636,7 +636,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
{t('settings.providers.page.auth.apiKeyLabel')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
<Icon name="information" className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
{t('settings.providers.page.auth.apiKeyTooltip')}
|
||||
@@ -772,7 +772,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<RiStackLine className="mx-auto mb-3 h-12 w-12 opacity-50" />
|
||||
<Icon name="stack" className="mx-auto mb-3 h-12 w-12 opacity-50" />
|
||||
<p className="typography-body">{t('settings.providers.page.empty.selectProviderFromSidebar')}</p>
|
||||
<p className="typography-meta mt-1 opacity-75">{t('settings.providers.page.empty.reviewDetailsAndConfigureAuth')}</p>
|
||||
</div>
|
||||
@@ -826,7 +826,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
{!showAuthPanel ? (
|
||||
<div className="flex items-center gap-1.5 py-1.5">
|
||||
<RiCheckLine className="w-4 h-4 text-[var(--status-success)] shrink-0" />
|
||||
<Icon name="check" className="w-4 h-4 text-[var(--status-success)] shrink-0" />
|
||||
<span className="typography-ui-label text-foreground">{t('settings.providers.page.auth.connected')}</span>
|
||||
<span className="typography-meta text-muted-foreground ml-1">{t('settings.providers.page.auth.useReconnectHint')}</span>
|
||||
</div>
|
||||
@@ -839,7 +839,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
{t('settings.providers.page.auth.apiKeyLabel')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
<Icon name="information" className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||
{t('settings.providers.page.auth.apiKeyTooltip')}
|
||||
@@ -1031,7 +1031,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<div className="relative mb-2">
|
||||
<RiSearchLine className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
value={modelQuery}
|
||||
onChange={(event) => setModelQuery(event.target.value)}
|
||||
@@ -1055,10 +1055,10 @@ export const ProvidersPage: React.FC = () => {
|
||||
const contextTokens = formatTokens(metadata?.limit?.context);
|
||||
const outputTokens = formatTokens(metadata?.limit?.output);
|
||||
|
||||
const capabilityIcons: Array<{ key: string; icon: typeof RiToolsLine; label: string }> = [];
|
||||
if (metadata?.tool_call) capabilityIcons.push({ key: 'tools', icon: RiToolsLine, label: t('settings.providers.page.models.capability.toolCalling') });
|
||||
if (metadata?.reasoning) capabilityIcons.push({ key: 'reasoning', icon: RiBrainAi3Line, label: t('settings.providers.page.models.capability.reasoning') });
|
||||
if (metadata?.attachment) capabilityIcons.push({ key: 'image', icon: RiFileImageLine, label: t('settings.providers.page.models.capability.imageInput') });
|
||||
const capabilityIcons: Array<{ key: string; icon: string; label: string }> = [];
|
||||
if (metadata?.tool_call) capabilityIcons.push({ key: 'tools', icon: "tools", label: t('settings.providers.page.models.capability.toolCalling') });
|
||||
if (metadata?.reasoning) capabilityIcons.push({ key: 'reasoning', icon: "brain-ai-3", label: t('settings.providers.page.models.capability.reasoning') });
|
||||
if (metadata?.attachment) capabilityIcons.push({ key: 'image', icon: "file-image", label: t('settings.providers.page.models.capability.imageInput') });
|
||||
|
||||
return (
|
||||
<div key={modelId} className="py-1.5">
|
||||
@@ -1081,14 +1081,14 @@ export const ProvidersPage: React.FC = () => {
|
||||
)}
|
||||
{capabilityIcons.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{capabilityIcons.map(({ key, icon: Icon, label }) => (
|
||||
{capabilityIcons.map(({ key, icon: iconName, label }) => (
|
||||
<span
|
||||
key={key}
|
||||
className="flex h-5 w-5 rounded items-center justify-center text-muted-foreground bg-[var(--surface-muted)]"
|
||||
title={label}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-3 w-3" />
|
||||
<Icon name={iconName} className="h-3 w-3" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -1100,7 +1100,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
title={isHidden ? t('settings.providers.page.models.actions.showModelInSelectors') : t('settings.providers.page.models.actions.hideModelFromSelectors')}
|
||||
aria-label={isHidden ? t('settings.providers.page.models.actions.showModel') : t('settings.providers.page.models.actions.hideModel')}
|
||||
>
|
||||
{isHidden ? <RiEyeOffLine className="h-3.5 w-3.5" /> : <RiEyeLine className="h-3.5 w-3.5" />}
|
||||
{isHidden ? <Icon name="eye-off" className="h-3.5 w-3.5" /> : <Icon name="eye" className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { RiAddLine, RiStackLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -122,7 +122,7 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
||||
aria-label={t('settings.providers.sidebar.actions.connectProviderAria')}
|
||||
title={t('settings.providers.sidebar.actions.connectProviderTitle')}
|
||||
>
|
||||
<RiAddLine className="h-3.5 w-3.5" />
|
||||
<Icon name="add" className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
|
||||
{providers.length === 0 ? (
|
||||
<div className="py-12 px-4 text-center text-muted-foreground">
|
||||
<RiStackLine className="mx-auto mb-3 h-10 w-10 opacity-50" />
|
||||
<Icon name="stack" className="mx-auto mb-3 h-10 w-10 opacity-50" />
|
||||
<p className="typography-ui-label font-medium">{t('settings.providers.sidebar.empty.title')}</p>
|
||||
<p className="typography-meta mt-1 opacity-75">{t('settings.providers.sidebar.empty.description')}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user