Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(components): introduce Icon component to replace direct reac…
…t-icons usage

- Create new Icon component with support for react-icons
- Add size and variant props for consistent icon styling
- Replace direct react-icons imports with centralized Icon component
- Remove individual react-icons imports from components
  • Loading branch information
olegshulyakov committed Oct 10, 2025
commit 8253d2936e4138ecbd92da278b2197ce0ff6a9c0
4 changes: 2 additions & 2 deletions src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LuChevronDown } from 'react-icons/lu';
import { isDev } from '../config';
import { classNames } from '../utils';
import { Button } from './Button';
import { Icon } from './Icon';

export interface DropdownOption {
value: string | number;
Expand Down Expand Up @@ -113,7 +113,7 @@ export function Dropdown<T extends DropdownOption>({
>
{currentValue}
{!hideChevron && (
<LuChevronDown className="lucide inline h-5 w-5 ml-1" />
<Icon icon="LuChevronDown" variant="rightside" size="md" />
)}
</summary>

Expand Down
8 changes: 4 additions & 4 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LuCog, LuMenu, LuSquarePen } from 'react-icons/lu';
import { useNavigate } from 'react-router';
import { useAppContext } from '../store/app';
import { useChatContext } from '../store/chat';
import { useInferenceContext } from '../store/inference';
import { Button } from './Button';
import { Dropdown } from './Dropdown';
import { Icon } from './Icon';
import { Label } from './Label';

export default function Header() {
Expand Down Expand Up @@ -42,7 +42,7 @@ export default function Header() {
<section className="flex flex-row items-center xl:hidden">
{/* open sidebar button */}
<Label variant="btn-ghost" size="icon" htmlFor="toggle-drawer">
<LuMenu className="lucide h-5 w-5" />
<Icon icon="LuMenu" size="md" />
</Label>

{/* spacer */}
Expand All @@ -68,7 +68,7 @@ export default function Header() {
title={t('header.buttons.newConv')}
aria-label={t('header.ariaLabels.newConv')}
>
<LuSquarePen className="lucide w-5 h-5" />
<Icon icon="LuSquarePen" size="md" />
</Button>
</section>

Expand Down Expand Up @@ -129,7 +129,7 @@ export default function Header() {
onClick={() => navigate('/settings')}
>
{/* settings button */}
<LuCog className="lucide w-5 h-5" />
<Icon icon="LuCog" size="md" />
</Button>
</div>
</section>
Expand Down
68 changes: 68 additions & 0 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { cva, VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { IconBaseProps } from 'react-icons';
import * as LucideIcons from 'react-icons/lu';
import { cn } from '../utils';

type IconNames = keyof Omit<typeof LucideIcons, 'IconContext'>;

const iconVariants = cva('', {
variants: {
library: {
lucide: 'lucide',
},
variant: {
spin: 'animate-spin',
current: 'fill-current',
leftside: 'inline mr-1',
rightside: 'inline ml-1',
},
size: {
xs: 'h-3 w-3',
sm: 'h-4 w-4',
md: 'h-5 w-5',
xl: 'h-8 w-8',
},
},
defaultVariants: {
library: 'lucide',
},
});

type IconProps = Omit<IconBaseProps, 'size'> &
VariantProps<typeof iconVariants> &
React.SVGAttributes<SVGSVGElement> & {
className?: string;
icon: IconNames;
};

const Icon = React.forwardRef<SVGSVGElement, IconProps>(
({ className, library = 'lucide', variant, size, icon, ...props }, ref) => {
let SelectedIcon: React.ElementType;

switch (library) {
case 'lucide':
SelectedIcon = LucideIcons[icon];
break;
default:
throw new Error(`Library "${library}" not found in icons`);
}

if (!SelectedIcon) {
throw new Error(`Icon "${icon}" not found in Lucide icons`);
}

return (
<SelectedIcon
ref={ref}
className={cn(iconVariants({ library, variant, size, className }))}
{...props}
/>
);
}
);

Icon.displayName = 'Icon';

export default Icon;
export { Icon };
21 changes: 7 additions & 14 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { Trans, useTranslation } from 'react-i18next';
import {
LuDownload,
LuEllipsisVertical,
LuPencil,
LuSquarePen,
LuTrash,
LuX,
} from 'react-icons/lu';
import { useNavigate } from 'react-router';
import IndexedDB from '../database/indexedDB';
import { useChatContext } from '../store/chat';
Expand All @@ -17,6 +9,7 @@ import { Conversation } from '../types';
import { classNames } from '../utils';
import { downloadAsFile } from '../utils/downloadAsFile';
import { Button } from './Button';
import { Icon } from './Icon';
import { Label } from './Label';

export default function Sidebar() {
Expand Down Expand Up @@ -79,7 +72,7 @@ export default function Sidebar() {
aria-label={t('sidebar.buttons.closeSideBar')}
tabIndex={0}
>
<LuX className="lucide w-5 h-5" />
<Icon icon="LuX" size="md" />
</Label>

<Label
Expand All @@ -100,7 +93,7 @@ export default function Sidebar() {
title={t('header.buttons.newConv')}
aria-label={t('header.ariaLabels.newConv')}
>
<LuSquarePen className="lucide w-5 h-5" />
<Icon icon="LuSquarePen" size="md" />
</Button>
</div>

Expand Down Expand Up @@ -261,7 +254,7 @@ const ConversationItem = memo(
title={t('sidebar.buttons.more')}
aria-label={t('sidebar.ariaLabels.more')}
>
<LuEllipsisVertical className="lucide w-5 h-5" />
<Icon icon="LuEllipsisVertical" size="md" />
</Button>
{/* dropdown menu */}
<ul
Expand All @@ -277,7 +270,7 @@ const ConversationItem = memo(
title={t('sidebar.buttons.rename')}
aria-label={t('sidebar.ariaLabels.rename')}
>
<LuPencil className="lucide w-4 h-4" />
<Icon icon="LuPencil" size="sm" />
<Trans i18nKey="sidebar.buttons.rename" />
</Button>
</li>
Expand All @@ -288,7 +281,7 @@ const ConversationItem = memo(
title={t('sidebar.buttons.download')}
aria-label={t('sidebar.ariaLabels.download')}
>
<LuDownload className="lucide w-4 h-4" />
<Icon icon="LuDownload" size="sm" />
<Trans i18nKey="sidebar.buttons.download" />
</Button>
</li>
Expand All @@ -304,7 +297,7 @@ const ConversationItem = memo(
title={t('sidebar.buttons.delete')}
aria-label={t('sidebar.ariaLabels.delete')}
>
<LuTrash className="lucide w-4 h-4" />
<Icon icon="LuTrash" size="sm" />
<Trans i18nKey="sidebar.buttons.delete" />
</Button>
</li>
Expand Down
1 change: 1 addition & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './Button';
export * from './Dropdown';
export * from './Footer';
export * from './Header';
export * from './Icon';
export * from './Label';
export * from './Sidebar';
export * from './Textarea';
Expand Down
9 changes: 4 additions & 5 deletions src/pages/Chat/components/CanvasPyInterpreter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LuPlay, LuSquare, LuX } from 'react-icons/lu';
import { Button, Textarea } from '../../../components';
import { Button, Icon, Textarea } from '../../../components';
import LocalStorage from '../../../database/localStorage';
import { useChatContext } from '../../../store/chat';
import { CanvasType } from '../../../types';
Expand Down Expand Up @@ -159,7 +158,7 @@ export default function CanvasPyInterpreter() {
title={t('codeRunner.buttons.close')}
onClick={() => setCanvasData(null)}
>
<LuX className="lucide w-5 h-5" />
<Icon icon="LuX" size="md" />
</Button>
</div>
<div className="grid grid-rows-3 gap-4 h-full">
Expand All @@ -177,7 +176,7 @@ export default function CanvasPyInterpreter() {
onClick={() => runCode(code)}
disabled={running}
>
<LuPlay className="lucide h-5 w-5 mr-1" />
<Icon icon="LuPlay" variant="leftside" size="md" />
{t('codeRunner.buttons.run')}
</Button>
{showStopBtn && (
Expand All @@ -187,7 +186,7 @@ export default function CanvasPyInterpreter() {
title={t('codeRunner.buttons.stop')}
onClick={() => interruptFn?.()}
>
<LuSquare className="lucide h-5 w-5 mr-1" />
<Icon icon="LuSquare" variant="leftside" size="md" />
{t('codeRunner.buttons.stop')}
</Button>
)}
Expand Down
19 changes: 6 additions & 13 deletions src/pages/Chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { memo, useCallback, useEffect, useMemo } from 'react';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import {
LuArrowUp,
LuCircleStop,
LuMic,
LuPaperclip,
LuSquare,
} from 'react-icons/lu';
import { TbAdjustmentsHorizontal } from 'react-icons/tb';
import { useNavigate } from 'react-router';
import { Button, Label, Textarea } from '../../../components';
import { Button, Icon, Label, Textarea } from '../../../components';
import {
ChatTextareaApi,
useChatTextarea,
Expand Down Expand Up @@ -150,7 +143,7 @@ export const ChatInput = memo(
tabIndex={0}
role="button"
>
<LuPaperclip className="lucide h-5 w-5" />
<Icon icon="LuPaperclip" size="md" />
</Label>

<Button
Expand Down Expand Up @@ -179,7 +172,7 @@ export const ChatInput = memo(
title="Record"
aria-label="Start Recording"
>
<LuMic className="h-5 w-5" />
<Icon icon="LuMic" size="md" />
</Button>
)}
{isRecording && (
Expand All @@ -191,7 +184,7 @@ export const ChatInput = memo(
title="Stop"
aria-label="Stop Recording"
>
<LuCircleStop className="h-5 w-5" />
<Icon icon="LuCircleStop" size="md" />
</Button>
)}
</>
Expand All @@ -205,7 +198,7 @@ export const ChatInput = memo(
size="icon-rounded"
onClick={handleStop}
>
<LuSquare className="lucide h-4 w-4" fill="currentColor" />
<Icon icon="LuSquare" size="sm" variant="current" />
</Button>
)}

Expand All @@ -216,7 +209,7 @@ export const ChatInput = memo(
onClick={sendNewMessage}
aria-label={t('chatInput.ariaLabels.send')}
>
<LuArrowUp className="lucide h-5 w-5" />
<Icon icon="LuArrowUp" size="md" />
</Button>
)}
</div>
Expand Down
19 changes: 13 additions & 6 deletions src/pages/Chat/components/ChatInputExtraContextItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LuFileText, LuVolume2, LuX } from 'react-icons/lu';
import { Button } from '../../../components';
import { Button, Icon } from '../../../components';
import { MessageExtra } from '../../../types';
import { classNames } from '../../../utils';

Expand Down Expand Up @@ -47,7 +46,7 @@ export default function ChatInputExtraContextItem({
size="icon-small"
onClick={() => removeItem(i)}
>
<LuX className="lucide h-3 w-3" />
<Icon icon="LuX" size="xs" />
</Button>
</div>
)}
Expand All @@ -73,9 +72,17 @@ export default function ChatInputExtraContextItem({
aria-description={t('chatInput.ariaLabels.documentIcon')}
>
{item.type === 'audioFile' ? (
<LuVolume2 className="lucide h-8 w-8 text-gray-500" />
<Icon
icon="LuVolume2"
size="xl"
className="text-gray-500"
/>
) : (
<LuFileText className="lucide h-8 w-8 text-gray-500" />
<Icon
icon="LuFileText"
size="xl"
className="text-gray-500"
/>
)}
</div>

Expand Down Expand Up @@ -103,7 +110,7 @@ export default function ChatInputExtraContextItem({
size="small"
aria-label={t('chatInput.previewDialog.closeButton')}
>
<LuX className="lucide h-5 w-5" onClick={() => setShow(-1)} />
<Icon icon="LuX" size="md" onClick={() => setShow(-1)} />
</Button>
</div>
{showingItem.type === 'imageFile' ? (
Expand Down
Loading