Dev-Tools: neues Tool mit YAML Formatter, Skizze BETA label

This commit is contained in:
2026-05-29 19:13:04 +02:00
parent 0d6b131cd0
commit b065c5350a
3 changed files with 324 additions and 3 deletions

View File

@@ -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);

View File

@@ -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:... },
]; ];

View 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>
);
}