refactor: migrate all Lucide icons to Phosphor + remove lucide-react (PR4)

Unified icon library:
- Create src/components/ui/icon.tsx as centralized icon re-export + size constants
- Migrate all 20+ files from lucide-react to @phosphor-icons/react
- Remove lucide-react from package.json dependencies
- Upgrade ESLint lucide-react rule from warn → error

Icon mappings applied:
  CheckIcon → Check, ChevronDown → CaretDown, ChevronUp → CaretUp,
  ChevronRight → CaretRight, X → X, Search → MagnifyingGlass,
  Loader2 → SpinnerGap, CornerDownLeft → ArrowElbowDownLeft,
  Image → Image, Brain → Brain, Terminal → Terminal, Book → Book,
  Dot → DotOutline, Circle → Circle

Files migrated: ui/ (6), ai-elements/ (11), layout/ (3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-11 21:51:38 +08:00
1 parent d9f2e6d614
commit 27bf396cd3
24 files changed
+107 -84

No files matched your search

+2 -2
View File
@@ -62,7 +62,7 @@ const eslintConfig = defineConfig([
files: ["src/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"warn",
"error",
{
patterns: [
{
@@ -100,7 +100,7 @@ const eslintConfig = defineConfig([
message: "Pattern components must be pure presentation — no hooks imports.",
},
{
group: ["@/lib/*", "@/lib"],
group: ["@/lib/*", "!@/lib/utils"],
message: "Pattern components must be pure presentation — no lib imports. Use @/lib/utils for cn() only.",
},
],
-1
View File
@@ -35,7 +35,6 @@
"cmdk": "^1.1.1",
"discord.js": "^14.25.1",
"electron-updater": "^6.8.3",
"lucide-react": "^0.563.0",
"markdown-it": "^14.1.1",
"motion": "^12.33.0",
"nanoid": "^5.1.6",
-1
View File
@@ -59,7 +59,6 @@
"cmdk": "^1.1.1",
"discord.js": "^14.25.1",
"electron-updater": "^6.8.3",
"lucide-react": "^0.563.0",
"markdown-it": "^14.1.1",
"motion": "^12.33.0",
"nanoid": "^5.1.6",
+4 -4
View File
@@ -1,6 +1,6 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
import type { ComponentProps, HTMLAttributes } from "react";
import { Button } from "@/components/ui/button";
@@ -11,7 +11,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { XIcon } from "lucide-react";
import { X } from "@phosphor-icons/react";
export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
@@ -59,7 +59,7 @@ export const ArtifactClose = ({
variant={variant}
{...props}
>
{children ?? <XIcon className="size-4" />}
{children ?? <X className="size-4" />}
<span className="sr-only">Close</span>
</Button>
);
@@ -94,7 +94,7 @@ export const ArtifactActions = ({
export type ArtifactActionProps = ComponentProps<typeof Button> & {
tooltip?: string;
label?: string;
icon?: LucideIcon;
icon?: PhosphorIcon;
};
export const ArtifactAction = ({
@@ -1,6 +1,6 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
import type { ComponentProps, ReactNode } from "react";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
@@ -11,7 +11,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react";
import { Brain, CaretDown, DotOutline } from "@phosphor-icons/react";
import { createContext, memo, useContext, useMemo } from "react";
interface ChainOfThoughtContextValue {
@@ -86,11 +86,11 @@ export const ChainOfThoughtHeader = memo(
)}
{...props}
>
<BrainIcon className="size-4" />
<Brain className="size-4" />
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
<CaretDown
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
@@ -103,7 +103,7 @@ export const ChainOfThoughtHeader = memo(
);
export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
icon?: LucideIcon;
icon?: PhosphorIcon;
label: ReactNode;
description?: ReactNode;
status?: "complete" | "active" | "pending";
@@ -118,7 +118,7 @@ const stepStatusStyles = {
export const ChainOfThoughtStep = memo(
({
className,
icon: Icon = DotIcon,
icon: Icon = DotOutline,
label,
description,
status = "complete",
+2 -2
View File
@@ -20,7 +20,7 @@ import {
import { cn } from "@/lib/utils";
import { useThemeFamily } from "@/lib/theme/context";
import { resolveShikiTheme, resolveShikiThemes, SHIKI_DEFAULT_LIGHT, SHIKI_DEFAULT_DARK } from "@/lib/theme/code-themes";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Check, Copy } from "@phosphor-icons/react";
import {
createContext,
memo,
@@ -526,7 +526,7 @@ export const CodeBlockCopyButton = ({
[]
);
const Icon = isCopied ? CheckIcon : CopyIcon;
const Icon = isCopied ? Check : Copy;
return (
<Button
+3 -3
View File
@@ -4,7 +4,7 @@ import type { ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
import { ArrowDown, DownloadSimple } from "@phosphor-icons/react";
import { useCallback } from "react";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
@@ -94,7 +94,7 @@ export const ConversationScrollButton = ({
variant="outline"
{...props}
>
<ArrowDownIcon className="size-4" />
<ArrowDown className="size-4" />
</Button>
)
);
@@ -161,7 +161,7 @@ export const ConversationDownload = ({
variant="outline"
{...props}
>
{children ?? <DownloadIcon className="size-4" />}
{children ?? <DownloadSimple className="size-4" />}
</Button>
);
};
+3 -3
View File
@@ -19,7 +19,7 @@ import { cjk } from "@streamdown/cjk";
import { createCodePlugin } from "@streamdown/code";
import { math } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { CaretLeft, CaretRight } from "@phosphor-icons/react";
import {
createContext,
memo,
@@ -271,7 +271,7 @@ export const MessageBranchPrevious = ({
variant="ghost"
{...props}
>
{children ?? <ChevronLeftIcon size={14} />}
{children ?? <CaretLeft size={14} />}
</Button>
);
};
@@ -294,7 +294,7 @@ export const MessageBranchNext = ({
variant="ghost"
{...props}
>
{children ?? <ChevronRightIcon size={14} />}
{children ?? <CaretRight size={14} />}
</Button>
);
};
+11 -11
View File
@@ -56,12 +56,12 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
CornerDownLeftIcon,
ImageIcon,
PlusIcon,
SquareIcon,
XIcon,
} from "lucide-react";
ArrowElbowDownLeft,
Image,
Plus,
Square,
X,
} from "@phosphor-icons/react";
import { nanoid } from "nanoid";
import {
Children,
@@ -353,7 +353,7 @@ export const PromptInputActionAddAttachments = ({
return (
<DropdownMenuItem {...props} onSelect={handleSelect}>
<ImageIcon className="mr-2 size-4" /> {label}
<Image className="mr-2 size-4" /> {label}
</DropdownMenuItem>
);
};
@@ -1062,7 +1062,7 @@ export const PromptInputActionMenuTrigger = ({
}: PromptInputActionMenuTriggerProps) => (
<DropdownMenuTrigger asChild>
<PromptInputButton className={className} {...props}>
{children ?? <PlusIcon className="size-4" />}
{children ?? <Plus className="size-4" />}
</PromptInputButton>
</DropdownMenuTrigger>
);
@@ -1107,14 +1107,14 @@ export const PromptInputSubmit = ({
}: PromptInputSubmitProps) => {
const isGenerating = status === "submitted" || status === "streaming";
let Icon = <CornerDownLeftIcon className="size-4" />;
let Icon = <ArrowElbowDownLeft className="size-4" />;
if (status === "submitted") {
Icon = <Spinner />;
} else if (status === "streaming") {
Icon = <SquareIcon className="size-4" />;
Icon = <Square className="size-4" />;
} else if (status === "error") {
Icon = <XIcon className="size-4" />;
Icon = <X className="size-4" />;
}
const handleClick = useCallback(
+3 -3
View File
@@ -13,7 +13,7 @@ import { cjk } from "@streamdown/cjk";
import { code } from "@streamdown/code";
import { math } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import { Brain, CaretDown } from "@phosphor-icons/react";
import {
createContext,
memo,
@@ -184,9 +184,9 @@ export const ReasoningTrigger = memo(
>
{children ?? (
<>
<BrainIcon className="size-4" />
<Brain className="size-4" />
{getThinkingMessage(isStreaming, duration)}
<ChevronDownIcon
<CaretDown
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
+3 -3
View File
@@ -8,7 +8,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { BookIcon, ChevronDownIcon } from "lucide-react";
import { Book, CaretDown } from "@phosphor-icons/react";
export type SourcesProps = ComponentProps<"div">;
@@ -36,7 +36,7 @@ export const SourcesTrigger = ({
{children ?? (
<>
<p className="font-medium">Used {count} sources</p>
<ChevronDownIcon className="h-4 w-4" />
<CaretDown className="h-4 w-4" />
</>
)}
</CollapsibleTrigger>
@@ -70,7 +70,7 @@ export const Source = ({ href, title, children, ...props }: SourceProps) => (
>
{children ?? (
<>
<BookIcon className="h-4 w-4" />
<Book className="h-4 w-4" />
<span className="block font-medium">{title}</span>
</>
)}
+3 -3
View File
@@ -8,7 +8,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { ChevronDownIcon, SearchIcon } from "lucide-react";
import { CaretDown, MagnifyingGlass } from "@phosphor-icons/react";
export type TaskItemFileProps = ComponentProps<"div">;
@@ -59,9 +59,9 @@ export const TaskTrigger = ({
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? (
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
<SearchIcon className="size-4" />
<MagnifyingGlass className="size-4" />
<p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
<CaretDown className="size-4 transition-transform group-data-[state=open]:rotate-180" />
</div>
)}
</CollapsibleTrigger>
+3 -3
View File
@@ -5,7 +5,7 @@ import type { ComponentProps, HTMLAttributes } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Ansi from "ansi-to-react";
import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react";
import { Check, Copy, Terminal as TerminalIcon, Trash } from "@phosphor-icons/react";
import {
createContext,
useCallback,
@@ -191,7 +191,7 @@ export const TerminalCopyButton = ({
[]
);
const Icon = isCopied ? CheckIcon : CopyIcon;
const Icon = isCopied ? Check : Copy;
return (
<Button
@@ -233,7 +233,7 @@ export const TerminalClearButton = ({
variant="ghost"
{...props}
>
{children ?? <Trash2Icon size={14} />}
{children ?? <Trash size={14} />}
</Button>
);
};
+16 -16
View File
@@ -11,13 +11,13 @@ import {
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import {
CheckCircleIcon,
ChevronDownIcon,
CircleIcon,
ClockIcon,
WrenchIcon,
XCircleIcon,
} from "lucide-react";
CheckCircle,
CaretDown,
Circle,
Clock,
Wrench,
XCircle,
} from "@phosphor-icons/react";
import { createContext, isValidElement, useContext, useState } from "react";
import { CodeBlock } from "./code-block";
@@ -73,13 +73,13 @@ const statusLabels: Record<ToolPart["state"], string> = {
};
const statusIcons: Record<ToolPart["state"], ReactNode> = {
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
"input-available": <ClockIcon className="size-4 animate-pulse" />,
"input-streaming": <CircleIcon className="size-4" />,
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
"output-error": <XCircleIcon className="size-4 text-red-600" />,
"approval-requested": <Clock className="size-4 text-yellow-600" />,
"approval-responded": <CheckCircle className="size-4 text-blue-600" />,
"input-available": <Clock className="size-4 animate-pulse" />,
"input-streaming": <Circle className="size-4" />,
"output-available": <CheckCircle className="size-4 text-green-600" />,
"output-denied": <XCircle className="size-4 text-orange-600" />,
"output-error": <XCircle className="size-4 text-red-600" />,
};
export const getStatusBadge = (status: ToolPart["state"]) => (
@@ -109,11 +109,11 @@ export const ToolHeader = ({
{...props}
>
<div className="flex items-center gap-2">
<WrenchIcon className="size-4 text-muted-foreground" />
<Wrench className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">{title ?? derivedName}</span>
{getStatusBadge(state)}
</div>
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
<CaretDown className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
);
};
+4 -3
View File
@@ -14,8 +14,9 @@ import {
Plus,
FolderOpen,
UserCircle,
Columns,
X,
} from "@phosphor-icons/react";
import { Columns2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -453,7 +454,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
{isSplitActive && (
<div className="mb-2 rounded-lg border border-border/60 bg-muted/30 p-1.5">
<div className="flex items-center gap-1.5 px-2 py-1">
<Columns2 className="h-3.5 w-3.5 text-muted-foreground" />
<Columns className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">{t('split.splitGroup')}</span>
</div>
<div className="mt-0.5 flex flex-col gap-0.5">
@@ -660,7 +661,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
});
}}
>
<Columns2 className="h-3.5 w-3.5" />
<Columns className="h-3.5 w-3.5" />
</button>
)}
{/* Streaming indicator: hidden when hover shows split icon */}
+2 -2
View File
@@ -1,7 +1,7 @@
"use client";
import React, { useState } from "react";
import { CircleAlert } from "lucide-react";
import { WarningCircle } from "@phosphor-icons/react";
import { useTranslation } from "@/hooks/useTranslation";
interface ErrorBoundaryProps {
@@ -30,7 +30,7 @@ function ErrorFallback({
<div className="flex h-full w-full items-center justify-center bg-background p-8">
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<CircleAlert size={24} />
<WarningCircle size={24} />
</div>
<h2 className="text-lg font-semibold text-foreground">
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { X } from "lucide-react";
import { X } from "@phosphor-icons/react";
import type { Message, MessagesResponse, ChatSession } from "@/types";
import { ChatView } from "@/components/chat/ChatView";
import { Button } from "@/components/ui/button";
+2 -2
View File
@@ -2,7 +2,7 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { MagnifyingGlass } from "@phosphor-icons/react"
import { cn } from "@/lib/utils"
import {
@@ -69,7 +69,7 @@ function CommandInput({
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<MagnifyingGlass className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
+2 -2
View File
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { XIcon } from "lucide-react"
import { X } from "@phosphor-icons/react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -72,7 +72,7 @@ function DialogContent({
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<X />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
+4 -4
View File
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { Check, CaretRight, Circle } from "@phosphor-icons/react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -100,7 +100,7 @@ function DropdownMenuCheckboxItem({
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
<Check className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -135,7 +135,7 @@ function DropdownMenuRadioItem({
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
<Circle className="size-2" weight="fill" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -217,7 +217,7 @@ function DropdownMenuSubTrigger({
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
<CaretRight className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
+24
View File
@@ -0,0 +1,24 @@
// Unified icon re-exports + size constants for the project
export {
Check,
X,
CaretDown,
CaretUp,
CaretRight,
MagnifyingGlass,
SpinnerGap,
ArrowElbowDownLeft,
Image,
Brain,
Terminal,
Book,
DotOutline,
Circle,
Plus,
Copy,
Trash,
} from "@phosphor-icons/react";
export type { Icon, IconProps } from "@phosphor-icons/react";
export const ICON_SIZE = { sm: 14, md: 16, lg: 20, xl: 24 } as const;
+5 -5
View File
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Check, CaretDown, CaretUp } from "@phosphor-icons/react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -44,7 +44,7 @@ function SelectTrigger({
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
<CaretDown className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
@@ -119,7 +119,7 @@ function SelectItem({
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
<Check className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
@@ -153,7 +153,7 @@ function SelectScrollUpButton({
)}
{...props}
>
<ChevronUpIcon className="size-4" />
<CaretUp className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
@@ -171,7 +171,7 @@ function SelectScrollDownButton({
)}
{...props}
>
<ChevronDownIcon className="size-4" />
<CaretDown className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
+2 -2
View File
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { XIcon } from "lucide-react"
import { X } from "@phosphor-icons/react"
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -76,7 +76,7 @@ function SheetContent({
{children}
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<X className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
+2 -2
View File
@@ -1,10 +1,10 @@
import { Loader2Icon } from "lucide-react"
import { SpinnerGap } from "@phosphor-icons/react"
import { cn } from "@/lib/utils"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
<SpinnerGap
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}