YAML Formatter: js-yaml, Baumansicht mit Linien, Einklappen, Mobile-Fix
This commit is contained in:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user