YAML Formatter: js-yaml, Baumansicht mit Linien, Einklappen, Mobile-Fix

This commit is contained in:
2026-05-29 19:26:07 +02:00
parent b065c5350a
commit a1b00a0f0d
2 changed files with 227 additions and 238 deletions

View File

@@ -7,6 +7,7 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"js-yaml": "^4.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },

View File

@@ -1,203 +1,245 @@
import { useState, useRef } from 'react'; import { useState, useCallback } from 'react';
import { S } from '../lib.js'; import { S } from '../lib.js';
import jsyaml from 'js-yaml';
// ── YAML Formatter ──────────────────────────────────────────────────────────── // ── YAML Tree Node (collapsible, mit Einrück-Linien) ─────────────────────────
function parseAndFormatYaml(input) { function YamlNode({ data, depth = 0, keyName = null, collapsed, onToggle, path }) {
// YAML-konformes Einrücken mit 2 Spaces const indent = depth * 18; // px pro Level
// Wir parsen manuell: kein externes package, aber korrekte Einrückung const isObj = data !== null && typeof data === 'object' && !Array.isArray(data);
const lines = input.split('\n'); const isArr = Array.isArray(data);
const out = []; const isLeaf = !isObj && !isArr;
let inMultiline = false;
let multilineIndent = 0;
for (let i = 0; i < lines.length; i++) { const nodeKey = path;
const raw = lines[i]; const isCollapsed = collapsed.has(nodeKey);
const trimmed = raw.trim(); const hasChildren = isObj || isArr;
// Leerzeilen beibehalten const keyColor = '#7ecaff';
if (!trimmed) { out.push(''); continue; } const strColor = '#ce9178';
const numColor = '#b5cea8';
const boolColor = '#569cd6';
const nullColor = '#808080';
const indentColor= 'rgba(255,255,255,0.08)';
// Kommentare unverändert const valueColor = (v) => {
if (trimmed.startsWith('#')) { if (v === null || v === undefined) return nullColor;
const depth = detectDepth(raw); if (typeof v === 'boolean') return boolColor;
out.push(' '.repeat(depth) + trimmed); if (typeof v === 'number') return numColor;
continue; return strColor;
};
const renderValue = (v) => {
if (v === null) return <span style={{color:nullColor}}>null</span>;
if (v === undefined) return <span style={{color:nullColor}}>~</span>;
if (typeof v === 'boolean') return <span style={{color:boolColor}}>{String(v)}</span>;
if (typeof v === 'number') return <span style={{color:numColor}}>{v}</span>;
const s = String(v);
// Quote strings that need it
const needsQuotes = /[:#\[\]{}&*!|>'"%@`]/.test(s) || s.trim()!==s || s==='' || /^(true|false|null|yes|no|on|off)$/i.test(s);
return <span style={{color:strColor}}>{needsQuotes ? `"${s.replace(/"/g,'\\"')}"` : s}</span>;
};
// Leaf: key: value
if (isLeaf) {
return (
<div style={{display:'flex',alignItems:'baseline',minHeight:20,position:'relative'}}>
{/* Vertical indent lines */}
{Array.from({length:depth},(_,i)=>(
<div key={i} style={{position:'absolute',left:i*18+6,top:0,bottom:0,
borderLeft:`1px solid ${indentColor}`,pointerEvents:'none'}}/>
))}
<div style={{paddingLeft:indent,fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7,whiteSpace:'pre-wrap',wordBreak:'break-all'}}>
{keyName!==null&&<span style={{color:keyColor}}>{keyName}<span style={{color:'rgba(255,255,255,0.4)'}}>: </span></span>}
{renderValue(data)}
</div>
</div>
);
} }
// Multiline-Block (| oder >) Zeilen danach unverändert einrücken const count = isObj ? Object.keys(data).length : data.length;
if (inMultiline) { const summary = isObj ? `{${count}}` : `[${count}]`;
// Endet wenn Einrückung nicht mehr tiefer ist
const currentIndent = raw.search(/\S/);
if (currentIndent <= multilineIndent && trimmed) {
inMultiline = false;
} else {
out.push(raw); // Multiline-Inhalt unverändert
continue;
}
}
const depth = detectDepth(raw); return (
const indent = ' '.repeat(depth); <div style={{position:'relative'}}>
{/* Vertical indent lines for this level */}
{Array.from({length:depth},(_,i)=>(
<div key={i} style={{position:'absolute',left:i*18+6,top:0,bottom:0,
borderLeft:`1px solid ${indentColor}`,pointerEvents:'none'}}/>
))}
// Key: Value {/* Header row */}
const colonIdx = findKeyColon(trimmed); <div style={{display:'flex',alignItems:'center',minHeight:20,cursor:hasChildren?'pointer':'default',
if (colonIdx > 0) { paddingLeft:indent,userSelect:'none'}}
const key = trimmed.slice(0, colonIdx).trim(); onClick={()=>hasChildren&&onToggle(nodeKey)}>
const value = trimmed.slice(colonIdx + 1).trim(); {hasChildren&&(
<span style={{color:'rgba(255,255,255,0.4)',fontSize:9,marginRight:5,flexShrink:0,
transform:isCollapsed?'rotate(-90deg)':'rotate(0)',display:'inline-block',transition:'transform 0.15s'}}></span>
)}
<span style={{fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7}}>
{keyName!==null&&<span style={{color:keyColor}}>{keyName}<span style={{color:'rgba(255,255,255,0.4)'}}>: </span></span>}
{isCollapsed&&<span style={{color:'rgba(255,255,255,0.3)',fontSize:10}}>{summary}</span>}
{!isCollapsed&&isArr&&<span style={{color:'rgba(255,255,255,0.25)',fontSize:10}}># {count} Einträge</span>}
</span>
</div>
if (value === '|' || value === '>' || value === '|-' || value === '>-') { {/* Children */}
inMultiline = true; {!isCollapsed && (
multilineIndent = raw.search(/\S/); <div>
out.push(`${indent}${key}: ${value}`); {isObj && Object.entries(data).map(([k,v],i)=>(
} else if (value === '') { <YamlNode key={k} data={v} depth={depth+1} keyName={k}
out.push(`${indent}${key}:`); collapsed={collapsed} onToggle={onToggle} path={`${path}.${k}`}/>
} else { ))}
out.push(`${indent}${key}: ${value}`); {isArr && data.map((v,i)=>(
<div key={i} style={{position:'relative'}}>
<div style={{position:'absolute',left:(depth)*18+6,top:10,width:8,
borderTop:`1px solid ${indentColor}`}}/>
<div style={{paddingLeft:(depth+1)*18,minHeight:20,display:'flex',alignItems:'baseline'}}>
<span style={{color:'rgba(255,255,255,0.35)',fontFamily:"'Courier New',monospace",fontSize:12,
position:'absolute',left:(depth)*18+8}}>-</span>
{typeof v === 'object' && v !== null
? <YamlNode data={v} depth={depth+1} keyName={null}
collapsed={collapsed} onToggle={onToggle} path={`${path}[${i}]`}/>
: <div style={{fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7}}>
{v===null?<span style={{color:nullColor}}>null</span>:typeof v==='boolean'?<span style={{color:boolColor}}>{String(v)}</span>:typeof v==='number'?<span style={{color:numColor}}>{v}</span>:<span style={{color:strColor}}>{String(v)}</span>}
</div>
} }
continue; </div>
} </div>
))}
// Listenelement (- ...) </div>
if (trimmed.startsWith('- ') || trimmed === '-') { )}
out.push(`${indent}${trimmed}`); </div>
continue; );
}
// Alles andere
out.push(`${indent}${trimmed}`);
}
return out.join('\n');
} }
// Berechnet die logische Einrückungstiefe (2 Spaces pro Level) function collectPaths(data, path='root', paths=[]) {
function detectDepth(line) { if (data !== null && typeof data === 'object') {
const spaces = line.search(/\S/); paths.push(path);
if (spaces < 0) return 0; if (Array.isArray(data)) data.forEach((_,i)=>collectPaths(data[i],`${path}[${i}]`,paths));
return Math.round(spaces / 2); else Object.entries(data).forEach(([k,v])=>collectPaths(v,`${path}.${k}`,paths));
}
return paths;
} }
// Findet den Doppelpunkt eines Keys (nicht innerhalb von Strings) // ── YAML Tool ─────────────────────────────────────────────────────────────────
function findKeyColon(trimmed) { function YamlTool({ mobile }) {
let inSingle = false, inDouble = false;
for (let i = 0; i < trimmed.length; i++) {
const c = trimmed[i];
if (c === "'" && !inDouble) inSingle = !inSingle;
if (c === '"' && !inSingle) inDouble = !inDouble;
if (c === ':' && !inSingle && !inDouble) {
// Muss von Leerzeichen oder End gefolgt sein
const next = trimmed[i+1];
if (next === undefined || next === ' ' || next === '\t') return i;
}
}
return -1;
}
function YamlTool() {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [output, setOutput] = useState(''); const [parsed, setParsed] = useState(null);
const [formatted,setFormatted]= useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const outputRef = useRef(null); const [view, setView] = useState('tree'); // 'tree' | 'raw'
const [collapsed,setCollapsed]= useState(new Set());
const format = () => { const process = useCallback((text) => {
if (!input.trim()) { setError('Bitte YAML eingeben.'); setOutput(''); return; } if (!text.trim()) { setParsed(null); setFormatted(''); setError(''); return; }
try { try {
const result = parseAndFormatYaml(input); const obj = jsyaml.load(text);
setOutput(result); setParsed(obj);
setFormatted(jsyaml.dump(obj, { indent:2, lineWidth:-1, noRefs:true }));
setError(''); setError('');
setCollapsed(new Set()); // reset collapsed
} catch(e) { } catch(e) {
setError('Fehler beim Formatieren: ' + e.message); setError(e.message);
setOutput(''); setParsed(null); setFormatted('');
} }
}; },[]);
const onInput = v => { setInput(v); process(v); };
const copy = async () => { const copy = async () => {
try { try { await navigator.clipboard.writeText(formatted); setCopied(true); setTimeout(()=>setCopied(false),1500); } catch {}
await navigator.clipboard.writeText(output);
setCopied(true);
setTimeout(()=>setCopied(false), 1500);
} catch { }
}; };
const clear = () => { setInput(''); setOutput(''); setError(''); }; const toggleCollapse = path => {
setCollapsed(prev => {
// Auto-format on paste const n = new Set(prev);
const onPaste = e => { n.has(path) ? n.delete(path) : n.add(path);
setTimeout(()=>{ return n;
const val = e.target.value; });
if (val.trim()) {
const result = parseAndFormatYaml(val);
setOutput(result);
setError('');
}
}, 0);
}; };
const collapseAll = () => { if(parsed) setCollapsed(new Set(collectPaths(parsed))); };
const expandAll = () => setCollapsed(new Set());
const colCols = mobile ? '1fr' : '1fr 1fr';
return ( return (
<div> <div>
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12,marginBottom:10}}> <div style={{display:'grid',gridTemplateColumns:colCols,gap:12,marginBottom:10}}>
{/* Input */} {/* Input */}
<div> <div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
<label style={{...S.head,marginBottom:0,fontSize:10}}>EINGABE</label> <label style={{...S.head,marginBottom:0,fontSize:10}}>EINGABE</label>
{input && <button onClick={clear} style={{background:'transparent',border:'none', {input&&<button onClick={()=>{setInput('');setParsed(null);setFormatted('');setError('');}}
color:'rgba(255,255,255,0.3)',cursor:'pointer',fontFamily:'monospace',fontSize:10}}> Leeren</button>} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.3)',cursor:'pointer',fontFamily:'monospace',fontSize:10}}></button>}
</div> </div>
<textarea <textarea value={input} onChange={e=>onInput(e.target.value)}
value={input} placeholder={'name: Beispiel\nversion: 1.0\nkonfiguration:\n host: localhost\n port: 8080\nliste:\n - item1\n - item2'}
onChange={e=>setInput(e.target.value)} rows={mobile?10:18}
onPaste={onPaste} style={{...S.inp,resize:'vertical',fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7,
placeholder={'# YAML hier einfügen\nname: Beispiel\nversion: 1.0\nkonfiguration:\n host: localhost\n port: 8080\n debug: true\nliste:\n - item1\n - item2'} width:'100%',boxSizing:'border-box'}}/>
rows={16}
style={{...S.inp,resize:'vertical',fontFamily:"'Courier New',monospace",
fontSize:12,lineHeight:1.7,width:'100%',boxSizing:'border-box'}}
/>
</div> </div>
{/* Output */} {/* Output */}
<div> <div>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6,flexWrap:'wrap',gap:6}}>
<label style={{...S.head,marginBottom:0,fontSize:10}}>FORMATIERT</label> <label style={{...S.head,marginBottom:0,fontSize:10}}>ERGEBNIS</label>
{output && ( <div style={{display:'flex',gap:5,flexWrap:'wrap'}}>
<button onClick={copy} style={{ {/* View toggle */}
background: copied ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.05)', {['tree','raw'].map(v=>(
border: `1px solid ${copied ? 'rgba(78,205,196,0.3)' : 'rgba(255,255,255,0.1)'}`, <button key={v} onClick={()=>setView(v)} style={{
borderRadius:6,color:copied?'#4ecdc4':'rgba(255,255,255,0.5)', padding:'2px 8px',borderRadius:5,fontFamily:'monospace',fontSize:9,cursor:'pointer',
cursor:'pointer',fontFamily:'monospace',fontSize:10,padding:'3px 9px'}}> background:view===v?'rgba(78,205,196,0.15)':'transparent',
{copied ? '✓ Kopiert' : '⎘ Kopieren'} border:`1px solid ${view===v?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
color:view===v?'#4ecdc4':'rgba(255,255,255,0.4)'}}>
{v==='tree'?'🌲 Baum':'📄 Raw'}
</button> </button>
)} ))}
{parsed&&view==='tree'&&<>
<button onClick={collapseAll} style={{padding:'2px 8px',borderRadius:5,fontFamily:'monospace',fontSize:9,cursor:'pointer',background:'transparent',border:'1px solid rgba(255,255,255,0.1)',color:'rgba(255,255,255,0.4)'}}> Alle</button>
<button onClick={expandAll} style={{padding:'2px 8px',borderRadius:5,fontFamily:'monospace',fontSize:9,cursor:'pointer',background:'transparent',border:'1px solid rgba(255,255,255,0.1)',color:'rgba(255,255,255,0.4)'}}>+ Alle</button>
</>}
{formatted&&<button onClick={copy} style={{
padding:'2px 9px',borderRadius:5,fontFamily:'monospace',fontSize:9,cursor:'pointer',
background:copied?'rgba(78,205,196,0.15)':'rgba(255,255,255,0.05)',
border:`1px solid ${copied?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
color:copied?'#4ecdc4':'rgba(255,255,255,0.5)'}}>
{copied?'✓':'⎘'} Kopieren
</button>}
</div> </div>
</div>
{error ? ( {error ? (
<div style={{background:'rgba(255,107,157,0.08)',border:'1px solid rgba(255,107,157,0.2)', <div style={{background:'rgba(255,107,157,0.08)',border:'1px solid rgba(255,107,157,0.2)',
borderRadius:8,padding:14,color:'#ff6b9d',fontFamily:'monospace',fontSize:11,lineHeight:1.7}}> borderRadius:8,padding:14,color:'#ff6b9d',fontFamily:'monospace',fontSize:11,lineHeight:1.7}}>
{error} {error}
</div>
) : view==='raw' ? (
<pre style={{margin:0,padding:14,minHeight:200,background:'rgba(0,0,0,0.35)',
border:'1px solid rgba(255,255,255,0.07)',borderRadius:8,
color:formatted?'#d4e6b5':'rgba(255,255,255,0.15)',
fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7,
overflowX:'auto',whiteSpace:'pre',maxHeight:440,overflowY:'auto'}}>
{formatted||'# Ergebnis erscheint hier'}
</pre>
) : parsed!==null ? (
<div style={{background:'rgba(0,0,0,0.35)',border:'1px solid rgba(255,255,255,0.07)',
borderRadius:8,padding:'10px 10px 10px 12px',minHeight:200,maxHeight:440,overflowY:'auto',
overflowX:'auto',position:'relative'}}>
<YamlNode data={parsed} depth={0} keyName={null}
collapsed={collapsed} onToggle={toggleCollapse} path="root"/>
</div> </div>
) : ( ) : (
<pre ref={outputRef} style={{ <div style={{background:'rgba(0,0,0,0.2)',border:'1px solid rgba(255,255,255,0.06)',
margin:0,padding:14,minHeight:280, borderRadius:8,padding:30,textAlign:'center',color:'rgba(255,255,255,0.15)',
background:'rgba(0,0,0,0.35)',border:'1px solid rgba(255,255,255,0.07)', fontFamily:'monospace',fontSize:11,minHeight:100,display:'flex',alignItems:'center',justifyContent:'center'}}>
borderRadius:8,color: output ? '#d4e6b5' : 'rgba(255,255,255,0.15)', YAML eingeben wird live formatiert
fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7, </div>
overflowX:'auto',whiteSpace:'pre',wordBreak:'normal',
overflowY:'auto',maxHeight:400,
}}>
{output || '# Ergebnis erscheint hier'}
</pre>
)} )}
</div> </div>
</div> </div>
<div style={{color:'rgba(255,255,255,0.15)',fontFamily:'monospace',fontSize:9}}>
<div style={{display:'flex',gap:8}}> Powered by js-yaml · 2-Space-Einrückung · YAML 1.2 kompatibel
<button onClick={format}
style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',fontSize:12}}>
Formatieren
</button>
</div>
<div style={{marginTop:10,color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:9,lineHeight:1.8}}>
Tipp: YAML wird beim Einfügen automatisch formatiert · 2-Space-Einrückung · Schlüssel: Wert Syntax
</div> </div>
</div> </div>
); );
@@ -205,58 +247,16 @@ function YamlTool() {
// ── Tool-Definitionen ───────────────────────────────────────────────────────── // ── Tool-Definitionen ─────────────────────────────────────────────────────────
const TOOL_DEFS = [ const TOOL_DEFS = [
{ { id:'yaml', label:'📄 YAML Formatter', desc:'YAML korrekt einrücken und formatieren', ready:true, component:YamlTool },
id: 'yaml', { id:'json', label:'{ } JSON Formatter', desc:'JSON formatieren und validieren', ready:false },
label: '📄 YAML Formatter', { id:'base64', label:'🔐 Base64', desc:'Text ↔ Base64 kodieren/dekodieren', ready:false },
desc: 'YAML korrekt einrücken und formatieren', { id:'regex', label:'🔍 Regex Tester', desc:'Reguläre Ausdrücke live testen', ready:false },
ready: true, { id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:false },
component: YamlTool, { id:'jwt', label:'🔑 JWT Decoder', desc:'JSON Web Tokens dekodieren', ready:false },
}, { id:'timestamp', label:'🕐 Timestamp', desc:'Unix Timestamps umrechnen', ready:false },
{ { id:'color', label:'🎨 Farben', desc:'HEX ↔ RGB ↔ HSL umrechnen', ready:false },
id: 'json',
label: '{ } JSON Formatter',
desc: 'JSON formatieren und validieren',
ready: false,
},
{
id: 'base64',
label: '🔐 Base64',
desc: 'Text ↔ Base64 kodieren/dekodieren',
ready: false,
},
{
id: 'regex',
label: '🔍 Regex Tester',
desc: 'Reguläre Ausdrücke live testen',
ready: false,
},
{
id: 'diff',
label: '± Text Diff',
desc: 'Zwei Texte vergleichen',
ready: false,
},
{
id: 'jwt',
label: '🔑 JWT Decoder',
desc: 'JSON Web Tokens dekodieren und prüfen',
ready: false,
},
{
id: 'timestamp',
label: '🕐 Timestamp',
desc: 'Unix Timestamps umrechnen',
ready: false,
},
{
id: 'color',
label: '🎨 Farben',
desc: 'HEX ↔ RGB ↔ HSL umrechnen',
ready: false,
},
]; ];
// ── Main Component ────────────────────────────────────────────────────────────
export default function DevTools({ toast, mobile }) { export default function DevTools({ toast, mobile }) {
const [activeTool, setActiveTool] = useState('yaml'); const [activeTool, setActiveTool] = useState('yaml');
const active = TOOL_DEFS.find(t=>t.id===activeTool); const active = TOOL_DEFS.find(t=>t.id===activeTool);
@@ -265,38 +265,26 @@ export default function DevTools({ toast, mobile }) {
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}> <div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}>
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:20,flexWrap:'wrap'}}> <div style={{display:'flex',alignItems:'center',gap:12,marginBottom:20,flexWrap:'wrap'}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Dev-Tools</h1> <h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Dev-Tools</h1>
<span style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10}}> <span style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10}}>Konverter · Formatter · Helfer</span>
Konverter · Formatter · Helfer
</span>
</div> </div>
{/* Tool-Auswahl */}
<div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:20}}> <div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:20}}>
{TOOL_DEFS.map(t=>( {TOOL_DEFS.map(t=>(
<button key={t.id} <button key={t.id}
onClick={()=>{ if(t.ready) setActiveTool(t.id); else toast('Kommt bald 🚧','error'); }} onClick={()=>t.ready ? setActiveTool(t.id) : toast('Kommt bald 🚧','error')}
title={t.desc} title={t.desc}
style={{ style={{
padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11, padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11,cursor:t.ready?'pointer':'default',
cursor:t.ready?'pointer':'default', background: activeTool===t.id?'#4ecdc4':t.ready?'rgba(255,255,255,0.06)':'rgba(255,255,255,0.02)',
background: activeTool===t.id ? '#4ecdc4' color: activeTool===t.id?'#0d0d0f':t.ready?'rgba(255,255,255,0.65)':'rgba(255,255,255,0.2)',
: t.ready ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)', border: activeTool===t.id?'none':t.ready?'1px solid rgba(255,255,255,0.1)':'1px solid rgba(255,255,255,0.05)',
color: activeTool===t.id ? '#0d0d0f' fontWeight: activeTool===t.id?700:400,
: t.ready ? 'rgba(255,255,255,0.65)' : 'rgba(255,255,255,0.2)',
border: activeTool===t.id ? 'none'
: t.ready ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(255,255,255,0.05)',
fontWeight: activeTool===t.id ? 700 : 400,
position:'relative',
}}> }}>
{t.label} {t.label}{!t.ready&&<span style={{marginLeft:4,fontSize:8,opacity:0.5}}>bald</span>}
{!t.ready && (
<span style={{marginLeft:5,fontSize:8,opacity:0.5}}>bald</span>
)}
</button> </button>
))} ))}
</div> </div>
{/* Aktives Tool */}
<div style={{...S.card}}> <div style={{...S.card}}>
<div style={{display:'flex',alignItems:'baseline',gap:10,marginBottom:14}}> <div style={{display:'flex',alignItems:'baseline',gap:10,marginBottom:14}}>
<div style={{...S.head,marginBottom:0}}>{active?.label}</div> <div style={{...S.head,marginBottom:0}}>{active?.label}</div>