DevTools: Farb-Rechner mit HEX/RGB/RGBA/HSL/HSV/CMYK, Color Picker, Alpha, WCAG-Kontrast
This commit is contained in:
@@ -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<b?6:0))/6; break;
|
||||||
|
case g: h=((b-r)/d+2)/6; break;
|
||||||
|
case b: h=((r-g)/d+4)/6; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { h:Math.round(h*360), s:Math.round(s*100), l:Math.round(l*100), a };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToRgb({h,s,l,a=1}) {
|
||||||
|
s/=100; l/=100;
|
||||||
|
const k=n=>(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 (
|
||||||
|
<div>
|
||||||
|
{/* Eingabe-Zeile */}
|
||||||
|
<div style={{display:'flex', gap:10, alignItems:'center', marginBottom:16, flexWrap:'wrap'}}>
|
||||||
|
<div style={{flex:1, minWidth:160}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||||
|
Farbe eingeben (HEX, RGB, HSL, Farbname…)
|
||||||
|
</div>
|
||||||
|
<input style={{...inp, width:'100%',
|
||||||
|
borderColor: input && !color ? 'rgba(255,107,157,0.5)' : undefined}}
|
||||||
|
placeholder="#4ecdc4 oder rgb(78,205,196) oder hsl(177,55%,55%)"
|
||||||
|
value={input} onChange={e=>handleInput(e.target.value)}/>
|
||||||
|
</div>
|
||||||
|
{/* Color Picker */}
|
||||||
|
<div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||||
|
Color Picker
|
||||||
|
</div>
|
||||||
|
<div style={{position:'relative', width:42, height:36}}>
|
||||||
|
<div style={{width:42,height:36,borderRadius:7,background:pickerHex,
|
||||||
|
border:'2px solid rgba(255,255,255,0.15)',cursor:'pointer',overflow:'hidden'}}>
|
||||||
|
<input type="color" value={pickerHex}
|
||||||
|
onChange={e=>handlePicker(e.target.value)}
|
||||||
|
style={{opacity:0,position:'absolute',inset:0,width:'100%',height:'100%',cursor:'pointer',border:'none',padding:0}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Alpha Slider */}
|
||||||
|
{!!color && (
|
||||||
|
<div style={{flex:'0 0 140px'}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:4}}>
|
||||||
|
Alpha / Transparenz: {Math.round((color.a??1)*100)}%
|
||||||
|
</div>
|
||||||
|
<input type="range" min={0} max={1} step={0.01}
|
||||||
|
value={color?.a??1}
|
||||||
|
onChange={e=>applyColor({...color, a:parseFloat(e.target.value)})}
|
||||||
|
style={{width:'100%', accentColor:'#4ecdc4'}}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!input && <button style={{...S.btn('#ff6b9d',false),fontSize:11,padding:'7px 14px'}}
|
||||||
|
onClick={()=>{setInput('');setColor(null);}}>✕ Leeren</button>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vorschau */}
|
||||||
|
{color && (
|
||||||
|
<div style={{borderRadius:10,overflow:'hidden',marginBottom:16,border:'1px solid rgba(255,255,255,0.08)'}}>
|
||||||
|
{/* Checkerboard + Farbe */}
|
||||||
|
<div style={{
|
||||||
|
height:80,
|
||||||
|
background:`linear-gradient(${previewBg},${previewBg}),
|
||||||
|
repeating-conic-gradient(rgba(255,255,255,0.1) 0% 25%, transparent 0% 50%) 0 0 / 16px 16px`,
|
||||||
|
display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||||
|
<div style={{
|
||||||
|
background: color.a < 1
|
||||||
|
? `rgba(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)},${color.a})`
|
||||||
|
: previewBg,
|
||||||
|
width:'100%', height:'100%', display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||||
|
<span style={{color:previewText, fontFamily:'monospace', fontSize:13, fontWeight:700,
|
||||||
|
textShadow: luminance(color)>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()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Komplementärfarbe Vorschau */}
|
||||||
|
<div style={{display:'flex',height:32}}>
|
||||||
|
{[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 <div key={dh} style={{flex:1,background:`rgb(${c2.r},${c2.g},${c2.b})`}} title={`+${dh}°`}/>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{padding:'4px 10px',background:'rgba(0,0,0,0.2)',
|
||||||
|
color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:9}}>
|
||||||
|
Farbrad-Vorschau (Töne in 30°-Schritten)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Format-Ausgaben */}
|
||||||
|
{!!formats.length && (
|
||||||
|
<div style={{display:'flex',flexDirection:'column',gap:4}}>
|
||||||
|
{formats.map(f=>(
|
||||||
|
<div key={f.key} onClick={()=>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)'}>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11,minWidth:120}}>{f.label}</span>
|
||||||
|
<code style={{color: f.muted?'rgba(255,255,255,0.55)':'#4ecdc4',
|
||||||
|
fontFamily:"'Courier New',monospace",fontSize:12,fontWeight:700,
|
||||||
|
flex:1,textAlign:'right',marginLeft:8,wordBreak:'break-all'}}>
|
||||||
|
{f.value}
|
||||||
|
</code>
|
||||||
|
<span style={{color:copied===f.key?'#4ecdc4':'rgba(255,255,255,0.2)',
|
||||||
|
fontSize:14,marginLeft:10,flexShrink:0,transition:'color 0.2s'}}>
|
||||||
|
{copied===f.key?'✓':'⎘'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!color && !input && (
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,
|
||||||
|
padding:'20px 0',textAlign:'center'}}>
|
||||||
|
Farbe eingeben oder im Color Picker auswählen → alle Formate werden berechnet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!input && !color && (
|
||||||
|
<div style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:11,padding:'8px 0'}}>
|
||||||
|
Format nicht erkannt · Beispiele: <code>#ff6b9d</code> · <code>rgb(255, 107, 157)</code> · <code>hsl(340, 100%, 71%)</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const TOOL_DEFS = [
|
const TOOL_DEFS = [
|
||||||
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
||||||
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
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:'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:'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:'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 }) {
|
export default function DevTools({ toast, mobile }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user