asdf98 commited on
Commit
1b0898d
verified
1 Parent(s): db7a938

fix: use explicit React event type imports in library panel

Browse files
Files changed (1) hide show
  1. src/components/LibraryPanel.tsx +11 -41
src/components/LibraryPanel.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
2
  import { X, Search, Folder, Grid, Plus, Tag, Trash2, RefreshCw, Upload, Edit3, Copy, ExternalLink, ChevronLeft, Image as ImageIcon, CheckCircle2, AlertCircle, Database } from 'lucide-react';
3
  import { useAppStore } from '../store';
4
  import { invoke } from '@tauri-apps/api/core';
@@ -80,7 +80,7 @@ export const LibraryPanel = () => {
80
  addToCanvas(item, true);
81
  setLibraryItems(prev => {
82
  const without = prev.filter(i => i.id !== item.id && i.hash !== item.hash);
83
- const next = duplicate ? [item, ...without] : [item, ...without];
84
  rebuildTags(next);
85
  return next;
86
  });
@@ -92,19 +92,19 @@ export const LibraryPanel = () => {
92
  reader.readAsDataURL(file);
93
  }, [libraryItems, addToCanvas, rebuildTags]);
94
 
95
- const handleBrowserDrop = (e: React.DragEvent) => {
96
  e.preventDefault(); e.stopPropagation(); setIsDragOver(false);
97
  const files = Array.from(e.dataTransfer.files);
98
  if (!files.length) return;
99
  files.forEach(importFileAsDataUrl);
100
  };
101
 
102
- const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
103
  if (e.target.files) Array.from(e.target.files).forEach(importFileAsDataUrl);
104
  e.target.value = '';
105
  };
106
 
107
- const handleDragStart = (e: React.DragEvent, item: LibItem) => {
108
  const payload = JSON.stringify({ id: item.id, data_url: item.data_url, dataUrl: item.data_url, width: item.width, height: item.height, title: item.title, source_url: item.source_url || item.url, sourceUrl: item.source_url || item.url });
109
  e.dataTransfer.setData('application/x-lumaref-library-item', payload);
110
  e.dataTransfer.setData('text/plain', payload);
@@ -140,49 +140,19 @@ export const LibraryPanel = () => {
140
  if (editingItem) return <MetadataEditor item={editingItem} onClose={() => setEditingItem(null)} onUpdate={updateItem} onDelete={() => { handleDelete(editingItem.id); setEditingItem(null); }} onAddToCanvas={() => addToCanvas(editingItem)} isOpen={isLibraryOpen} onClosePanel={() => setIsLibraryOpen(false)} />;
141
 
142
  return (
143
- <div className={`absolute left-0 top-0 h-full w-[40%] min-w-[360px] max-w-[500px] bg-[var(--panel-bg)] shadow-2xl flex flex-col z-[60] transform transition-transform duration-300 ease-out border-r border-[var(--panel-border)] ${isLibraryOpen ? 'translate-x-0' : '-translate-x-full'}`} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setIsDragOver(true); }} onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsDragOver(false); }} onDrop={handleBrowserDrop}>
144
  {isDragOver && <div className="absolute inset-0 z-50 bg-[var(--accent)]/10 border-2 border-dashed border-[var(--accent)] rounded-xl m-3 flex items-center justify-center pointer-events-none"><div className="bg-[var(--panel-surface)] border border-[var(--accent)] rounded-xl px-5 py-4 text-[var(--accent)] font-semibold text-sm flex flex-col items-center gap-2 shadow-2xl"><Upload size={28} />Drop images to import<div className="text-[10px] font-normal text-[var(--ui-secondary)]">PNG 路 JPG 路 WEBP 路 GIF 路 BMP 路 ICO 路 TIFF</div></div></div>}
145
-
146
- <div className="h-12 flex items-center justify-between px-3 border-b border-[var(--panel-border)] shrink-0">
147
- <div className="flex items-center gap-2 text-[var(--ui-primary)] text-[13px] font-semibold"><Folder size={15} className="text-[var(--accent)]" /> Library <span className="text-[11px] text-[var(--ui-secondary)] font-medium">{libraryItems.length}</span></div>
148
- <div className="flex items-center gap-1"><button onClick={loadLibrary} className="text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] p-1.5 rounded-md hover:bg-white/5" title="Refresh"><RefreshCw size={14} className={isLoading ? 'animate-spin' : ''} /></button><button onClick={() => setIsLibraryOpen(false)} className="text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] p-1.5 rounded-md hover:bg-white/5"><X size={16} /></button></div>
149
- </div>
150
-
151
- <div className="px-3 py-2 bg-[var(--panel-surface)] border-b border-[var(--panel-border)] shrink-0 space-y-2">
152
- <div className="relative"><Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--ui-secondary)]" /><input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search title, tag, source, hash..." className="w-full bg-black/25 text-[var(--ui-primary)] pl-9 pr-8 py-2 text-[13px] rounded-lg border border-[var(--panel-border)] focus:border-[var(--accent)] outline-none placeholder:text-[var(--ui-secondary)]" />{search && <button onClick={() => setSearch('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)]"><X size={13}/></button>}</div>
153
- <div className="flex items-center justify-between gap-2 text-[10px] text-[var(--ui-secondary)]">
154
- <div className="flex items-center gap-2"><Database size={11}/> {prettyBytes(totalBytes)} approx</div>
155
- {importingCount > 0 ? <span className="flex items-center gap-1 text-[var(--accent)]"><RefreshCw size={11} className="animate-spin"/> Importing {importingCount}</span> : lastImportError ? <span className="flex items-center gap-1 text-[#FF453A] truncate max-w-[220px]"><AlertCircle size={11}/>{lastImportError}</span> : <span className="flex items-center gap-1"><CheckCircle2 size={11}/> Ready</span>}
156
- </div>
157
- {allTags.length > 0 && <div className="flex items-center gap-1.5 overflow-x-auto pb-0.5 hide-scrollbar"><button onClick={() => setActiveTag(null)} className={`whitespace-nowrap px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${!activeTag ? 'bg-[var(--accent)] text-white' : 'bg-white/5 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/10'}`}>All</button>{allTags.map(tag => <button key={tag} onClick={() => setActiveTag(activeTag === tag ? null : tag)} className={`whitespace-nowrap px-2.5 py-1 rounded-full text-[11px] font-medium flex items-center gap-1 transition-colors ${activeTag === tag ? 'bg-[var(--accent)] text-white' : 'bg-white/5 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/10'}`}><Tag size={10} />{tag}</button>)}</div>}
158
- </div>
159
-
160
- <div className="flex-1 overflow-y-auto p-3 custom-scrollbar">
161
- <div className="flex justify-between items-center mb-2"><span className="text-[10px] font-semibold text-[var(--ui-secondary)] uppercase tracking-widest">{hasFilters ? `Results (${filtered.length})` : `Images (${filtered.length})`}</span><Grid size={13} className="text-[var(--ui-secondary)]" /></div>
162
- <div className="grid grid-cols-3 gap-2">
163
- <button onClick={() => fileInputRef.current?.click()} className="aspect-square bg-white/5 hover:bg-white/10 border border-dashed border-white/20 hover:border-[var(--accent)]/60 rounded-lg cursor-pointer flex flex-col items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] transition-all group shadow-sm"><div className="w-7 h-7 rounded-full bg-black/20 flex items-center justify-center mb-1.5 group-hover:scale-110 transition-transform"><Plus size={15} /></div><span className="text-[10px] font-medium">Import</span></button>
164
- <input ref={fileInputRef} type="file" accept={FILE_ACCEPT} multiple className="hidden" onChange={handleFileUpload} />
165
- {filtered.map(img => <LibraryTile key={img.id} item={img} onAdd={() => addToCanvas(img)} onDragStart={handleDragStart} onEdit={() => setEditingItem(img)} onDelete={() => handleDelete(img.id)} />)}
166
- {filtered.length === 0 && !isLoading && <div className="col-span-3 text-center text-[var(--ui-secondary)] py-14 text-sm flex flex-col items-center gap-2"><Folder size={28} className="opacity-20" /><p>{hasFilters ? 'No matching images' : 'Library is empty'}</p>{hasFilters && <button onClick={() => { setSearch(''); setActiveTag(null); }} className="text-[var(--accent)] text-xs hover:underline">Clear filters</button>}</div>}
167
- {isLoading && <div className="col-span-3 text-center text-[var(--ui-secondary)] py-12"><RefreshCw size={18} className="animate-spin mx-auto mb-2" />Loading...</div>}
168
- </div>
169
- </div>
170
  </div>
171
  );
172
  };
173
 
174
- function LibraryTile({ item, onAdd, onDragStart, onEdit, onDelete }: { item: LibItem; onAdd: () => void; onDragStart: (e: React.DragEvent, item: LibItem) => void; onEdit: () => void; onDelete: () => void }) {
175
  const [failed, setFailed] = useState(false);
176
  const kind = inferKind(item);
177
- return <div className="aspect-square bg-[var(--panel-surface)] rounded-lg cursor-pointer group relative overflow-hidden ring-1 ring-[var(--panel-border)] hover:ring-[var(--accent)] transition-all" draggable onDragStart={(e) => onDragStart(e, item)} onClick={onAdd} title={`${item.title || 'Reference'} 路 ${item.width}脳${item.height}`}>
178
- {failed ? <div className="w-full h-full flex items-center justify-center text-[var(--ui-secondary)]"><ImageIcon size={24}/></div> : <img src={item.data_url || item.url} className="w-full h-full object-cover opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300" draggable={false} loading="lazy" onError={() => setFailed(true)} />}
179
- <div className="absolute top-1 left-1 px-1.5 py-0.5 rounded bg-black/55 text-white/80 text-[8px] font-semibold tracking-wide pointer-events-none">{kind}</div>
180
- <div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2 pointer-events-none">
181
- <div className="flex justify-end gap-1 pointer-events-auto"><button onClick={(e) => { e.stopPropagation(); onEdit(); }} className="w-6 h-6 rounded bg-black/55 flex items-center justify-center text-white/80 hover:text-white hover:bg-black/75" title="Edit metadata"><Edit3 size={11} /></button><button onClick={(e) => { e.stopPropagation(); if (confirm('Remove this image from the library?')) onDelete(); }} className="w-6 h-6 rounded bg-black/55 flex items-center justify-center text-red-300 hover:text-white hover:bg-red-500/80" title="Delete"><Trash2 size={11} /></button></div>
182
- <div className="pointer-events-auto min-w-0"><span className="text-[10px] text-white font-medium truncate block">{item.title || 'Reference'}</span>{item.tags?.length > 0 && <div className="flex gap-1 mt-0.5 overflow-hidden">{item.tags.slice(0, 2).map(t => <span key={t} className="text-[8px] bg-white/20 text-white/90 px-1 rounded truncate max-w-[48px]">{t}</span>)}</div>}<div className="text-[9px] text-white/55 mt-0.5">{item.width}脳{item.height} 路 {formatDate(item.created_at)}</div></div>
183
- </div>
184
- {item.colors?.length > 0 && <div className="absolute bottom-0 left-0 right-0 h-1 flex opacity-70 group-hover:opacity-100 transition-opacity">{item.colors.slice(0, 6).map((c, ci) => <div key={ci} className="flex-1" style={{ backgroundColor: c }} />)}</div>}
185
- </div>;
186
  }
187
 
188
  function MetadataEditor({ item, onClose, onUpdate, onDelete, onAddToCanvas, isOpen, onClosePanel }: { item: LibItem; onClose: () => void; onUpdate: (item: LibItem) => void; onDelete: () => void; onAddToCanvas: () => void; isOpen: boolean; onClosePanel: () => void }) {
 
1
+ import { useState, useEffect, useCallback, useRef, useMemo, type DragEvent, type ChangeEvent } from 'react';
2
  import { X, Search, Folder, Grid, Plus, Tag, Trash2, RefreshCw, Upload, Edit3, Copy, ExternalLink, ChevronLeft, Image as ImageIcon, CheckCircle2, AlertCircle, Database } from 'lucide-react';
3
  import { useAppStore } from '../store';
4
  import { invoke } from '@tauri-apps/api/core';
 
80
  addToCanvas(item, true);
81
  setLibraryItems(prev => {
82
  const without = prev.filter(i => i.id !== item.id && i.hash !== item.hash);
83
+ const next = [item, ...without];
84
  rebuildTags(next);
85
  return next;
86
  });
 
92
  reader.readAsDataURL(file);
93
  }, [libraryItems, addToCanvas, rebuildTags]);
94
 
95
+ const handleBrowserDrop = (e: DragEvent<HTMLDivElement>) => {
96
  e.preventDefault(); e.stopPropagation(); setIsDragOver(false);
97
  const files = Array.from(e.dataTransfer.files);
98
  if (!files.length) return;
99
  files.forEach(importFileAsDataUrl);
100
  };
101
 
102
+ const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
103
  if (e.target.files) Array.from(e.target.files).forEach(importFileAsDataUrl);
104
  e.target.value = '';
105
  };
106
 
107
+ const handleDragStart = (e: DragEvent, item: LibItem) => {
108
  const payload = JSON.stringify({ id: item.id, data_url: item.data_url, dataUrl: item.data_url, width: item.width, height: item.height, title: item.title, source_url: item.source_url || item.url, sourceUrl: item.source_url || item.url });
109
  e.dataTransfer.setData('application/x-lumaref-library-item', payload);
110
  e.dataTransfer.setData('text/plain', payload);
 
140
  if (editingItem) return <MetadataEditor item={editingItem} onClose={() => setEditingItem(null)} onUpdate={updateItem} onDelete={() => { handleDelete(editingItem.id); setEditingItem(null); }} onAddToCanvas={() => addToCanvas(editingItem)} isOpen={isLibraryOpen} onClosePanel={() => setIsLibraryOpen(false)} />;
141
 
142
  return (
143
+ <div className={`absolute left-0 top-0 h-full w-[40%] min-w-[360px] max-w-[500px] bg-[var(--panel-bg)] shadow-2xl flex flex-col z-[60] transform transition-transform duration-300 ease-out border-r border-[var(--panel-border)] ${isLibraryOpen ? 'translate-x-0' : '-translate-x-full'}`} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setIsDragOver(true); }} onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setIsDragOver(false); }} onDrop={handleBrowserDrop}>
144
  {isDragOver && <div className="absolute inset-0 z-50 bg-[var(--accent)]/10 border-2 border-dashed border-[var(--accent)] rounded-xl m-3 flex items-center justify-center pointer-events-none"><div className="bg-[var(--panel-surface)] border border-[var(--accent)] rounded-xl px-5 py-4 text-[var(--accent)] font-semibold text-sm flex flex-col items-center gap-2 shadow-2xl"><Upload size={28} />Drop images to import<div className="text-[10px] font-normal text-[var(--ui-secondary)]">PNG 路 JPG 路 WEBP 路 GIF 路 BMP 路 ICO 路 TIFF</div></div></div>}
145
+ <div className="h-12 flex items-center justify-between px-3 border-b border-[var(--panel-border)] shrink-0"><div className="flex items-center gap-2 text-[var(--ui-primary)] text-[13px] font-semibold"><Folder size={15} className="text-[var(--accent)]" /> Library <span className="text-[11px] text-[var(--ui-secondary)] font-medium">{libraryItems.length}</span></div><div className="flex items-center gap-1"><button onClick={loadLibrary} className="text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] p-1.5 rounded-md hover:bg-white/5" title="Refresh"><RefreshCw size={14} className={isLoading ? 'animate-spin' : ''} /></button><button onClick={() => setIsLibraryOpen(false)} className="text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] p-1.5 rounded-md hover:bg-white/5"><X size={16} /></button></div></div>
146
+ <div className="px-3 py-2 bg-[var(--panel-surface)] border-b border-[var(--panel-border)] shrink-0 space-y-2"><div className="relative"><Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--ui-secondary)]" /><input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search title, tag, source, hash..." className="w-full bg-black/25 text-[var(--ui-primary)] pl-9 pr-8 py-2 text-[13px] rounded-lg border border-[var(--panel-border)] focus:border-[var(--accent)] outline-none placeholder:text-[var(--ui-secondary)]" />{search && <button onClick={() => setSearch('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)]"><X size={13}/></button>}</div><div className="flex items-center justify-between gap-2 text-[10px] text-[var(--ui-secondary)]"><div className="flex items-center gap-2"><Database size={11}/> {prettyBytes(totalBytes)} approx</div>{importingCount > 0 ? <span className="flex items-center gap-1 text-[var(--accent)]"><RefreshCw size={11} className="animate-spin"/> Importing {importingCount}</span> : lastImportError ? <span className="flex items-center gap-1 text-[#FF453A] truncate max-w-[220px]"><AlertCircle size={11}/>{lastImportError}</span> : <span className="flex items-center gap-1"><CheckCircle2 size={11}/> Ready</span>}</div>{allTags.length > 0 && <div className="flex items-center gap-1.5 overflow-x-auto pb-0.5 hide-scrollbar"><button onClick={() => setActiveTag(null)} className={`whitespace-nowrap px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${!activeTag ? 'bg-[var(--accent)] text-white' : 'bg-white/5 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/10'}`}>All</button>{allTags.map(tag => <button key={tag} onClick={() => setActiveTag(activeTag === tag ? null : tag)} className={`whitespace-nowrap px-2.5 py-1 rounded-full text-[11px] font-medium flex items-center gap-1 transition-colors ${activeTag === tag ? 'bg-[var(--accent)] text-white' : 'bg-white/5 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/10'}`}><Tag size={10} />{tag}</button>)}</div>}</div>
147
+ <div className="flex-1 overflow-y-auto p-3 custom-scrollbar"><div className="flex justify-between items-center mb-2"><span className="text-[10px] font-semibold text-[var(--ui-secondary)] uppercase tracking-widest">{hasFilters ? `Results (${filtered.length})` : `Images (${filtered.length})`}</span><Grid size={13} className="text-[var(--ui-secondary)]" /></div><div className="grid grid-cols-3 gap-2"><button onClick={() => fileInputRef.current?.click()} className="aspect-square bg-white/5 hover:bg-white/10 border border-dashed border-white/20 hover:border-[var(--accent)]/60 rounded-lg cursor-pointer flex flex-col items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] transition-all group shadow-sm"><div className="w-7 h-7 rounded-full bg-black/20 flex items-center justify-center mb-1.5 group-hover:scale-110 transition-transform"><Plus size={15} /></div><span className="text-[10px] font-medium">Import</span></button><input ref={fileInputRef} type="file" accept={FILE_ACCEPT} multiple className="hidden" onChange={handleFileUpload} />{filtered.map(img => <LibraryTile key={img.id} item={img} onAdd={() => addToCanvas(img)} onDragStart={handleDragStart} onEdit={() => setEditingItem(img)} onDelete={() => handleDelete(img.id)} />)}{filtered.length === 0 && !isLoading && <div className="col-span-3 text-center text-[var(--ui-secondary)] py-14 text-sm flex flex-col items-center gap-2"><Folder size={28} className="opacity-20" /><p>{hasFilters ? 'No matching images' : 'Library is empty'}</p>{hasFilters && <button onClick={() => { setSearch(''); setActiveTag(null); }} className="text-[var(--accent)] text-xs hover:underline">Clear filters</button>}</div>}{isLoading && <div className="col-span-3 text-center text-[var(--ui-secondary)] py-12"><RefreshCw size={18} className="animate-spin mx-auto mb-2" />Loading...</div>}</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
  );
150
  };
151
 
152
+ function LibraryTile({ item, onAdd, onDragStart, onEdit, onDelete }: { item: LibItem; onAdd: () => void; onDragStart: (e: DragEvent, item: LibItem) => void; onEdit: () => void; onDelete: () => void }) {
153
  const [failed, setFailed] = useState(false);
154
  const kind = inferKind(item);
155
+ return <div className="aspect-square bg-[var(--panel-surface)] rounded-lg cursor-pointer group relative overflow-hidden ring-1 ring-[var(--panel-border)] hover:ring-[var(--accent)] transition-all" draggable onDragStart={(e) => onDragStart(e, item)} onClick={onAdd} title={`${item.title || 'Reference'} 路 ${item.width}脳${item.height}`}>{failed ? <div className="w-full h-full flex items-center justify-center text-[var(--ui-secondary)]"><ImageIcon size={24}/></div> : <img src={item.data_url || item.url} className="w-full h-full object-cover opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300" draggable={false} loading="lazy" onError={() => setFailed(true)} />}<div className="absolute top-1 left-1 px-1.5 py-0.5 rounded bg-black/55 text-white/80 text-[8px] font-semibold tracking-wide pointer-events-none">{kind}</div><div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-2 pointer-events-none"><div className="flex justify-end gap-1 pointer-events-auto"><button onClick={(e) => { e.stopPropagation(); onEdit(); }} className="w-6 h-6 rounded bg-black/55 flex items-center justify-center text-white/80 hover:text-white hover:bg-black/75" title="Edit metadata"><Edit3 size={11} /></button><button onClick={(e) => { e.stopPropagation(); if (confirm('Remove this image from the library?')) onDelete(); }} className="w-6 h-6 rounded bg-black/55 flex items-center justify-center text-red-300 hover:text-white hover:bg-red-500/80" title="Delete"><Trash2 size={11} /></button></div><div className="pointer-events-auto min-w-0"><span className="text-[10px] text-white font-medium truncate block">{item.title || 'Reference'}</span>{item.tags?.length > 0 && <div className="flex gap-1 mt-0.5 overflow-hidden">{item.tags.slice(0, 2).map(t => <span key={t} className="text-[8px] bg-white/20 text-white/90 px-1 rounded truncate max-w-[48px]">{t}</span>)}</div>}<div className="text-[9px] text-white/55 mt-0.5">{item.width}脳{item.height} 路 {formatDate(item.created_at)}</div></div></div>{item.colors?.length > 0 && <div className="absolute bottom-0 left-0 right-0 h-1 flex opacity-70 group-hover:opacity-100 transition-opacity">{item.colors.slice(0, 6).map((c, ci) => <div key={ci} className="flex-1" style={{ backgroundColor: c }} />)}</div>}</div>;
 
 
 
 
 
 
 
 
156
  }
157
 
158
  function MetadataEditor({ item, onClose, onUpdate, onDelete, onAddToCanvas, isOpen, onClosePanel }: { item: LibItem; onClose: () => void; onUpdate: (item: LibItem) => void; onDelete: () => void; onAddToCanvas: () => void; isOpen: boolean; onClosePanel: () => void }) {