asdf98 commited on
Commit
b69eb83
·
verified ·
1 Parent(s): 6313940

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
+ }