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
@@ -7,12 +7,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiAddLine,
RiCloseLine,
RiDeleteBinLine,
RiFolderLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useI18n } from '@/lib/i18n';
@@ -67,7 +62,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={iconButtonClass}
aria-label={t('sessions.sidebar.bulkActions.moveToFolder')}
>
<RiFolderLine className="h-4 w-4" />
<Icon name="folder" className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -87,7 +82,7 @@ export const BulkActionBar: React.FC<Props> = ({
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onCreateFolderAndMove}>
<RiAddLine className="mr-1 h-4 w-4" />
<Icon name="add" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.newFolderEllipsis')}
</DropdownMenuItem>
{canRemoveFromFolder ? (
@@ -95,7 +90,7 @@ export const BulkActionBar: React.FC<Props> = ({
onClick={onRemoveFromFolder}
className="text-destructive focus:text-destructive"
>
<RiCloseLine className="mr-1 h-4 w-4" />
<Icon name="close" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.removeFromFolder')}
</DropdownMenuItem>
) : null}
@@ -111,7 +106,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={cn(destructiveIconButtonClass)}
aria-label={destructiveLabel}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{destructiveLabel}</p></TooltipContent>
@@ -125,7 +120,7 @@ export const BulkActionBar: React.FC<Props> = ({
className={iconButtonClass}
aria-label={t('sessions.sidebar.header.actions.exitSelection')}
>
<RiCloseLine className="h-4 w-4" />
<Icon name="close" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.header.actions.exitSelection')}</p></TooltipContent>
@@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import type { Session } from '@opencode-ai/sdk/v2';
import { useI18n } from '@/lib/i18n';
@@ -65,7 +65,7 @@ export function SessionDeleteConfirmDialog(props: {
className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
aria-pressed={!showDeletionDialog}
>
{!showDeletionDialog ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
{!showDeletionDialog ? <Icon name="checkbox" className="h-4 w-4 text-primary" /> : <Icon name="checkbox-blank" className="h-4 w-4" />}
{t('sessions.sidebar.dialogs.neverAsk')}
</button>
<div className="flex items-center gap-2">
@@ -135,7 +135,7 @@ export function BulkSessionDeleteConfirmDialog(props: {
className="inline-flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
aria-pressed={!showDeletionDialog}
>
{!showDeletionDialog ? <RiCheckboxLine className="h-4 w-4 text-primary" /> : <RiCheckboxBlankLine className="h-4 w-4" />}
{!showDeletionDialog ? <Icon name="checkbox" className="h-4 w-4 text-primary" /> : <Icon name="checkbox-blank" className="h-4 w-4" />}
{t('sessions.sidebar.dialogs.neverAsk')}
</button>
<div className="flex items-center gap-2">
@@ -1,15 +1,7 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowLeftLongLine,
RiArrowRightSLine,
RiDeleteBinLine,
RiGitBranchLine,
} from '@remixicon/react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { sessionEvents } from '@/lib/sessionEvents';
import type { MainTab } from '@/stores/useUIStore';
@@ -536,7 +528,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center gap-1 leading-none align-middle">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -544,7 +536,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
{prIndicator.url ? (
@@ -568,7 +560,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -593,12 +585,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : group.isArchivedBucket ? (
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiArchiveLine className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<Icon name="archive" className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
<span className={cn(
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -606,7 +598,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
) : (!group.isMain || group.worktree) ? (
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<RiGitBranchLine
<Icon name="git-branch"
className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
@@ -614,7 +606,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
</span>
<span className="min-w-0 flex-1 truncate">{renderHighlightedText(group.label, normalizedSessionSearchQuery)}</span>
@@ -626,16 +618,15 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{showBranchSubtitle && statusLine ? (
<span className="inline-flex min-w-0 items-center gap-1.5 leading-tight">
{group.isArchivedBucket ? (
<RiArchiveLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
<Icon name="archive" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
) : (!group.isMain || isGitProject) ? (
showInlinePrTitle && prIndicator ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
<RiGitBranchLine
className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} align="start" className="max-w-sm">
@@ -645,7 +636,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{baseBranchLabel && headBranchLabel ? (
<>
<span>{baseBranchLabel}</span>
<RiArrowLeftLongLine className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<Icon name="arrow-left-long" className="mx-0.5 inline h-3 w-3 align-[-2px]" />
<span>{headBranchLabel}</span>
</>
) : (
@@ -666,10 +657,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
) : (
<RiGitBranchLine
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}
/>
<Icon name="git-branch" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
style={branchIconColor ? { color: branchIconColor } : undefined}/>
)
) : null}
<span
@@ -698,7 +688,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteArchivedInGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteArchivedSessions')}</p></TooltipContent>
@@ -722,7 +712,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.deleteGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
<Icon name="delete-bin" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteWorktree')}</p></TooltipContent>
@@ -745,7 +735,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.group.actions.newDraftInGroupAria', { label: group.label })}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.project.actions.newDraftSession')}</p></TooltipContent>
@@ -11,34 +11,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowRightSLine,
RiChat4Line,
RiCheckLine,
RiCloseLine,
RiDeleteBinLine,
RiDownloadLine,
RiErrorWarningLine,
RiFileCopyLine,
RiFolderLine,
RiLinkUnlinkM,
RiMore2Line,
RiPencilAiLine,
RiPushpinLine,
RiShare2Line,
RiShieldLine,
RiUnpinLine,
RiGitBranchLine,
RiWindowLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Icon } from "@/components/icon/Icon";
import { buildExportFilename, downloadAsMarkdown, formatSessionAsMarkdown, getExportRevealLabelKey, revealExportedMarkdown, saveAsMarkdownDesktop } from '@/lib/exportSession';
import type { ChildSessionExport } from '@/lib/exportSession';
import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSessionStatus, useSession, useSessionPermissions } from '@/sync/sync-context';
@@ -479,7 +457,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
title={t('sessions.sidebar.session.rename.save')}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCheckLine className="size-4" />
<Icon name="check" className="size-4" />
</button>
<button
type="button"
@@ -488,17 +466,17 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
title={t('sessions.sidebar.session.rename.cancel')}
className="shrink-0 text-muted-foreground hover:text-foreground"
>
<RiCloseLine className="size-4" />
<Icon name="close" className="size-4" />
</button>
</form>
{!isMinimalMode ? (
<div className="flex items-center justify-between gap-3 text-muted-foreground/60 min-w-0 overflow-hidden leading-tight" style={{ fontSize: 'calc(var(--text-ui-label) * 0.85)' }}>
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
{hasChildren ? <span className="inline-flex items-center justify-center flex-shrink-0">{isExpanded ? <RiArrowDownSLine className="h-3 w-3" /> : <RiArrowRightSLine className="h-3 w-3" />}</span> : null}
{hasChildren ? <span className="inline-flex items-center justify-center flex-shrink-0">{isExpanded ? <Icon name="arrow-down-s" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}</span> : null}
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-status-error/65">/-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
</div>
) : null}
@@ -537,7 +515,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
)}
>
{showStatusMarker ? statusMarkerContent : null}
{isPinnedSession ? <RiPushpinLine className="h-3 w-3 flex-shrink-0 text-primary" aria-label={t('sessions.sidebar.session.status.pinned')} /> : null}
{isPinnedSession ? <Icon name="pushpin" className="h-3 w-3 flex-shrink-0 text-primary" aria-label={t('sessions.sidebar.session.status.pinned')}/> : null}
</span>
) : null;
const subsessionChevron = hasChildren ? (
@@ -566,12 +544,12 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
? t('sessions.sidebar.session.subsessions.collapse')
: t('sessions.sidebar.session.subsessions.expand')}
>
{isExpanded ? <RiArrowDownSLine className="h-3 w-3" /> : <RiArrowRightSLine className="h-3 w-3" />}
{isExpanded ? <Icon name="arrow-down-s" className="h-3 w-3" /> : <Icon name="arrow-right-s" className="h-3 w-3" />}
</span>
) : null;
const streamingIndicator = isZombie
? <RiErrorWarningLine className="h-4 w-4 text-status-warning" />
? <Icon name="error-warning" className="h-4 w-4 text-status-warning" />
: null;
const handleMenuOpenChange = (open: boolean) => {
@@ -663,33 +641,33 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
}}
className="[&>svg]:mr-1"
>
<RiPencilAiLine className="mr-1 h-4 w-4" />
<Icon name="pencil-ai" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.rename')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => togglePinnedSession(session.id)} className="[&>svg]:mr-1">
{isPinnedSession ? <RiUnpinLine className="mr-1 h-4 w-4" /> : <RiPushpinLine className="mr-1 h-4 w-4" />}
{isPinnedSession ? <Icon name="unpin" className="mr-1 h-4 w-4" /> : <Icon name="pushpin" className="mr-1 h-4 w-4" />}
{isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')}
</DropdownMenuItem>
{!resolvedSession.share ? (
<DropdownMenuItem onClick={() => handleShareSession(resolvedSession)} className="[&>svg]:mr-1">
<RiShare2Line className="mr-1 h-4 w-4" />
<Icon name="share-2" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.share')}
</DropdownMenuItem>
) : (
<>
<DropdownMenuItem onClick={() => { if (resolvedSession.share?.url) handleCopyShareUrl(resolvedSession.share.url, session.id); }} className="[&>svg]:mr-1">
{copiedSessionId === session.id
? <><RiCheckLine className="mr-1 h-4 w-4" style={{ color: 'var(--status-success)' }} />{t('sessions.sidebar.session.menu.copied')}</>
: <><RiFileCopyLine className="mr-1 h-4 w-4" />{t('sessions.sidebar.session.menu.copyLink')}</>}
? <><Icon name="check" className="mr-1 h-4 w-4" style={{ color: 'var(--status-success)' }}/>{t('sessions.sidebar.session.menu.copied')}</>
: <><Icon name="file-copy" className="mr-1 h-4 w-4" />{t('sessions.sidebar.session.menu.copyLink')}</>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleUnshareSession(session.id)} className="[&>svg]:mr-1">
<RiLinkUnlinkM className="mr-1 h-4 w-4" />
<Icon name="link-unlink-m" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.unshare')}
</DropdownMenuItem>
</>
)}
<DropdownMenuItem onClick={() => { void handleExportSession(); }} className="[&>svg]:mr-1">
<RiDownloadLine className="mr-1 h-4 w-4" />
<Icon name="download" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.exportMarkdown')}
</DropdownMenuItem>
@@ -700,7 +678,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger className="[&>svg]:mr-1"><RiFolderLine className="h-4 w-4" />{t('sessions.sidebar.folders.moveToFolder')}</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger className="[&>svg]:mr-1"><Icon name="folder" className="h-4 w-4" />{t('sessions.sidebar.folders.moveToFolder')}</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="min-w-[180px]">
{scopeFolders.length === 0 ? (
<DropdownMenuItem disabled className="text-muted-foreground">{t('sessions.sidebar.folders.none')}</DropdownMenuItem>
@@ -708,18 +686,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
scopeFolders.map((folder) => (
<DropdownMenuItem key={folder.id} onClick={() => { if (currentFolderId === folder.id) removeSessionFromFolder(sessionDirectory, session.id); else addSessionToFolder(sessionDirectory, folder.id, session.id); }}>
<span className="flex-1 truncate">{folder.name}</span>
{currentFolderId === folder.id ? <RiCheckLine className="ml-2 h-3.5 w-3.5 text-primary flex-shrink-0" /> : null}
{currentFolderId === folder.id ? <Icon name="check" className="ml-2 h-3.5 w-3.5 text-primary flex-shrink-0" /> : null}
</DropdownMenuItem>
))
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => { const newFolder = createFolderAndStartRename(sessionDirectory); if (!newFolder) return; addSessionToFolder(sessionDirectory, newFolder.id, session.id); }}>
<RiAddLine className="mr-1 h-4 w-4" />
<Icon name="add" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.newFolderEllipsis')}
</DropdownMenuItem>
{currentFolderId ? (
<DropdownMenuItem onClick={() => { removeSessionFromFolder(sessionDirectory, session.id); }} className="text-destructive focus:text-destructive">
<RiCloseLine className="mr-1 h-4 w-4" />
<Icon name="close" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.folders.removeFromFolder')}
</DropdownMenuItem>
) : null}
@@ -742,7 +720,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
}}
className="[&>svg]:mr-1"
>
<RiChat4Line className="mr-1 h-4 w-4" />
<Icon name="chat-4" className="mr-1 h-4 w-4" />
<span className="truncate">{t('sessions.sidebar.session.menu.openInSidePanel')}</span>
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">{t('sessions.sidebar.session.menu.betaBadge')}</span>
</DropdownMenuItem>
@@ -754,14 +732,14 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleOpenMiniChatWindow}
className="[&>svg]:mr-1"
>
<RiWindowLine className="mr-1 h-4 w-4" />
<Icon name="window" className="mr-1 h-4 w-4" />
<span className="truncate">{t('sessions.sidebar.session.menu.openMiniChatWindow')}</span>
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive [&>svg]:mr-1" onClick={() => handleDeleteSession(session, { archivedBucket })}>
<RiDeleteBinLine className="mr-1 h-4 w-4" />
<Icon name="delete-bin" className="mr-1 h-4 w-4" />
{archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -824,7 +802,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
) : null}
{pendingPermissionCount > 0 ? (
<span className="inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0" title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
<RiShieldLine className="h-3 w-3" />
<Icon name="shield" className="h-3 w-3" />
<span className="leading-none">{pendingPermissionCount}</span>
</span>
) : null}
@@ -841,7 +819,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className={cn('flex items-center gap-3 text-left text-muted-foreground', secondaryMeta?.branchLabel ? 'justify-between' : 'justify-start')}>
{secondaryMeta?.branchLabel ? (
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
<span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0" /><span className="truncate">{secondaryMeta.branchLabel}</span></span>
</div>
) : null}
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0.5"><span className="text-status-success">+{sessionDiffStats.additions}</span><span className="text-status-error">-{sessionDiffStats.deletions}</span></span> : null}
@@ -875,7 +853,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
{pendingPermissionCount > 0 ? (
<span className="inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0" title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
<RiShieldLine className="h-3 w-3" />
<Icon name="shield" className="h-3 w-3" />
<span className="leading-none">{pendingPermissionCount}</span>
</span>
) : null}
@@ -887,7 +865,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<span className="flex-shrink-0">{sessionUpdatedLabel}</span>
{sessionDiffStats ? <span className="flex flex-shrink-0 items-center gap-0 text-[0.92em]"><span className="text-status-success/80">+{sessionDiffStats.additions}</span><span className="text-muted-foreground/60">/</span><span className="text-status-error/65">-{sessionDiffStats.deletions}</span></span> : null}
{hasSecondaryProjectLabel ? <span className="truncate">{secondaryMeta?.projectLabel}</span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><RiGitBranchLine className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
{hasSecondaryBranchLabel ? <span className="inline-flex min-w-0 items-center gap-0.5"><Icon name="git-branch" className="h-3 w-3 flex-shrink-0 text-muted-foreground/70" /><span className="truncate">{secondaryMeta?.branchLabel}</span></span> : null}
</div>
</div>
) : null}
@@ -924,7 +902,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleQuickArchiveClick}
onKeyDown={(event) => event.stopPropagation()}
>
<RiArchiveLine className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
<Icon name="archive" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
@@ -950,7 +928,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
onClick={handleMenuTriggerClick}
onKeyDown={(event) => event.stopPropagation()}
>
<RiMore2Line className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
<Icon name="more-2" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
</button>
</DropdownMenuTrigger>
{sessionMenuContent}
@@ -1,8 +1,8 @@
import React from 'react';
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import type { SessionNode } from './types';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
type ActivityItem = {
node: SessionNode;
@@ -77,7 +77,7 @@ export function SidebarActivitySections({ sections, renderSessionNode }: Props):
aria-expanded={!isCollapsed}
>
<span className="inline-flex h-4 w-4 items-center justify-center text-muted-foreground">
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
<span className="text-[14px] font-normal text-foreground/95">{section.title}</span>
</button>
@@ -1,7 +1,7 @@
import React from 'react';
import { RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
type Props = {
@@ -32,7 +32,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenSettings} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.settings')}>
<RiSettings3Line className="h-4.5 w-4.5" />
<Icon name="settings-3" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.settings')}</p></TooltipContent>
@@ -40,7 +40,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenShortcuts} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.shortcuts')}>
<RiQuestionLine className="h-4.5 w-4.5" />
<Icon name="question" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.shortcuts')}</p></TooltipContent>
@@ -48,7 +48,7 @@ export function SidebarFooter({
<Tooltip>
<TooltipTrigger asChild>
<button type="button" onClick={onOpenAbout} className={footerButtonClassName} aria-label={t('sessions.sidebar.footer.actions.aboutOpenChamber')}>
<RiInformationLine className="h-4.5 w-4.5" />
<Icon name="information" className="h-4.5 w-4.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}><p>{t('sessions.sidebar.footer.actions.aboutOpenChamber')}</p></TooltipContent>
@@ -7,21 +7,9 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiCheckLine,
RiCheckboxMultipleLine,
RiChatNewLine,
RiEqualizer2Line,
RiFolderAddLine,
RiLayoutLeftLine,
RiSearchLine,
RiCloseLine,
RiContractUpDownLine,
RiExpandUpDownLine,
RiCalendarScheduleLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { Icon } from "@/components/icon/Icon";
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useI18n } from '@/lib/i18n';
@@ -118,7 +106,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50"
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
<Icon name="layout-left" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.closeSessions')}</p></TooltipContent>
@@ -132,7 +120,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.addProject')}
>
<RiFolderAddLine className={headerActionIconClass} />
<Icon name="folder-add" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.addProject')}</p></TooltipContent>
@@ -145,7 +133,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.newSession')}
>
<RiChatNewLine className={headerActionIconClass} />
<Icon name="chat-new" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.newSession')}</p></TooltipContent>
@@ -176,7 +164,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.scheduledTasks')}
>
<RiCalendarScheduleLine className={headerActionIconClass} />
<Icon name="calendar-schedule" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.scheduledTasks')}</p></TooltipContent>
@@ -191,7 +179,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
aria-label={t('sessions.sidebar.header.actions.searchSessions')}
aria-expanded={isSessionSearchOpen}
>
<RiSearchLine className={headerActionIconClass} />
<Icon name="search" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.searchSessions')}</p></TooltipContent>
@@ -208,7 +196,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
: t('sessions.sidebar.header.actions.selectSessions')}
aria-pressed={selectionModeEnabled}
>
<RiCheckboxMultipleLine className={headerActionIconClass} />
<Icon name="checkbox-multiple" className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
@@ -227,7 +215,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className={headerActionButtonClass}
aria-label={t('sessions.sidebar.header.actions.sessionDisplayMode')}
>
<RiEqualizer2Line className={headerActionIconClass} />
<Icon name="equalizer-2" className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -239,14 +227,14 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'default' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{displayMode === 'minimal' ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -254,15 +242,15 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="flex items-center justify-between"
>
<span>{t('sessions.sidebar.header.displayMode.showRecent')}</span>
{showRecentSection ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
{showRecentSection ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={collapseAllProjects} className="flex items-center gap-2">
<RiContractUpDownLine className="h-4 w-4" />
<Icon name="contract-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.collapseAll')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={expandAllProjects} className="flex items-center gap-2">
<RiExpandUpDownLine className="h-4 w-4" />
<Icon name="expand-up-down" className="h-4 w-4" />
<span>{t('sessions.sidebar.header.displayMode.expandAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -281,7 +269,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
<span>{t('sessions.sidebar.header.search.escapeHint')}</span>
</div>
<div className="relative">
<RiSearchLine className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Icon name="search" className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
ref={sessionSearchInputRef}
value={sessionSearchQuery}
@@ -306,7 +294,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
className="absolute right-1 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.header.search.clear')}
>
<RiCloseLine className="h-3.5 w-3.5" />
<Icon name="close" className="h-3.5 w-3.5" />
</button>
) : null}
</div>
@@ -10,7 +10,7 @@ import {
useDroppable,
type DragEndEvent,
} from '@dnd-kit/core';
import { RiStickyNoteLine } from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
export const DraggableSessionRow: React.FC<{
sessionId: string;
@@ -122,7 +122,7 @@ export const SessionFolderDndScope: React.FC<{
}}
className="flex items-center rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2.5 py-1 shadow-none pointer-events-none"
>
<RiStickyNoteLine className="h-4 w-4 text-muted-foreground mr-2 flex-shrink-0" />
<Icon name="sticky-note" className="h-4 w-4 text-muted-foreground mr-2 flex-shrink-0" />
<div className="min-w-0 flex-1 truncate typography-ui-label font-normal text-foreground">
{activeDragTitle}
</div>
@@ -8,16 +8,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
import {
RiAddLine,
RiArrowDownSLine,
RiArrowRightSLine,
RiCloseLine,
RiFolderLine,
RiMore2Line,
RiNodeTree,
RiPencilAiLine,
} from '@remixicon/react';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import { useThemeSystem } from '@/contexts/useThemeSystem';
@@ -104,7 +95,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
setImageFailed(false);
}, [id, projectIconImage?.updatedAt]);
const ProjectIcon = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
const projectIconName = projectIcon ? PROJECT_ICON_MAP[projectIcon] : null;
const iconColor = projectColor ? (PROJECT_COLOR_MAP[projectColor] ?? null) : null;
const imageUrl = !imageFailed
? getProjectIconImageUrl({ id, iconImage: projectIconImage }, {
@@ -186,7 +177,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
'h-3.5 w-3.5 items-center justify-center text-muted-foreground',
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex',
)}>
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
{isCollapsed ? <Icon name="arrow-right-s" className="h-3.5 w-3.5" /> : <Icon name="arrow-down-s" className="h-3.5 w-3.5" />}
</span>
{imageUrl ? (
<span
@@ -204,10 +195,10 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
onError={() => setImageFailed(true)}
/>
</span>
) : ProjectIcon ? (
<ProjectIcon className={cn('h-3.5 w-3.5', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
) : projectIconName ? (
<Icon name={projectIconName} className={cn('h-3.5 w-3.5', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
) : (
<RiFolderLine className={cn('h-3.5 w-3.5 text-muted-foreground/80', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
<Icon name="folder" className={cn('h-3.5 w-3.5 text-muted-foreground/80', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
)}
</span>
<span className={cn(
@@ -242,7 +233,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
)}
aria-label={t('sessions.sidebar.project.actions.newWorktree')}
>
<RiNodeTree className="h-4 w-4" />
<Icon name="node-tree" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
@@ -271,25 +262,25 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
onMouseDown={handleMenuTriggerMouseDown}
onClick={handleMenuTriggerClick}
>
<RiMore2Line className="h-3.5 w-3.5" />
<Icon name="more-2" className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[180px]">
{showCreateButtons && !isRepo && !hideDirectoryControls && onNewSession && (
<DropdownMenuItem onClick={onNewSession}>
<RiAddLine className="mr-1.5 h-4 w-4" />
<Icon name="add" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.project.actions.newSession')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onRenameStart}>
<RiPencilAiLine className="mr-1.5 h-4 w-4" />
<Icon name="pencil-ai" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.session.menu.rename')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={onClose}
className="text-destructive focus:text-destructive"
>
<RiCloseLine className="mr-1.5 h-4 w-4" />
<Icon name="close" className="mr-1.5 h-4 w-4" />
{t('sessions.sidebar.project.actions.closeProject')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -314,7 +305,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
? t('sessions.sidebar.project.actions.newDraftSession')
: t('sessions.sidebar.project.actions.newSession')}
>
<RiAddLine className="h-4 w-4" />
<Icon name="add" className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>