Files
invyone/frontend/lib/registry/components/common/IconPicker.tsx
T

120 lines
4.3 KiB
TypeScript

"use client";
import React, { useState, useMemo } from "react";
import * as LucideIcons from "lucide-react";
// lucide-react에서 실제 아이콘만 추출 (유틸/타입 제외)
const ICON_ENTRIES = Object.entries(LucideIcons).filter(
([name, comp]) =>
typeof comp === "object" &&
name !== "default" &&
name !== "createLucideIcon" &&
name !== "icons" &&
name[0] === name[0].toUpperCase() &&
!name.endsWith("Icon"),
);
// 자주 쓰는 아이콘 (상단 표시)
const POPULAR = [
"Search", "Plus", "Edit", "Trash2", "Save", "Check", "X", "ChevronDown",
"ChevronRight", "Settings", "User", "Users", "Mail", "Calendar", "Clock",
"File", "Folder", "Download", "Upload", "Eye", "EyeOff", "Lock", "Unlock",
"Star", "Heart", "Home", "ArrowLeft", "ArrowRight", "RefreshCw", "Filter",
"BarChart3", "PieChart", "TrendingUp", "DollarSign", "ShoppingCart", "Package",
];
interface IconPickerProps {
value?: string;
onChange: (iconName: string) => void;
className?: string;
}
export const IconPicker: React.FC<IconPickerProps> = ({ value, onChange, className }) => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
if (!search.trim()) {
// 인기 아이콘 + 나머지 (최대 80개)
const popularSet = new Set(POPULAR);
const popular = ICON_ENTRIES.filter(([n]) => popularSet.has(n));
const rest = ICON_ENTRIES.filter(([n]) => !popularSet.has(n)).slice(0, 80 - popular.length);
return [...popular, ...rest];
}
const q = search.toLowerCase();
return ICON_ENTRIES.filter(([name]) => name.toLowerCase().includes(q)).slice(0, 80);
}, [search]);
// 현재 선택된 아이콘 렌더
const SelectedIcon = value ? (LucideIcons as any)[value] : null;
return (
<div className={`relative ${className || ""}`}>
<button
type="button"
onClick={() => setOpen(!open)}
className="border-border bg-background flex w-full items-center gap-2 rounded border px-2 py-1 text-xs"
>
{SelectedIcon ? (
<>
<SelectedIcon size={14} />
<span className="flex-1 truncate text-left font-mono">{value}</span>
</>
) : (
<span className="text-muted-foreground flex-1 text-left"> ...</span>
)}
<LucideIcons.ChevronDown size={12} className="text-muted-foreground shrink-0" />
</button>
{open && (
<div className="border-border bg-card absolute top-full right-0 left-0 z-50 mt-1 rounded border shadow-lg">
<div className="border-border border-b p-1.5">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="아이콘 검색..."
className="border-border bg-background w-full rounded border px-2 py-1 text-[0.65rem]"
autoFocus
/>
</div>
<div className="grid max-h-48 grid-cols-6 gap-0.5 overflow-y-auto p-1.5">
{/* 선택 해제 */}
<button
type="button"
onClick={() => { onChange(""); setOpen(false); }}
className="hover:bg-accent text-muted-foreground flex h-7 items-center justify-center rounded text-[0.55rem]"
title="없음"
>
--
</button>
{filtered.map(([name, Icon]) => {
const IconComp = Icon as React.FC<{ size?: number }>;
return (
<button
key={name}
type="button"
onClick={() => { onChange(name); setOpen(false); setSearch(""); }}
className={`flex h-7 items-center justify-center rounded transition-colors ${
name === value ? "bg-primary/10 text-primary" : "hover:bg-accent text-foreground"
}`}
title={name}
>
<IconComp size={14} />
</button>
);
})}
</div>
{filtered.length === 0 && (
<div className="text-muted-foreground p-3 text-center text-[0.6rem]">
&ldquo;{search}&rdquo;
</div>
)}
</div>
)}
</div>
);
};
export default IconPicker;