| import { useState } from "react"; | |
| import { X, Search, Folder, Grid, Filter, Plus, Tag } from "lucide-react"; | |
| import { useAppStore } from "../store"; | |
| const ALL_TAGS = [ | |
| "anatomy", | |
| "environment", | |
| "character", | |
| "lighting", | |
| "cyberpunk", | |
| "fantasy", | |
| "sci-fi", | |
| ]; | |
| const mockLibrary = Array.from({ length: 24 }).map((_, i) => ({ | |
| url: `https://picsum.photos/id/${100 + i}/400/400`, | |
| tags: [ALL_TAGS[i % ALL_TAGS.length], ALL_TAGS[(i + 2) % ALL_TAGS.length]], | |
| })); | |
| export const LibraryPanel = () => { | |
| const { isLibraryOpen, setIsLibraryOpen, setImages, pan, zoom } = | |
| useAppStore(); | |
| const [search, setSearch] = useState(""); | |
| const [activeTag, setActiveTag] = useState<string | null>(null); | |
| const handleAdd = (src: string) => { | |
| const newImg = { | |
| id: Math.random().toString(36).substr(2, 9), | |
| url: src, | |
| x: (-pan.x + window.innerWidth / 4) / zoom, | |
| y: (-pan.y + window.innerHeight / 4) / zoom, | |
| width: 250, | |
| height: 250, | |
| aspectRatio: 1, | |
| }; | |
| setImages((prev) => [...prev, newImg]); | |
| }; | |
| const filteredLibrary = mockLibrary.filter((img) => { | |
| if (activeTag && !img.tags.includes(activeTag)) return false; | |
| if (search && !img.tags.some((t) => t.includes(search.toLowerCase()))) | |
| return false; | |
| return true; | |
| }); | |
| return ( | |
| <div | |
| className={`absolute left-0 top-0 h-full w-[45%] max-w-[500px] bg-panel-bg shadow-2xl flex flex-col z-[60] transform transition-transform duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] ${isLibraryOpen ? "translate-x-0" : "-translate-x-full"}`} | |
| > | |
| <div className="flex items-center justify-between p-4 bg-panel-bg z-10 border-b border-[#3A3A3E]"> | |
| <div className="flex items-center gap-2 text-[#E0E0E0] text-[14px] font-medium tracking-tight"> | |
| <Folder size={16} className="text-[#0A84FF]" /> | |
| Asset Library | |
| </div> | |
| <button | |
| onClick={() => setIsLibraryOpen(false)} | |
| className="text-[#A0A0A0] hover:text-[#E0E0E0] p-1.5 rounded-md hover:bg-white/5 transition-colors" | |
| > | |
| <X size={16} /> | |
| </button> | |
| </div> | |
| <div className="px-4 py-3 bg-[#2A2A2E] flex flex-col gap-3"> | |
| <div className="relative group"> | |
| <Search | |
| size={14} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-[#808080] group-focus-within:text-[#0A84FF] transition-colors" | |
| /> | |
| <input | |
| type="text" | |
| value={search} | |
| onChange={(e) => setSearch(e.target.value)} | |
| placeholder="Search assets or tags..." | |
| className="w-full bg-[#1C1C1E] text-[#E0E0E0] pl-9 pr-3 py-2 text-[13px] rounded-lg border border-[#3A3A3E] focus:border-[#0A84FF] outline-none transition-all placeholder:text-[#808080]" | |
| /> | |
| </div> | |
| {/* Tag Pills */} | |
| <div className="flex items-center gap-1.5 overflow-x-auto hide-scrollbar pb-1"> | |
| <button | |
| onClick={() => setActiveTag(null)} | |
| className={`whitespace-nowrap px-3 py-1 rounded-full text-[11px] font-medium transition-colors ${!activeTag ? "bg-[#0A84FF] text-white" : "bg-[#3A3A3E] text-[#C0C0C0] hover:bg-[#4A4A4E]"}`} | |
| > | |
| All | |
| </button> | |
| {ALL_TAGS.map((tag) => ( | |
| <button | |
| key={tag} | |
| onClick={() => setActiveTag(tag)} | |
| className={`whitespace-nowrap px-3 py-1 rounded-full text-[11px] font-medium transition-colors flex items-center gap-1 ${activeTag === tag ? "bg-[#0A84FF] text-white" : "bg-[#3A3A3E] text-[#C0C0C0] hover:bg-[#4A4A4E]"}`} | |
| > | |
| <Tag size={10} /> | |
| {tag} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-y-auto bg-[#1C1C1E] p-4 custom-scrollbar"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <span className="text-[11px] font-medium text-[#808080] uppercase tracking-widest"> | |
| Images ({filteredLibrary.length}) | |
| </span> | |
| <Grid size={14} className="text-[#808080]" /> | |
| </div> | |
| <div className="grid grid-cols-3 gap-3"> | |
| <div className="aspect-square bg-white/5 hover:bg-white/10 border border-dashed border-white/20 hover:border-white/30 rounded-xl cursor-pointer flex flex-col items-center justify-center text-[#A0A0A0] hover:text-[#E0E0E0] transition-all group shadow-sm"> | |
| <div className="w-8 h-8 rounded-full bg-black/20 flex items-center justify-center mb-2 group-hover:scale-110 transition-transform"> | |
| <Plus size={16} /> | |
| </div> | |
| <span className="text-[11px] font-medium">Upload File</span> | |
| </div> | |
| {filteredLibrary.map((img, i) => ( | |
| <div | |
| key={i} | |
| className="aspect-square bg-[#2A2A2E] rounded-xl cursor-pointer group relative overflow-hidden shadow-sm ring-1 ring-[#3A3A3E] hover:ring-[#0A84FF] transition-all" | |
| onClick={() => handleAdd(img.url)} | |
| > | |
| <img | |
| src={img.url} | |
| className="w-full h-full object-cover opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-500 ease-out" | |
| /> | |
| <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-2 pointer-events-none"> | |
| <span className="text-[10px] text-white font-medium truncate mb-1"> | |
| IMG_{100 + i}.jpg | |
| </span> | |
| <div className="flex items-center gap-1 overflow-hidden"> | |
| {img.tags.map((t) => ( | |
| <span | |
| key={t} | |
| className="text-[9px] bg-white/20 text-white/90 px-1.5 py-0.5 rounded backdrop-blur" | |
| > | |
| {t} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |