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="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"/>
|
<path d="M13 13h5M13 17h3" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round"/>
|
||||||
</>, size, color, sw);
|
</>, 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 Linkliste from './tools/linkliste.jsx';
|
||||||
import CodeSchnipsel from './tools/codeschnipsel.jsx';
|
import CodeSchnipsel from './tools/codeschnipsel.jsx';
|
||||||
import Skizze from './tools/skizze.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 = [
|
export const TOOLS = [
|
||||||
{
|
{
|
||||||
@@ -67,11 +68,19 @@ export const TOOLS = [
|
|||||||
{
|
{
|
||||||
id: 'skizze',
|
id: 'skizze',
|
||||||
Icon: SkizzeIcon,
|
Icon: SkizzeIcon,
|
||||||
label: 'Skizzen-Rechner',
|
label: 'Skizzen-Rechner (β)',
|
||||||
navLabel: 'Skizze',
|
navLabel: 'Skizze β',
|
||||||
group: 'Werkzeuge',
|
group: 'Werkzeuge',
|
||||||
component: Skizze,
|
component: Skizze,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'devtools',
|
||||||
|
Icon: WrenchIcon,
|
||||||
|
label: 'Dev-Tools',
|
||||||
|
navLabel: 'Dev-Tools',
|
||||||
|
group: 'Werkzeuge',
|
||||||
|
component: DevTools,
|
||||||
|
},
|
||||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
// 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