120 lines
4.3 KiB
TypeScript
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]">
|
|
“{search}” 결과 없음
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IconPicker;
|