fix: use explicit React event type imports in library panel
Browse files- 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 =
|
| 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:
|
| 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:
|
| 103 |
if (e.target.files) Array.from(e.target.files).forEach(importFileAsDataUrl);
|
| 104 |
e.target.value = '';
|
| 105 |
};
|
| 106 |
|
| 107 |
-
const handleDragStart = (e:
|
| 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="
|
| 147 |
-
|
| 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:
|
| 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 }) {
|