Dev-Tools: neues Tool mit YAML Formatter, Skizze BETA label
This commit is contained in:
@@ -150,3 +150,6 @@ export const SkizzeIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M3 9h18M9 21V9" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round"/>
|
||||
<path d="M13 13h5M13 17h3" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round"/>
|
||||
</>, size, color, sw);
|
||||
export const WrenchIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</>, size, color, sw);
|
||||
|
||||
@@ -5,7 +5,8 @@ import Nachrichten from './tools/nachrichten.jsx';
|
||||
import Linkliste from './tools/linkliste.jsx';
|
||||
import CodeSchnipsel from './tools/codeschnipsel.jsx';
|
||||
import Skizze from './tools/skizze.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon } from './icons.jsx';
|
||||
import DevTools from './tools/devtools.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon } from './icons.jsx';
|
||||
|
||||
export const TOOLS = [
|
||||
{
|
||||
@@ -67,11 +68,19 @@ export const TOOLS = [
|
||||
{
|
||||
id: 'skizze',
|
||||
Icon: SkizzeIcon,
|
||||
label: 'Skizzen-Rechner',
|
||||
navLabel: 'Skizze',
|
||||
label: 'Skizzen-Rechner (β)',
|
||||
navLabel: 'Skizze β',
|
||||
group: 'Werkzeuge',
|
||||
component: Skizze,
|
||||
},
|
||||
{
|
||||
id: 'devtools',
|
||||
Icon: WrenchIcon,
|
||||
label: 'Dev-Tools',
|
||||
navLabel: 'Dev-Tools',
|
||||
group: 'Werkzeuge',
|
||||
component: DevTools,
|
||||
},
|
||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
||||
];
|
||||
|
||||
|
||||
309
frontend/src/tools/devtools.jsx
Normal file
309
frontend/src/tools/devtools.jsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { S } from '../lib.js';
|
||||
|
||||
// ── YAML Formatter ────────────────────────────────────────────────────────────
|
||||
function parseAndFormatYaml(input) {
|
||||
// YAML-konformes Einrücken mit 2 Spaces
|
||||
// Wir parsen manuell: kein externes package, aber korrekte Einrückung
|
||||
const lines = input.split('\n');
|
||||
const out = [];
|
||||
let inMultiline = false;
|
||||
let multilineIndent = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const raw = lines[i];
|
||||
const trimmed = raw.trim();
|
||||
|
||||
// Leerzeilen beibehalten
|
||||
if (!trimmed) { out.push(''); continue; }
|
||||
|
||||
// Kommentare unverändert
|
||||
if (trimmed.startsWith('#')) {
|
||||
const depth = detectDepth(raw);
|
||||
out.push(' '.repeat(depth) + trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiline-Block (| oder >) – Zeilen danach unverändert einrücken
|
||||
if (inMultiline) {
|
||||
// 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);
|
||||
const indent = ' '.repeat(depth);
|
||||
|
||||
// Key: Value
|
||||
const colonIdx = findKeyColon(trimmed);
|
||||
if (colonIdx > 0) {
|
||||
const key = trimmed.slice(0, colonIdx).trim();
|
||||
const value = trimmed.slice(colonIdx + 1).trim();
|
||||
|
||||
if (value === '|' || value === '>' || value === '|-' || value === '>-') {
|
||||
inMultiline = true;
|
||||
multilineIndent = raw.search(/\S/);
|
||||
out.push(`${indent}${key}: ${value}`);
|
||||
} else if (value === '') {
|
||||
out.push(`${indent}${key}:`);
|
||||
} else {
|
||||
out.push(`${indent}${key}: ${value}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Listenelement (- ...)
|
||||
if (trimmed.startsWith('- ') || trimmed === '-') {
|
||||
out.push(`${indent}${trimmed}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Alles andere
|
||||
out.push(`${indent}${trimmed}`);
|
||||
}
|
||||
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
// Berechnet die logische Einrückungstiefe (2 Spaces pro Level)
|
||||
function detectDepth(line) {
|
||||
const spaces = line.search(/\S/);
|
||||
if (spaces < 0) return 0;
|
||||
return Math.round(spaces / 2);
|
||||
}
|
||||
|
||||
// Findet den Doppelpunkt eines Keys (nicht innerhalb von Strings)
|
||||
function findKeyColon(trimmed) {
|
||||
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 [output, setOutput] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const outputRef = useRef(null);
|
||||
|
||||
const format = () => {
|
||||
if (!input.trim()) { setError('Bitte YAML eingeben.'); setOutput(''); return; }
|
||||
try {
|
||||
const result = parseAndFormatYaml(input);
|
||||
setOutput(result);
|
||||
setError('');
|
||||
} catch(e) {
|
||||
setError('Fehler beim Formatieren: ' + e.message);
|
||||
setOutput('');
|
||||
}
|
||||
};
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(output);
|
||||
setCopied(true);
|
||||
setTimeout(()=>setCopied(false), 1500);
|
||||
} catch { }
|
||||
};
|
||||
|
||||
const clear = () => { setInput(''); setOutput(''); setError(''); };
|
||||
|
||||
// Auto-format on paste
|
||||
const onPaste = e => {
|
||||
setTimeout(()=>{
|
||||
const val = e.target.value;
|
||||
if (val.trim()) {
|
||||
const result = parseAndFormatYaml(val);
|
||||
setOutput(result);
|
||||
setError('');
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12,marginBottom:10}}>
|
||||
{/* Input */}
|
||||
<div>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
|
||||
<label style={{...S.head,marginBottom:0,fontSize:10}}>EINGABE</label>
|
||||
{input && <button onClick={clear} style={{background:'transparent',border:'none',
|
||||
color:'rgba(255,255,255,0.3)',cursor:'pointer',fontFamily:'monospace',fontSize:10}}>✕ Leeren</button>}
|
||||
</div>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={e=>setInput(e.target.value)}
|
||||
onPaste={onPaste}
|
||||
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'}
|
||||
rows={16}
|
||||
style={{...S.inp,resize:'vertical',fontFamily:"'Courier New',monospace",
|
||||
fontSize:12,lineHeight:1.7,width:'100%',boxSizing:'border-box'}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Output */}
|
||||
<div>
|
||||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:6}}>
|
||||
<label style={{...S.head,marginBottom:0,fontSize:10}}>FORMATIERT</label>
|
||||
{output && (
|
||||
<button onClick={copy} style={{
|
||||
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)'}`,
|
||||
borderRadius:6,color:copied?'#4ecdc4':'rgba(255,255,255,0.5)',
|
||||
cursor:'pointer',fontFamily:'monospace',fontSize:10,padding:'3px 9px'}}>
|
||||
{copied ? '✓ Kopiert' : '⎘ Kopieren'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error ? (
|
||||
<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}}>
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<pre ref={outputRef} style={{
|
||||
margin:0,padding:14,minHeight:280,
|
||||
background:'rgba(0,0,0,0.35)',border:'1px solid rgba(255,255,255,0.07)',
|
||||
borderRadius:8,color: output ? '#d4e6b5' : 'rgba(255,255,255,0.15)',
|
||||
fontFamily:"'Courier New',monospace",fontSize:12,lineHeight:1.7,
|
||||
overflowX:'auto',whiteSpace:'pre',wordBreak:'normal',
|
||||
overflowY:'auto',maxHeight:400,
|
||||
}}>
|
||||
{output || '# Ergebnis erscheint hier'}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{display:'flex',gap:8}}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tool-Definitionen ─────────────────────────────────────────────────────────
|
||||
const TOOL_DEFS = [
|
||||
{
|
||||
id: 'yaml',
|
||||
label: '📄 YAML Formatter',
|
||||
desc: 'YAML korrekt einrücken und formatieren',
|
||||
ready: true,
|
||||
component: YamlTool,
|
||||
},
|
||||
{
|
||||
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 }) {
|
||||
const [activeTool, setActiveTool] = useState('yaml');
|
||||
const active = TOOL_DEFS.find(t=>t.id===activeTool);
|
||||
|
||||
return (
|
||||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}>
|
||||
<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>
|
||||
<span style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10}}>
|
||||
Konverter · Formatter · Helfer
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tool-Auswahl */}
|
||||
<div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:20}}>
|
||||
{TOOL_DEFS.map(t=>(
|
||||
<button key={t.id}
|
||||
onClick={()=>{ if(t.ready) setActiveTool(t.id); else toast('Kommt bald 🚧','error'); }}
|
||||
title={t.desc}
|
||||
style={{
|
||||
padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11,
|
||||
cursor:t.ready?'pointer':'default',
|
||||
background: activeTool===t.id ? '#4ecdc4'
|
||||
: t.ready ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
|
||||
color: activeTool===t.id ? '#0d0d0f'
|
||||
: 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.ready && (
|
||||
<span style={{marginLeft:5,fontSize:8,opacity:0.5}}>bald</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Aktives Tool */}
|
||||
<div style={{...S.card}}>
|
||||
<div style={{display:'flex',alignItems:'baseline',gap:10,marginBottom:14}}>
|
||||
<div style={{...S.head,marginBottom:0}}>{active?.label}</div>
|
||||
<span style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10}}>{active?.desc}</span>
|
||||
</div>
|
||||
{active?.component && <active.component toast={toast} mobile={mobile}/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user