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:
Bohdan Triapitsyn
2026-05-13 13:26:35 +03:00
parent b4cbd7f0d6
commit 14357257ae
173 changed files with 2342 additions and 2227 deletions
+21 -41
View File
@@ -1,24 +1,3 @@
import {
RiBrainAi3Line,
RiCheckboxCircleLine,
RiBugLine,
RiCodeLine,
RiCommandLine,
RiFileTextLine,
RiFlaskLine,
RiGitBranchLine,
RiHammerLine,
RiPlayLine,
RiRocketLine,
RiRobot2Line,
RiSearchLine,
RiServerLine,
RiSettings3Line,
RiStackLine,
RiTerminalBoxLine,
RiToolsLine,
} from '@remixicon/react';
import type { ComponentType } from 'react';
import type {
OpenChamberProjectAction,
OpenChamberProjectActionPlatform,
@@ -27,6 +6,7 @@ import type {
DesktopSshInstance,
DesktopSshPortForward,
} from '@/lib/desktopSsh';
import type { IconName } from "@/components/icon/icons";
export type ProjectActionIconKey =
| 'play'
@@ -51,31 +31,31 @@ export type ProjectActionIconKey =
export const PROJECT_ACTION_ICONS: Array<{
key: ProjectActionIconKey;
label: string;
Icon: ComponentType<{ className?: string }>;
Icon: IconName;
}> = [
{ key: 'play', label: 'Play', Icon: RiPlayLine },
{ key: 'build', label: 'Build', Icon: RiHammerLine },
{ key: 'lint', label: 'Lint', Icon: RiCheckboxCircleLine },
{ key: 'terminal', label: 'Terminal', Icon: RiTerminalBoxLine },
{ key: 'tools', label: 'Tools', Icon: RiToolsLine },
{ key: 'bug', label: 'Bug', Icon: RiBugLine },
{ key: 'flask', label: 'Flask', Icon: RiFlaskLine },
{ key: 'rocket', label: 'Rocket', Icon: RiRocketLine },
{ key: 'code', label: 'Code', Icon: RiCodeLine },
{ key: 'server', label: 'Server', Icon: RiServerLine },
{ key: 'branch', label: 'Branch', Icon: RiGitBranchLine },
{ key: 'search', label: 'Search', Icon: RiSearchLine },
{ key: 'settings', label: 'Settings', Icon: RiSettings3Line },
{ key: 'brain', label: 'Brain', Icon: RiBrainAi3Line },
{ key: 'stack', label: 'Stack', Icon: RiStackLine },
{ key: 'robot', label: 'Robot', Icon: RiRobot2Line },
{ key: 'command', label: 'Command', Icon: RiCommandLine },
{ key: 'file', label: 'File', Icon: RiFileTextLine },
{ key: 'play', label: 'Play', Icon: 'play' },
{ key: 'build', label: 'Build', Icon: 'hammer' },
{ key: 'lint', label: 'Lint', Icon: 'checkbox-circle' },
{ key: 'terminal', label: 'Terminal', Icon: 'terminal-box' },
{ key: 'tools', label: 'Tools', Icon: 'tools' },
{ key: 'bug', label: 'Bug', Icon: 'bug' },
{ key: 'flask', label: 'Flask', Icon: 'flask' },
{ key: 'rocket', label: 'Rocket', Icon: 'rocket' },
{ key: 'code', label: 'Code', Icon: 'code' },
{ key: 'server', label: 'Server', Icon: 'server' },
{ key: 'branch', label: 'Branch', Icon: 'git-branch' },
{ key: 'search', label: 'Search', Icon: 'search' },
{ key: 'settings', label: 'Settings', Icon: 'settings-3' },
{ key: 'brain', label: 'Brain', Icon: 'brain-ai-3' },
{ key: 'stack', label: 'Stack', Icon: 'stack' },
{ key: 'robot', label: 'Robot', Icon: 'robot-2' },
{ key: 'command', label: 'Command', Icon: 'command' },
{ key: 'file', label: 'File', Icon: 'file-text' },
];
export const PROJECT_ACTION_ICON_MAP = Object.fromEntries(
PROJECT_ACTION_ICONS.map((entry) => [entry.key, entry.Icon])
) as Record<ProjectActionIconKey, ComponentType<{ className?: string }>>;
) as Record<ProjectActionIconKey, IconName>;
export const PROJECT_ACTIONS_UPDATED_EVENT = 'openchamber:project-actions-updated';
+22 -43
View File
@@ -1,52 +1,31 @@
import {
RiCodeBoxLine,
RiTerminalBoxLine,
RiRocketLine,
RiFlaskLine,
RiGamepadLine,
RiBriefcaseLine,
RiHomeLine,
RiGlobalLine,
RiLeafLine,
RiShieldLine,
RiPaletteLine,
RiServerLine,
RiSmartphoneLine,
RiDatabase2Line,
RiLightbulbLine,
RiMusicLine,
RiCameraLine,
RiBookOpenLine,
RiHeartLine,
type RemixiconComponentType,
} from '@remixicon/react';
import type { ProjectEntry } from '@/lib/api/types';
import type { IconName } from "@/components/icon/icons";
type ThemeVariant = 'light' | 'dark';
export const PROJECT_ICONS: Array<{ key: string; Icon: RemixiconComponentType; label: string }> = [
{ key: 'code', Icon: RiCodeBoxLine, label: 'Code' },
{ key: 'terminal', Icon: RiTerminalBoxLine, label: 'Terminal' },
{ key: 'rocket', Icon: RiRocketLine, label: 'Rocket' },
{ key: 'flask', Icon: RiFlaskLine, label: 'Lab' },
{ key: 'gamepad', Icon: RiGamepadLine, label: 'Game' },
{ key: 'briefcase', Icon: RiBriefcaseLine, label: 'Work' },
{ key: 'home', Icon: RiHomeLine, label: 'Home' },
{ key: 'globe', Icon: RiGlobalLine, label: 'Web' },
{ key: 'leaf', Icon: RiLeafLine, label: 'Nature' },
{ key: 'shield', Icon: RiShieldLine, label: 'Security' },
{ key: 'palette', Icon: RiPaletteLine, label: 'Design' },
{ key: 'server', Icon: RiServerLine, label: 'Server' },
{ key: 'phone', Icon: RiSmartphoneLine, label: 'Mobile' },
{ key: 'database', Icon: RiDatabase2Line, label: 'Data' },
{ key: 'lightbulb', Icon: RiLightbulbLine, label: 'Idea' },
{ key: 'music', Icon: RiMusicLine, label: 'Music' },
{ key: 'camera', Icon: RiCameraLine, label: 'Media' },
{ key: 'book', Icon: RiBookOpenLine, label: 'Docs' },
{ key: 'heart', Icon: RiHeartLine, label: 'Favorite' },
export const PROJECT_ICONS: Array<{ key: string; Icon: IconName; label: string }> = [
{ key: 'code', Icon: 'code-box', label: 'Code' },
{ key: 'terminal', Icon: 'terminal-box', label: 'Terminal' },
{ key: 'rocket', Icon: 'rocket', label: 'Rocket' },
{ key: 'flask', Icon: 'flask', label: 'Lab' },
{ key: 'gamepad', Icon: 'gamepad', label: 'Game' },
{ key: 'briefcase', Icon: 'briefcase', label: 'Work' },
{ key: 'home', Icon: 'home', label: 'Home' },
{ key: 'globe', Icon: 'global', label: 'Web' },
{ key: 'leaf', Icon: 'leaf', label: 'Nature' },
{ key: 'shield', Icon: 'shield', label: 'Security' },
{ key: 'palette', Icon: 'palette', label: 'Design' },
{ key: 'server', Icon: 'server', label: 'Server' },
{ key: 'phone', Icon: 'smartphone', label: 'Mobile' },
{ key: 'database', Icon: 'database-2', label: 'Data' },
{ key: 'lightbulb', Icon: 'lightbulb', label: 'Idea' },
{ key: 'music', Icon: 'music', label: 'Music' },
{ key: 'camera', Icon: 'camera', label: 'Media' },
{ key: 'book', Icon: 'book-open', label: 'Docs' },
{ key: 'heart', Icon: 'heart', label: 'Favorite' },
];
export const PROJECT_ICON_MAP: Record<string, RemixiconComponentType> = Object.fromEntries(
export const PROJECT_ICON_MAP: Record<string, IconName> = Object.fromEntries(
PROJECT_ICONS.map((i) => [i.key, i.Icon])
);