diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index e99908b..253b686 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -2360,6 +2360,325 @@ function JsonTool({ toast, mobile }) { +// ──────────────────────────────────────────────────────────── Farb-Rechner ── + +// ── Konvertierungsfunktionen ── +function hexToRgb(hex) { + hex = hex.replace(/^#/, ''); + if (hex.length === 3) hex = hex.split('').map(c=>c+c).join(''); + if (hex.length === 4) hex = hex.split('').map(c=>c+c).join(''); // #rgba + const n = parseInt(hex.slice(0,6), 16); + const a = hex.length === 8 ? parseInt(hex.slice(6,8),16)/255 : 1; + return { r:(n>>16)&255, g:(n>>8)&255, b:n&255, a }; +} + +function rgbToHex({r,g,b,a=1}) { + const h = [r,g,b].map(v=>Math.round(v).toString(16).padStart(2,'0')).join(''); + const ah = a < 1 ? Math.round(a*255).toString(16).padStart(2,'0') : ''; + return '#' + h + ah; +} + +function rgbToHsl({r,g,b,a=1}) { + r/=255; g/=255; b/=255; + const max=Math.max(r,g,b), min=Math.min(r,g,b); + let h=0,s=0,l=(max+min)/2; + if (max!==min) { + const d=max-min; + s=l>0.5?d/(2-max-min):d/(max+min); + switch(max){ + case r: h=((g-b)/d+(g(n+h/30)%12; + const c=s*Math.min(l,1-l); + const f=n=>l-c*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1))); + return {r:Math.round(f(0)*255), g:Math.round(f(8)*255), b:Math.round(f(4)*255), a}; +} + +function rgbToHsv({r,g,b,a=1}) { + r/=255; g/=255; b/=255; + const max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min; + let h=0; + if (d!==0){ + switch(max){ + case r: h=((g-b)/d)%6; break; + case g: h=(b-r)/d+2; break; + case b: h=(r-g)/d+4; break; + } + h=Math.round(h*60); if(h<0) h+=360; + } + return {h, s:max===0?0:Math.round(d/max*100), v:Math.round(max*100), a}; +} + +function rgbToCmyk({r,g,b}) { + r/=255; g/=255; b/=255; + const k=1-Math.max(r,g,b); + if (k===1) return {c:0,m:0,y:0,k:100}; + return { + c:Math.round((1-r-k)/(1-k)*100), + m:Math.round((1-g-k)/(1-k)*100), + y:Math.round((1-b-k)/(1-k)*100), + k:Math.round(k*100) + }; +} + +// Relative luminance (WCAG) +function luminance({r,g,b}) { + const c = [r,g,b].map(v => { + v/=255; + return v<=0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055,2.4); + }); + return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2]; +} + +function contrastRatio(lum1, lum2) { + const [l,d] = lum1>lum2 ? [lum1,lum2] : [lum2,lum1]; + return (l+0.05)/(d+0.05); +} + +// Parse any color string input → {r,g,b,a} or null +function parseColor(raw) { + if (!raw) return null; + raw = raw.trim(); + + // hex + if (/^#?[0-9a-fA-F]{3,8}$/.test(raw)) { + try { return hexToRgb(raw.startsWith('#')?raw:'#'+raw); } catch {} + } + + // rgb / rgba + const rgbM = raw.match(/rgba?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)\s*[,\s]\s*([\d.]+)(?:\s*[,\/]\s*([\d.]+%?))?\s*\)/i); + if (rgbM) { + const a = rgbM[4] ? (rgbM[4].includes('%') ? parseFloat(rgbM[4])/100 : parseFloat(rgbM[4])) : 1; + return {r:+rgbM[1],g:+rgbM[2],b:+rgbM[3],a}; + } + + // hsl / hsla + const hslM = raw.match(/hsla?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%?\s*[,\s]\s*([\d.]+)%?(?:\s*[,\/]\s*([\d.]+%?))?\s*\)/i); + if (hslM) { + const a = hslM[4] ? (hslM[4].includes('%')?parseFloat(hslM[4])/100:parseFloat(hslM[4])) : 1; + return hslToRgb({h:+hslM[1],s:+hslM[2],l:+hslM[3],a}); + } + + // CSS color names (basic set) + const NAMES = {red:[255,0,0],green:[0,128,0],blue:[0,0,255],white:[255,255,255],black:[0,0,0], + yellow:[255,255,0],cyan:[0,255,255],magenta:[255,0,255],orange:[255,165,0],purple:[128,0,128], + pink:[255,192,203],gray:[128,128,128],grey:[128,128,128],lime:[0,255,0],navy:[0,0,128], + teal:[0,128,128],silver:[192,192,192],maroon:[128,0,0],olive:[128,128,0]}; + const name = NAMES[raw.toLowerCase()]; + if (name) return {r:name[0],g:name[1],b:name[2],a:1}; + + return null; +} + +function fmt2(n) { return Math.round(n*100)/100; } + +function FarbRechner({ toast, mobile }) { + const [input, setInput] = useState(''); + const [pickerHex,setPickerHex]= useState('#4ecdc4'); + const [color, setColor] = useState(null); // {r,g,b,a} + const [copied, setCopied] = useState(''); + + function applyColor(rgba) { + setColor(rgba); + // sync picker (ignore alpha for picker) + setPickerHex(rgbToHex({...rgba,a:1})); + } + + function handleInput(val) { + setInput(val); + const c = parseColor(val); + if (c) applyColor(c); + else if (!val.trim()) setColor(null); + } + + function handlePicker(hex) { + setPickerHex(hex); + const c = hexToRgb(hex); + // preserve alpha from current color + const a = color?.a ?? 1; + setInput(hex); + applyColor({...c, a}); + setInput(hex); + } + + async function copy(text, key) { + try { await navigator.clipboard.writeText(text); setCopied(key); setTimeout(()=>setCopied(''),1800); } + catch { toast('Kopieren fehlgeschlagen'); } + } + + const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'}; + + // Derived formats + const formats = color ? (() => { + const {r,g,b,a} = color; + const hsl = rgbToHsl(color); + const hsv = rgbToHsv(color); + const cmyk = rgbToCmyk(color); + const hex6 = rgbToHex({r,g,b,a:1}); + const hex8 = rgbToHex({r,g,b,a}); + const hasAlpha = a < 1; + + const lum = luminance({r,g,b}); + const contrastWhite = fmt2(contrastRatio(lum, 1)); + const contrastBlack = fmt2(contrastRatio(lum, 0)); + const wcagWhite = contrastWhite>=7?'AAA':contrastWhite>=4.5?'AA':contrastWhite>=3?'AA (groß)':'✗'; + const wcagBlack = contrastBlack>=7?'AAA':contrastBlack>=4.5?'AA':contrastBlack>=3?'AA (groß)':'✗'; + const textColor = lum > 0.179 ? '#000' : '#fff'; + + return [ + {key:'hex6', label:'HEX', value: hex6, copy: hex6}, + {key:'hex8', label:'HEX + Alpha', value: hex8, copy: hex8, hide: !hasAlpha}, + {key:'rgb', label:'RGB', value: `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`, copy: `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`}, + {key:'rgba', label:'RGBA', value: `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${fmt2(a)})`, copy: `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${fmt2(a)})`}, + {key:'hsl', label:'HSL', value: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`, copy: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`}, + {key:'hsla', label:'HSLA', value: `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${fmt2(a)})`, copy: `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${fmt2(a)})`}, + {key:'hsv', label:'HSV / HSB', value: `hsv(${hsv.h}°, ${hsv.s}%, ${hsv.v}%)`, copy: `hsv(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`}, + {key:'cmyk', label:'CMYK', value: `cmyk(${cmyk.c}%, ${cmyk.m}%, ${cmyk.y}%, ${cmyk.k}%)`, copy: `cmyk(${cmyk.c}%, ${cmyk.m}%, ${cmyk.y}%, ${cmyk.k}%)`}, + {key:'css', label:'CSS custom property', value: `--color: ${hex6};`, copy: `--color: ${hex6};`}, + {key:'r', label:'R (0–255)', value: String(Math.round(r)), copy: String(Math.round(r)), muted:true}, + {key:'g', label:'G (0–255)', value: String(Math.round(g)), copy: String(Math.round(g)), muted:true}, + {key:'b', label:'B (0–255)', value: String(Math.round(b)), copy: String(Math.round(b)), muted:true}, + {key:'a', label:'Alpha (0–1)', value: fmt2(a), copy: String(fmt2(a)), muted:true}, + {key:'lum', label:'Relative Luminanz', value: fmt2(lum), copy: String(fmt2(lum)), muted:true}, + {key:'cw', label:'Kontrast zu Weiß', value: `${contrastWhite}:1 — WCAG: ${wcagWhite}`, copy: `${contrastWhite}:1`, muted:true}, + {key:'cb', label:'Kontrast zu Schwarz', value: `${contrastBlack}:1 — WCAG: ${wcagBlack}`, copy: `${contrastBlack}:1`, muted:true}, + ].filter(f=>!f.hide); + })() : []; + + const previewBg = color ? `rgb(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)})` : 'rgba(255,255,255,0.05)'; + const previewText = color ? (luminance(color) > 0.179 ? '#000' : '#fff') : 'rgba(255,255,255,0.3)'; + + // Gradient bar stops for hue slider background + const hueBar = 'linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)'; + + return ( +
+ {/* Eingabe-Zeile */} +
+
+
+ Farbe eingeben (HEX, RGB, HSL, Farbname…) +
+ handleInput(e.target.value)}/> +
+ {/* Color Picker */} +
+
+ Color Picker +
+
+
+ handlePicker(e.target.value)} + style={{opacity:0,position:'absolute',inset:0,width:'100%',height:'100%',cursor:'pointer',border:'none',padding:0}}/> +
+
+
+ {/* Alpha Slider */} + {!!color && ( +
+
+ Alpha / Transparenz: {Math.round((color.a??1)*100)}% +
+ applyColor({...color, a:parseFloat(e.target.value)})} + style={{width:'100%', accentColor:'#4ecdc4'}}/> +
+ )} + {!!input && } +
+ + {/* Vorschau */} + {color && ( +
+ {/* Checkerboard + Farbe */} +
+
+ 0.5?'0 1px 2px rgba(0,0,0,0.2)':'0 1px 2px rgba(0,0,0,0.5)'}}> + {rgbToHex({...color,a:1}).toUpperCase()} + +
+
+ {/* Komplementärfarbe Vorschau */} +
+ {[0,30,60,90,120,150,180,210,240,270,300,330].map(dh=>{ + const hsl = rgbToHsl(color); + const c2 = hslToRgb({...hsl, h:(hsl.h+dh)%360, a:1}); + return
; + })} +
+
+ Farbrad-Vorschau (Töne in 30°-Schritten) +
+
+ )} + + {/* Format-Ausgaben */} + {!!formats.length && ( +
+ {formats.map(f=>( +
copy(f.copy, f.key)} + style={{display:'flex',justifyContent:'space-between',alignItems:'center', + padding:'7px 10px',borderRadius:7,cursor:'pointer',userSelect:'none', + background:'rgba(255,255,255,0.03)',border:'1px solid rgba(255,255,255,0.05)', + transition:'background 0.15s'}} + onMouseEnter={e=>e.currentTarget.style.background='rgba(255,255,255,0.07)'} + onMouseLeave={e=>e.currentTarget.style.background='rgba(255,255,255,0.03)'}> + {f.label} + + {f.value} + + + {copied===f.key?'✓':'⎘'} + +
+ ))} +
+ )} + + {!color && !input && ( +
+ Farbe eingeben oder im Color Picker auswählen → alle Formate werden berechnet +
+ )} + {!!input && !color && ( +
+ Format nicht erkannt · Beispiele: #ff6b9d · rgb(255, 107, 157) · hsl(340, 100%, 71%) +
+ )} +
+ ); +} + + const TOOL_DEFS = [ {id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true, tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}], @@ -2370,7 +2689,7 @@ const TOOL_DEFS = [ {id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff}, {id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner}, {id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner}, - {id:'keygen', label:'🔐 Key-Gen', desc:'Sichere Keys & Secrets generieren', ready:true, component:KeyGen}, + {id:'farben', label:'🎨 Farben', desc:'HEX, RGB, HSL, OKLCH umrechnen', ready:true, component:FarbRechner}, ]; export default function DevTools({ toast, mobile }) {