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,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { RiAddLine, RiBrainAi3Line, RiCloseLine, RiSearchLine, RiStarFill, RiTimeLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isIMECompositionEvent } from '@/lib/ime';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
@@ -79,7 +79,7 @@ export const ModelChip: React.FC<{
|
||||
onClick={onRemove}
|
||||
className="text-muted-foreground hover:text-foreground ml-0.5"
|
||||
>
|
||||
<RiCloseLine className="h-3.5 w-3.5" />
|
||||
<Icon name="close" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -349,7 +349,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<RiAddLine className="h-3.5 w-3.5 mr-1" />
|
||||
<Icon name="add" className="h-3.5 w-3.5 mr-1" />
|
||||
{addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
|
||||
</Button>
|
||||
|
||||
@@ -426,7 +426,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-border/40">
|
||||
<div className="relative">
|
||||
<RiSearchLine className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
@@ -455,7 +455,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{filteredFavorites.length > 0 && (
|
||||
<>
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiStarFill className="h-4 w-4 text-primary" />
|
||||
<Icon name="star-fill" className="h-4 w-4 text-primary" />
|
||||
{t('multirun.modelMultiSelect.sections.favorites')}
|
||||
</div>
|
||||
{filteredFavorites.map(({ model, providerID, modelID }) => {
|
||||
@@ -470,7 +470,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
<>
|
||||
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiTimeLine className="h-4 w-4" />
|
||||
<Icon name="time" className="h-4 w-4" />
|
||||
{t('multirun.modelMultiSelect.sections.recent')}
|
||||
</div>
|
||||
{filteredRecents.map(({ model, providerID, modelID }) => {
|
||||
@@ -554,7 +554,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
size="chip"
|
||||
className="px-2 gap-1.5 rounded-md !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 typography-meta font-medium text-foreground"
|
||||
>
|
||||
<RiBrainAi3Line
|
||||
<Icon name="brain-ai-3"
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 flex-shrink-0',
|
||||
variantValue === DEFAULT_VARIANT_VALUE ? 'text-muted-foreground' : 'text-[color:var(--status-info)]'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { RiAddLine, RiArrowDownSLine, RiAttachment2, RiCloseLine, RiFileImageLine, RiFileLine, RiFolderLine, RiInformationLine, RiTerminalLine } from '@remixicon/react';
|
||||
import { toast } from '@/components/ui';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -21,6 +20,7 @@ import { BranchSelector, useBranchOptions } from './BranchSelector';
|
||||
import { AgentSelector } from './AgentSelector';
|
||||
import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from '@/components/chat/CommandAutocomplete';
|
||||
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { isDesktopShell } from '@/lib/desktop';
|
||||
import { useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -60,7 +60,7 @@ const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button" tabIndex={-1} className="inline-flex items-center justify-center text-muted-foreground/50 hover:text-muted-foreground transition-colors">
|
||||
<RiInformationLine className="h-3.5 w-3.5" />
|
||||
<Icon name="information" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[240px]">
|
||||
@@ -168,7 +168,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
iconColor: currentTheme.colors.surface.foreground,
|
||||
},
|
||||
);
|
||||
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
|
||||
const projectIconName = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
|
||||
const iconColor = project.color ? PROJECT_COLOR_MAP[project.color] : undefined;
|
||||
|
||||
return (
|
||||
@@ -180,10 +180,10 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
>
|
||||
<img src={imageUrl} alt="" className="h-full w-full object-contain" draggable={false} />
|
||||
</span>
|
||||
) : ProjectIcon ? (
|
||||
<ProjectIcon className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
|
||||
) : projectIconName ? (
|
||||
<Icon name={projectIconName} className="h-3.5 w-3.5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
|
||||
) : (
|
||||
<RiFolderLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined} />
|
||||
<Icon name="folder" className="h-3.5 w-3.5 shrink-0 text-muted-foreground/80" style={iconColor ? { color: iconColor } : undefined}/>
|
||||
)}
|
||||
<span className="truncate">{displayLabel}</span>
|
||||
</span>
|
||||
@@ -615,7 +615,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
aria-label={t('multirun.launcher.actions.closeEsc')}
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
<Icon name="close" className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -716,7 +716,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
{/* ── Setup commands (collapsible, full width) ── */}
|
||||
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
|
||||
<CollapsibleTrigger className="w-full flex items-center gap-2 py-1.5 px-2 -mx-2 rounded-lg hover:bg-[var(--interactive-hover)]/50 transition-colors group">
|
||||
<RiTerminalLine className="h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<Icon name="terminal" className="h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<span className="typography-meta font-medium text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
{t('multirun.launcher.setupCommands.label')}
|
||||
</span>
|
||||
@@ -733,7 +733,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
{configuredSetupCount}
|
||||
</span>
|
||||
)}
|
||||
<RiArrowDownSLine className={cn(
|
||||
<Icon name="arrow-down-s" className={cn(
|
||||
'h-3.5 w-3.5 text-muted-foreground/50 transition-transform duration-200 ml-auto',
|
||||
isSetupCommandsOpen && 'rotate-180'
|
||||
)} />
|
||||
@@ -765,7 +765,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label={t('multirun.launcher.setupCommands.removeCommandAria')}
|
||||
>
|
||||
<RiCloseLine className="h-3.5 w-3.5" />
|
||||
<Icon name="close" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -774,7 +774,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
onClick={() => setSetupCommands([...setupCommands, ''])}
|
||||
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors px-1"
|
||||
>
|
||||
<RiAddLine className="h-3 w-3" />
|
||||
<Icon name="add" className="h-3 w-3" />
|
||||
{t('multirun.launcher.setupCommands.addCommand')}
|
||||
</button>
|
||||
</>
|
||||
@@ -854,7 +854,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="inline-flex items-center gap-1 h-6 px-2 rounded-md typography-micro text-muted-foreground hover:text-foreground hover:bg-[var(--interactive-hover)]/50 transition-colors"
|
||||
>
|
||||
<RiAttachment2 className="h-3 w-3" />
|
||||
<Icon name="attachment-2" className="h-3 w-3" />
|
||||
{t('multirun.launcher.attachments.attach')}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -871,9 +871,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
}}
|
||||
>
|
||||
{file.mimeType.startsWith('image/') ? (
|
||||
<RiFileImageLine className="h-3 w-3 text-muted-foreground" />
|
||||
<Icon name="file-image" className="h-3 w-3 text-muted-foreground" />
|
||||
) : (
|
||||
<RiFileLine className="h-3 w-3 text-muted-foreground" />
|
||||
<Icon name="file" className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
<span className="truncate max-w-[100px]" title={file.filename}>
|
||||
{file.filename}
|
||||
@@ -883,7 +883,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
||||
onClick={() => handleRemoveFile(file.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<RiCloseLine className="h-3 w-3" />
|
||||
<Icon name="close" className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user