feat: Phase 4 - Color Tools view with palette export (HEX, CSS, GPL, ASE, Procreate formats)
Browse files
src/components/views/ColorToolsView.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'motion/react';
|
| 2 |
+
import { Palette, Copy, Download, Plus, X, Pipette } from 'lucide-react';
|
| 3 |
+
import { useState, useEffect } from 'react';
|
| 4 |
+
import { invoke } from '@tauri-apps/api/core';
|
| 5 |
+
import { cn, defaultTransition } from '../../lib/utils';
|
| 6 |
+
|
| 7 |
+
interface ColorExportResult { format: string; content: string; filename: string; }
|
| 8 |
+
interface LibItem { id: string; data_url: string; title: string; colors: string[]; }
|
| 9 |
+
|
| 10 |
+
export default function ColorToolsView() {
|
| 11 |
+
const [colors, setColors] = useState<string[]>(['#D4A373', '#2C2926', '#6C89E8', '#C4655B', '#4A4B3A', '#E8E3DF']);
|
| 12 |
+
const [libItems, setLibItems] = useState<LibItem[]>([]);
|
| 13 |
+
const [exportFormat, setExportFormat] = useState('hex');
|
| 14 |
+
const [exportResult, setExportResult] = useState<string>('');
|
| 15 |
+
const [newColor, setNewColor] = useState('#D4A373');
|
| 16 |
+
|
| 17 |
+
useEffect(() => { invoke<LibItem[]>('library_items').then(items => { setLibItems(items.filter(i => i.colors?.length > 0)); }).catch(() => {}); }, []);
|
| 18 |
+
|
| 19 |
+
const doExport = (fmt: string) => {
|
| 20 |
+
setExportFormat(fmt);
|
| 21 |
+
invoke<ColorExportResult>('color_export', { colors, format: fmt }).then(r => setExportResult(r.content)).catch(console.error);
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
useEffect(() => { doExport(exportFormat); }, [colors]);
|
| 25 |
+
|
| 26 |
+
const addColor = () => { if (colors.length < 12) setColors([...colors, newColor]); };
|
| 27 |
+
const removeColor = (idx: number) => setColors(colors.filter((_, i) => i !== idx));
|
| 28 |
+
const loadFromLibrary = (item: LibItem) => { if (item.colors.length) setColors(item.colors.slice(0, 12)); };
|
| 29 |
+
const copyAll = () => navigator.clipboard.writeText(exportResult);
|
| 30 |
+
|
| 31 |
+
const formats = [
|
| 32 |
+
{ id: 'hex', label: 'HEX' },
|
| 33 |
+
{ id: 'css', label: 'CSS Variables' },
|
| 34 |
+
{ id: 'gpl', label: 'GIMP Palette' },
|
| 35 |
+
{ id: 'ase', label: 'Adobe ASE' },
|
| 36 |
+
{ id: 'procreate', label: 'Procreate' },
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} transition={defaultTransition} className="w-full h-full bg-dusk-bg flex overflow-hidden">
|
| 41 |
+
{/* Main */}
|
| 42 |
+
<div className="flex-1 p-8 overflow-auto flex flex-col gap-8 max-w-3xl mx-auto">
|
| 43 |
+
<div>
|
| 44 |
+
<h1 className="text-3xl font-medium text-dusk-text mb-2">Color Tools</h1>
|
| 45 |
+
<p className="text-sm text-dusk-text-muted">Build and export palettes for your art tools.</p>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
{/* Active Palette */}
|
| 49 |
+
<div>
|
| 50 |
+
<h3 className="text-[11px] font-bold text-dusk-text-muted uppercase tracking-wider mb-3">Active Palette</h3>
|
| 51 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 52 |
+
{colors.map((c, i) => (
|
| 53 |
+
<div key={i} className="relative group">
|
| 54 |
+
<div className="w-14 h-14 rounded-xl shadow-md cursor-pointer hover:scale-110 transition-transform border border-black/20" style={{ backgroundColor: c }} title={c} onClick={() => navigator.clipboard.writeText(c)} />
|
| 55 |
+
<button onClick={() => removeColor(i)} className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"><X className="w-2.5 h-2.5" /></button>
|
| 56 |
+
<span className="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] text-dusk-text-muted font-mono">{c}</span>
|
| 57 |
+
</div>
|
| 58 |
+
))}
|
| 59 |
+
{colors.length < 12 && (
|
| 60 |
+
<div className="flex items-center gap-2 ml-2">
|
| 61 |
+
<input type="color" value={newColor} onChange={e => setNewColor(e.target.value)} className="w-10 h-10 rounded-lg cursor-pointer border-0" />
|
| 62 |
+
<button onClick={addColor} className="w-10 h-10 rounded-xl border border-dashed border-dusk-border hover:border-dusk-accent flex items-center justify-center text-dusk-text-muted hover:text-dusk-accent"><Plus className="w-4 h-4" /></button>
|
| 63 |
+
</div>
|
| 64 |
+
)}
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* Export Format Selection */}
|
| 69 |
+
<div>
|
| 70 |
+
<h3 className="text-[11px] font-bold text-dusk-text-muted uppercase tracking-wider mb-3">Export Format</h3>
|
| 71 |
+
<div className="flex gap-2 flex-wrap">
|
| 72 |
+
{formats.map(f => (
|
| 73 |
+
<button key={f.id} onClick={() => doExport(f.id)} className={cn('px-4 py-2 rounded-xl text-sm font-medium border transition-colors', exportFormat === f.id ? 'bg-dusk-accent/20 text-dusk-accent border-dusk-accent/30' : 'bg-dusk-surface text-dusk-text-muted border-dusk-border hover:border-dusk-accent/30')}>{f.label}</button>
|
| 74 |
+
))}
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{/* Export Preview */}
|
| 79 |
+
<div>
|
| 80 |
+
<div className="flex items-center justify-between mb-3">
|
| 81 |
+
<h3 className="text-[11px] font-bold text-dusk-text-muted uppercase tracking-wider">Export Preview</h3>
|
| 82 |
+
<button onClick={copyAll} className="flex items-center gap-1.5 text-[12px] text-dusk-accent hover:text-dusk-accent/80 font-medium"><Copy className="w-3.5 h-3.5" /> Copy</button>
|
| 83 |
+
</div>
|
| 84 |
+
<pre className="bg-dusk-surface border border-dusk-border rounded-xl p-4 text-[12px] font-mono text-dusk-text overflow-auto max-h-64 whitespace-pre-wrap">{exportResult || 'Select colors and format'}</pre>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
{/* Library Colors Sidebar */}
|
| 89 |
+
<div className="w-64 border-l border-dusk-border bg-dusk-surface flex flex-col shrink-0">
|
| 90 |
+
<div className="h-12 border-b border-dusk-border flex items-center px-4"><span className="text-[13px] font-semibold text-dusk-text">From Library</span></div>
|
| 91 |
+
<div className="flex-1 overflow-auto p-3 flex flex-col gap-2">
|
| 92 |
+
{libItems.length === 0 && <p className="text-xs text-dusk-text-muted text-center py-8">No items with extracted colors</p>}
|
| 93 |
+
{libItems.map(it => (
|
| 94 |
+
<button key={it.id} onClick={() => loadFromLibrary(it)} className="p-2 rounded-xl bg-dusk-bg border border-dusk-border/50 hover:border-dusk-accent/30 transition-colors text-left">
|
| 95 |
+
<div className="flex h-6 rounded overflow-hidden mb-2">{it.colors.slice(0, 6).map((c, i) => <div key={i} className="flex-1" style={{ backgroundColor: c }} />)}</div>
|
| 96 |
+
<span className="text-[11px] text-dusk-text-muted truncate block">{it.title}</span>
|
| 97 |
+
</button>
|
| 98 |
+
))}
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</motion.div>
|
| 102 |
+
);
|
| 103 |
+
}
|