File size: 6,256 Bytes
3d7d9b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 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>
);
};
|