Files
dickendock/frontend/src/tools/skizze.jsx

493 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useCallback, useEffect } from 'react';
import { api, S } from '../lib.js';
// ── Hilfsfunktionen ──────────────────────────────────────────────────────────
const uid = () => Math.random().toString(36).slice(2,8);
const dist = (x1,y1,x2,y2) => Math.sqrt((x2-x1)**2+(y2-y1)**2);
const snapToGrid = (v, g) => Math.round(v / g) * g;
function parseDim(s) {
if (!s) return null;
const n = parseFloat(String(s).replace(/[^0-9.,\-]/g,'').replace(',','.'));
return isNaN(n) ? null : n;
}
function evalFormula(expr, vars) {
try {
const sanitized = expr.replace(/[^0-9+\-*/().,%a-zA-Z_\s]/g,'');
const keys = Object.keys(vars), vals = keys.map(k=>vars[k]);
// eslint-disable-next-line no-new-func
return new Function(...keys, `"use strict"; return (${sanitized});`)(...vals);
} catch { return null; }
}
function buildVars(shapes) {
const vars = {};
shapes.forEach(s => {
const n = (s.name||'').replace(/[^a-zA-Z0-9_]/g,'_');
if (!n) return;
if (s.type==='rect') {
const l=parseDim(s.dimW), b=parseDim(s.dimH);
if (l!=null) { vars[`${n}_l`]=l; vars[`${n}_b`]=b??l; }
if (l!=null&&b!=null) { vars[`${n}_fl`]=+(l*b).toFixed(4); vars[`${n}_um`]=+(2*(l+b)).toFixed(4); vars[`${n}_d`]=+(Math.sqrt(l*l+b*b)).toFixed(4); }
}
if (s.type==='line') { const v=parseDim(s.dimLen); if(v!=null) vars[`${n}_len`]=v; }
if (s.type==='circle') {
const r=parseDim(s.dimR);
if(r!=null){ vars[`${n}_r`]=r; vars[`${n}_d`]=+(r*2).toFixed(4); vars[`${n}_fl`]=+(Math.PI*r*r).toFixed(4); vars[`${n}_um`]=+(2*Math.PI*r).toFixed(4); }
}
});
return vars;
}
// ── Row-Komponente (AUSSERHALB um Input-Bug zu vermeiden) ─────────────────────
function DimRow({ label, value, field, placeholder, onChange }) {
return (
<div style={{marginBottom:8}}>
<div style={{...S.head,marginBottom:3,fontSize:9}}>{label}</div>
<input value={value||''} onChange={e=>onChange({[field]:e.target.value})}
placeholder={placeholder} autoCapitalize="none"
style={{...S.inp,fontSize:12,padding:'6px 8px'}}/>
</div>
);
}
// ── Maßlinie ─────────────────────────────────────────────────────────────────
function DimLine({x1,y1,x2,y2,label,color='#4ecdc4',offset=18}) {
const mx=(x1+x2)/2, my=(y1+y2)/2;
const dx=x2-x1, dy=y2-y1, len=Math.sqrt(dx*dx+dy*dy);
if (len<4) return null;
const nx=-dy/len*offset, ny=dx/len*offset;
return (
<g>
<line x1={x1+nx} y1={y1+ny} x2={x2+nx} y2={y2+ny} stroke={color} strokeWidth={1} strokeDasharray="3,2" opacity={0.7}/>
<line x1={x1} y1={y1} x2={x1+nx} y2={y1+ny} stroke={color} strokeWidth={0.7} opacity={0.4}/>
<line x1={x2} y1={y2} x2={x2+nx} y2={y2+ny} stroke={color} strokeWidth={0.7} opacity={0.4}/>
<text x={mx+nx} y={my+ny} textAnchor="middle" dominantBaseline="middle"
fontSize={9} fill={color} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{label}</text>
</g>
);
}
// ── Shape + Mittelpunkt + Versatz ─────────────────────────────────────────────
function Shape({ s, selected, onClick }) {
const sel = selected===s.id;
const c = sel ? '#4ecdc4' : (s.color||'#7ecaff');
const offset = parseDim(s.offset) || 0; // Versatz in px (scaled)
if (s.type==='rect') {
const x=Math.min(s.x,s.x+s.w), y=Math.min(s.y,s.y+s.h);
const w=Math.abs(s.w), h=Math.abs(s.h);
const cx=x+w/2, cy=y+h/2;
const offPx = s.offsetPx||0;
return (
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
<rect x={x} y={y} width={w} height={h}
fill={sel?'rgba(78,205,196,0.06)':'rgba(126,202,255,0.04)'}
stroke={c} strokeWidth={sel?1.5:1}/>
{/* Mittelpunkt */}
<line x1={cx-6} y1={cy} x2={cx+6} y2={cy} stroke={c} strokeWidth={0.8} opacity={0.5}/>
<line x1={cx} y1={cy-6} x2={cx} y2={cy+6} stroke={c} strokeWidth={0.8} opacity={0.5}/>
{/* Versatz */}
{offPx!==0&&<rect x={x-offPx} y={y-offPx} width={w+2*offPx} height={h+2*offPx}
fill="none" stroke={offPx>0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>}
{s.name&&<text x={cx} y={cy+2} textAnchor="middle" dominantBaseline="middle"
fontSize={10} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{s.name}</text>}
{s.dimW&&<DimLine x1={x} y1={y+h} x2={x+w} y2={y+h} label={s.dimW} offset={14}/>}
{s.dimH&&<DimLine x1={x+w} y1={y} x2={x+w} y2={y+h} label={s.dimH} offset={14}/>}
{sel&&<>
<rect x={x-3} y={y-3} width={6} height={6} fill={c} rx={1}/>
<rect x={x+w-3} y={y-3} width={6} height={6} fill={c} rx={1}/>
<rect x={x-3} y={y+h-3} width={6} height={6} fill={c} rx={1}/>
<rect x={x+w-3} y={y+h-3} width={6} height={6} fill={c} rx={1}/>
</>}
</g>
);
}
if (s.type==='line') {
const len=dist(s.x1,s.y1,s.x2,s.y2);
const mx=(s.x1+s.x2)/2, my=(s.y1+s.y2)/2;
return (
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="transparent" strokeWidth={12}/>
<line x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke={c} strokeWidth={sel?2:1.5} strokeLinecap="round"/>
<circle cx={mx} cy={my} r={2} fill={c} opacity={0.6}/>
{(s.dimLen||s.name)&&len>20&&<DimLine x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} label={s.dimLen||s.name} offset={12}/>}
{sel&&<><circle cx={s.x1} cy={s.y1} r={4} fill={c}/><circle cx={s.x2} cy={s.y2} r={4} fill={c}/></>}
</g>
);
}
if (s.type==='circle') {
const r=Math.max(1,s.r);
const offPx=s.offsetPx||0;
return (
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
<circle cx={s.cx} cy={s.cy} r={r} fill={sel?'rgba(78,205,196,0.06)':'rgba(126,202,255,0.04)'} stroke={c} strokeWidth={sel?1.5:1}/>
{offPx!==0&&<circle cx={s.cx} cy={s.cy} r={r+offPx} fill="none" stroke={offPx>0?'#ffe66d':'#ff6b9d'} strokeWidth={1} strokeDasharray="4,3" opacity={0.7}/>}
{/* Mittelpunkt */}
<line x1={s.cx-6} y1={s.cy} x2={s.cx+6} y2={s.cy} stroke={c} strokeWidth={0.8} opacity={0.5}/>
<line x1={s.cx} y1={s.cy-6} x2={s.cx} y2={s.cy+6} stroke={c} strokeWidth={0.8} opacity={0.5}/>
{/* Radiuslinie */}
<line x1={s.cx} y1={s.cy} x2={s.cx+r} y2={s.cy} stroke={c} strokeWidth={0.7} strokeDasharray="3,2" opacity={0.4}/>
{s.name&&<text x={s.cx} y={s.cy+2} textAnchor="middle" dominantBaseline="middle"
fontSize={10} fill={c} fontFamily="'Courier New',monospace" style={{userSelect:'none'}}
stroke="#0a0a0c" strokeWidth={3} paintOrder="stroke">{s.name}</text>}
{s.dimR&&<DimLine x1={s.cx} y1={s.cy} x2={s.cx+r} y2={s.cy} label={'r='+s.dimR} offset={8} color="#ffe66d"/>}
{sel&&<circle cx={s.cx} cy={s.cy} r={4} fill={c}/>}
</g>
);
}
if (s.type==='text') {
return (
<g onClick={e=>{e.stopPropagation();onClick(s.id);}} style={{cursor:'pointer'}}>
<text x={s.x} y={s.y} fontSize={12} fill={sel?'#4ecdc4':'rgba(255,255,255,0.75)'}
fontFamily="'Courier New',monospace" style={{userSelect:'none'}}>{s.label||'Text'}</text>
{sel&&<rect x={s.x-2} y={s.y-14} width={Math.max(40,(s.label||'Text').length*7+4)} height={18}
fill="none" stroke="#4ecdc4" strokeWidth={1} strokeDasharray="3,2"/>}
</g>
);
}
return null;
}
// ── Properties Panel ──────────────────────────────────────────────────────────
function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) {
if (!shape) return (
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,padding:'14px 0',textAlign:'center',lineHeight:1.8}}>
Form auswählen oder zeichnen.<br/>
<span style={{fontSize:9,opacity:0.6}}>V=Auswählen R=Rect L=Linie C=Kreis</span>
</div>
);
const shapeVarKeys = Object.keys(vars).filter(k=>k.startsWith((shape.name||'').replace(/[^a-zA-Z0-9_]/g,'_')+'_'));
return (
<div>
<div style={{...S.head,marginBottom:10}}>EIGENSCHAFTEN</div>
<DimRow label="NAME" value={shape.name} field="name" placeholder="z.B. Kasten" onChange={onChange}/>
{shape.type==='rect'&&<>
<DimRow label="LÄNGE" value={shape.dimW} field="dimW" placeholder="225mm" onChange={onChange}/>
<DimRow label="BREITE" value={shape.dimH} field="dimH" placeholder="30mm" onChange={onChange}/>
<DimRow label="VERSATZ (+ aussen / innen)" value={shape.dimOffset} field="dimOffset" placeholder="5mm" onChange={onChange}/>
</>}
{shape.type==='line'&&<DimRow label="LÄNGE" value={shape.dimLen} field="dimLen" placeholder="100mm" onChange={onChange}/>}
{shape.type==='circle'&&<>
<DimRow label="RADIUS" value={shape.dimR} field="dimR" placeholder="50mm" onChange={onChange}/>
<DimRow label="VERSATZ (+ aussen / innen)" value={shape.dimOffset} field="dimOffset" placeholder="5mm" onChange={onChange}/>
</>}
{shape.type==='text'&&<DimRow label="TEXT" value={shape.label} field="label" placeholder="Beschriftung" onChange={onChange}/>}
<button onClick={onDelete}
style={{...S.btn('#ff6b9d',true),width:'100%',textAlign:'center',padding:'7px 0',fontSize:11,marginTop:4}}>
Löschen
</button>
{shapeVarKeys.length>0&&(
<div style={{marginTop:12}}>
<div style={{...S.head,marginBottom:5,fontSize:9}}>VARIABLEN</div>
{shapeVarKeys.map(k=>(
<div key={k} style={{display:'flex',justifyContent:'space-between',padding:'3px 0',
borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
<code style={{color:'#4ecdc4',fontSize:10}}>{k}</code>
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10}}>
{typeof vars[k]==='number'?vars[k].toFixed(2):vars[k]}
</span>
</div>
))}
</div>
)}
<div style={{marginTop:14}}>
<div style={{...S.head,marginBottom:5,fontSize:9}}>MASSSTAB (mm/px)</div>
<div style={{display:'flex',gap:6,alignItems:'center'}}>
<input type="number" value={scale} onChange={e=>onScaleChange(parseFloat(e.target.value)||1)}
step={0.1} min={0.1} style={{...S.inp,flex:1,fontSize:12,padding:'5px 8px'}}/>
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:9}}>mm/px</span>
</div>
</div>
</div>
);
}
// ── Main ──────────────────────────────────────────────────────────────────────
const TOOLS = [
{id:'select',label:'▲',title:'Auswählen (V)'},
{id:'rect', label:'▭',title:'Rechteck (R)'},
{id:'line', label:'',title:'Linie (L)'},
{id:'circle',label:'○',title:'Kreis (C)'},
{id:'text', label:'T', title:'Text (T)'},
];
const GRID = 10;
const CALC_BTNS = ['(',')',' ','7','8','9','×','4','5','6','÷','1','2','3','-','0','.','=','+'];
export default function Skizze({ toast, mobile }) {
const [shapes, setShapes] = useState([]);
const [selected, setSelected] = useState(null);
const [tool, setTool] = useState('select');
const [drawing, setDrawing] = useState(null);
const [formula, setFormula] = useState('');
const [results, setResults] = useState([]);
const [snapGrid, setSnapGrid] = useState(true);
const [scale, setScale] = useState(1); // mm per pixel
const svgRef = useRef(null);
const formulaRef= useRef(null);
const vars = buildVars(shapes);
const selShape = shapes.find(s=>s.id===selected)||null;
// Keyboard shortcuts
useEffect(()=>{
const h = e => {
if (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
const map={v:'select',r:'rect',l:'line',c:'circle',t:'text'};
if (map[e.key.toLowerCase()]) setTool(map[e.key.toLowerCase()]);
if ((e.key==='Delete'||e.key==='Backspace')&&selected) deleteSelected();
if (e.key==='Escape') { setSelected(null); setDrawing(null); }
};
window.addEventListener('keydown',h);
return()=>window.removeEventListener('keydown',h);
},[selected]);
const getSVGPoint = useCallback(e=>{
const rect=svgRef.current?.getBoundingClientRect();
if (!rect) return{x:0,y:0};
const cx=e.touches?e.touches[0].clientX:e.clientX;
const cy=e.touches?e.touches[0].clientY:e.clientY;
const x=cx-rect.left, y=cy-rect.top;
return {x:snapGrid?snapToGrid(x,GRID):x, y:snapGrid?snapToGrid(y,GRID):y};
},[snapGrid]);
const onSVGDown = useCallback(e=>{
if (tool==='select'){setSelected(null);return;}
const pt=getSVGPoint(e);
if (tool==='text'){
setShapes(p=>[...p,{id:uid(),type:'text',x:pt.x,y:pt.y,label:'Text',name:''}]);
setTool('select'); return;
}
setDrawing({tool,startX:pt.x,startY:pt.y,curX:pt.x,curY:pt.y});
},[tool,getSVGPoint]);
const onSVGMove = useCallback(e=>{
if (!drawing) return;
const pt=getSVGPoint(e);
setDrawing(d=>({...d,curX:pt.x,curY:pt.y}));
},[drawing,getSVGPoint]);
const onSVGUp = useCallback(e=>{
if (!drawing) return;
const pt=getSVGPoint(e);
const dx=pt.x-drawing.startX, dy=pt.y-drawing.startY;
if (Math.abs(dx)<4&&Math.abs(dy)<4){setDrawing(null);return;}
let s;
if (drawing.tool==='rect') s={id:uid(),type:'rect',x:drawing.startX,y:drawing.startY,w:dx,h:dy,name:'',dimW:'',dimH:'',dimOffset:''};
if (drawing.tool==='line') s={id:uid(),type:'line',x1:drawing.startX,y1:drawing.startY,x2:pt.x,y2:pt.y,name:'',dimLen:''};
if (drawing.tool==='circle') s={id:uid(),type:'circle',cx:drawing.startX,cy:drawing.startY,r:dist(drawing.startX,drawing.startY,pt.x,pt.y),name:'',dimR:'',dimOffset:''};
if (s){setShapes(p=>[...p,s]);setSelected(s.id);}
setDrawing(null);
},[drawing,getSVGPoint]);
const deleteSelected = ()=>{setShapes(p=>p.filter(s=>s.id!==selected));setSelected(null);};
// Update shape + auto-resize visual proportions based on scale
const updateShape = patch => {
setShapes(p=>p.map(s=>{
if (s.id!==selected) return s;
const upd={...s,...patch};
// Auto-resize rect
if (upd.type==='rect') {
const mW=parseDim(upd.dimW), mH=parseDim(upd.dimH);
if (mW!=null&&mW>0) {
upd.w=Math.sign(upd.w||1)*mW/scale;
if (mH!=null&&mH>0) upd.h=Math.sign(upd.h||1)*mH/scale;
}
// Versatz in px
const mO=parseDim(upd.dimOffset);
upd.offsetPx=mO!=null?mO/scale:0;
}
// Auto-resize circle
if (upd.type==='circle') {
const mR=parseDim(upd.dimR);
if (mR!=null&&mR>0) upd.r=mR/scale;
const mO=parseDim(upd.dimOffset);
upd.offsetPx=mO!=null?mO/scale:0;
}
return upd;
}));
};
const calcFormula = () => {
if (!formula.trim()) return;
const result=evalFormula(formula,vars);
const isArea=formula.toLowerCase().includes('_fl');
setResults(p=>[{
id:uid(), expr:formula,
result:result!=null?`${typeof result==='number'?result.toFixed(4).replace(/.?0+$/,''):result}${isArea?' mm²':' mm'}`:null,
},...p].slice(0,30));
};
const insertCalc = btn => {
if (btn==='='){calcFormula();return;}
const map={'×':'*','÷':'/',' ':' '};
const char=map[btn]??btn;
setFormula(f=>f+char);
formulaRef.current?.focus();
};
// Preview while drawing
let preview=null;
if (drawing){
const dx=drawing.curX-drawing.startX, dy=drawing.curY-drawing.startY;
const pc='#4ecdc4';
if (drawing.tool==='rect') preview=<rect x={Math.min(drawing.startX,drawing.curX)} y={Math.min(drawing.startY,drawing.curY)} width={Math.abs(dx)} height={Math.abs(dy)} fill="rgba(78,205,196,0.04)" stroke={pc} strokeWidth={1} strokeDasharray="4,3"/>;
if (drawing.tool==='line') preview=<line x1={drawing.startX} y1={drawing.startY} x2={drawing.curX} y2={drawing.curY} stroke={pc} strokeWidth={1.5} strokeDasharray="4,3"/>;
if (drawing.tool==='circle'){const r=dist(drawing.startX,drawing.startY,drawing.curX,drawing.curY); preview=<circle cx={drawing.startX} cy={drawing.startY} r={r} fill="rgba(78,205,196,0.04)" stroke={pc} strokeWidth={1} strokeDasharray="4,3"/>;}
}
const canvasH = mobile ? 260 : 420;
return (
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:1100}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:12,flexWrap:'wrap',gap:8}}>
<div style={{display:'flex',alignItems:'center',gap:12,flexWrap:'wrap'}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?16:22,margin:0}}>Skizzen-Rechner</h1>
<div style={{
display:'flex',alignItems:'center',gap:6,
background:'repeating-linear-gradient(45deg,rgba(255,230,109,0.12),rgba(255,230,109,0.12) 6px,rgba(0,0,0,0) 6px,rgba(0,0,0,0) 12px)',
border:'1px solid rgba(255,230,109,0.3)',borderRadius:8,
padding:'4px 10px',flexShrink:0,
}}>
<span style={{fontSize:14}}>🚧</span>
<span style={{color:'#ffe66d',fontFamily:'monospace',fontSize:10,fontWeight:700,letterSpacing:0.5}}>IM AUFBAU</span>
</div>
</div>
<div style={{display:'flex',gap:8,alignItems:'center'}}>
<label style={{display:'flex',alignItems:'center',gap:5,cursor:'pointer'}}>
<input type="checkbox" checked={snapGrid} onChange={e=>setSnapGrid(e.target.checked)} style={{accentColor:'#4ecdc4'}}/>
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>Raster</span>
</label>
<button onClick={()=>{setShapes([]);setSelected(null);}}
style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'5px 10px'}}> Leeren</button>
</div>
</div>
{/* Toolbar */}
<div style={{display:'flex',gap:5,marginBottom:10,flexWrap:'wrap'}}>
{TOOLS.map(t=>(
<button key={t.id} onClick={()=>setTool(t.id)} title={t.title} style={{
padding:'7px 13px',borderRadius:8,fontFamily:'monospace',fontSize:t.id==='text'?14:16,
cursor:'pointer',fontWeight:700,
background:tool===t.id?'#4ecdc4':'rgba(255,255,255,0.06)',
color: tool===t.id?'#0d0d0f':'rgba(255,255,255,0.6)',
border: tool===t.id?'none':'1px solid rgba(255,255,255,0.1)',
}}>{t.label}</button>
))}
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:9,alignSelf:'center',marginLeft:4}}>
{TOOLS.find(t=>t.id===tool)?.title}
</span>
</div>
{/* Canvas + Panel */}
<div style={{display:'flex',gap:12,flexDirection:mobile?'column':'row'}}>
<svg ref={svgRef} style={{flex:1,minWidth:0,background:'#0a0a0c',border:'1px solid rgba(255,255,255,0.08)',borderRadius:10,display:'block',cursor:tool==='select'?'default':tool==='text'?'text':'crosshair'}}
height={canvasH}
onMouseDown={onSVGDown} onMouseMove={onSVGMove} onMouseUp={onSVGUp}
onTouchStart={onSVGDown} onTouchMove={onSVGMove} onTouchEnd={onSVGUp}>
<defs>
<pattern id="g10" width={GRID} height={GRID} patternUnits="userSpaceOnUse">
<path d={`M ${GRID} 0 L 0 0 0 ${GRID}`} fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth={0.5}/>
</pattern>
<pattern id="g50" width={50} height={50} patternUnits="userSpaceOnUse">
<rect width={50} height={50} fill="url(#g10)"/>
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth={0.5}/>
</pattern>
</defs>
{snapGrid&&<rect width="100%" height="100%" fill="url(#g50)"/>}
{shapes.map(s=><Shape key={s.id} s={s} selected={selected} onClick={id=>{if(tool==='select')setSelected(id);}}/>)}
{preview}
</svg>
<div style={{width:mobile?'100%':210,flexShrink:0}}>
<div style={{...S.card}}>
<PropsPanel shape={selShape} onChange={updateShape} onDelete={deleteSelected}
vars={vars} scale={scale} onScaleChange={setScale}/>
</div>
</div>
</div>
{/* Rechner */}
<div style={{...S.card,marginTop:12}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<div style={{...S.head,marginBottom:0}}>RECHNER</div>
{results.length>0&&<button onClick={()=>setResults([])} style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'3px 8px'}}> Verlauf</button>}
</div>
{/* Alle Variablen */}
{Object.keys(vars).length>0&&(
<div style={{display:'flex',flexWrap:'wrap',gap:'3px 14px',marginBottom:10,padding:'7px 10px',
background:'rgba(0,0,0,0.3)',borderRadius:7,border:'1px solid rgba(255,255,255,0.05)'}}>
{Object.entries(vars).map(([k,v])=>(
<span key={k} style={{cursor:'pointer'}} onClick={()=>{setFormula(f=>f+k);formulaRef.current?.focus();}}>
<code style={{color:'#4ecdc4',fontSize:10}}>{k}</code>
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:10}}>={typeof v==='number'?v.toFixed(2):v}</span>
</span>
))}
</div>
)}
{/* Input */}
<div style={{display:'flex',gap:8,marginBottom:8}}>
<input ref={formulaRef} value={formula} onChange={e=>setFormula(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&calcFormula()}
placeholder="z.B. Kasten_l / 2"
autoCapitalize="none"
style={{...S.inp,flex:1,fontFamily:"'Courier New',monospace",fontSize:13}}/>
<button onClick={calcFormula} style={{...S.btn('#4ecdc4'),padding:'0 16px',flexShrink:0}}>= Berechnen</button>
</div>
{/* Operator-Buttons */}
<div style={{display:'grid',gridTemplateColumns:'repeat(5,1fr)',gap:5,marginBottom:10}}>
{['(',')',' × ',' ÷ ',' '].map((b,i)=>(
b.trim()===''?<div key={i}/>:
<button key={i} onClick={()=>insertCalc(b.trim())} style={{
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:7,color:'rgba(255,255,255,0.7)',cursor:'pointer',
fontFamily:'monospace',fontSize:14,padding:'6px 0',fontWeight:700,
}}>{b.trim()==='×'?'×':b.trim()==='÷'?'÷':b.trim()}</button>
))}
</div>
{/* Ergebnisse */}
{results.length===0&&Object.keys(vars).length===0&&(
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11,textAlign:'center',padding:'8px 0'}}>
Formen zeichnen benennen hier rechnen
</div>
)}
{results.map(r=>(
<div key={r.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
padding:'7px 10px',marginBottom:5,borderRadius:7,
background:r.result?'rgba(78,205,196,0.06)':'rgba(255,107,157,0.06)',
border:`1px solid ${r.result?'rgba(78,205,196,0.14)':'rgba(255,107,157,0.14)'}`}}>
<code style={{color:'rgba(255,255,255,0.45)',fontSize:11,fontFamily:"'Courier New',monospace",
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:'60%'}}>
{r.expr}
</code>
<div style={{display:'flex',alignItems:'center',gap:8}}>
<code style={{color:r.result?'#4ecdc4':'#ff6b9d',fontSize:14,fontFamily:"'Courier New',monospace",fontWeight:700}}>
{r.result||'❌ Fehler'}
</code>
<button onClick={()=>setResults(p=>p.filter(x=>x.id!==r.id))}
style={{background:'transparent',border:'none',cursor:'pointer',
color:'rgba(255,255,255,0.5)',fontSize:12,padding:'0 2px',lineHeight:1}}></button>
</div>
</div>
))}
</div>
</div>
);
}