Whiteboard: PNG-Export Fix, Text-Input Mobile, Suche + Linkliste ergänzt; Kanban in Suche ergänzt

This commit is contained in:
2026-06-25 21:42:30 +02:00
parent ad7aad0b2c
commit b940823d91
3 changed files with 55 additions and 20 deletions

View File

@@ -178,7 +178,7 @@ function renderElements(ctx, elements, vp, selectedId, forExport) {
// Offscreen-Export (ohne Auswahl-Rahmen, original Viewport)
function exportToPng(elements, title) {
// Bounding-Box aller Elemente berechnen
if (!elements?.length) return; // Nichts zu exportieren
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
for (const el of elements) {
const b = getBounds(el); if (!b) continue;
@@ -194,7 +194,9 @@ function exportToPng(elements, title) {
const a = document.createElement('a');
a.href = offscreen.toDataURL('image/png');
a.download = (title||'whiteboard').replace(/[^\w\s-]/g,'').trim()+'.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// ── Share-Modal ───────────────────────────────────────────────────────────────
@@ -681,8 +683,7 @@ export default function Whiteboard({ toast, mobile }) {
</div>
</div>
<div style={{display:'flex',gap:4,flexShrink:0}}>
<button onClick={e=>{e.stopPropagation(); exportToPng([], b.title);
// Daten erst laden dann exportieren
<button onClick={e=>{e.stopPropagation();
api(`/tools/whiteboard/${b.id}/data`).then(d=>{
const els=JSON.parse(typeof d.elements==='string'?d.elements:JSON.stringify(d.elements||[]));
exportToPng(els,b.title);
@@ -715,24 +716,54 @@ export default function Whiteboard({ toast, mobile }) {
)}
{textInput&&(
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100}}>
<input autoFocus
onKeyDown={e=>{
if(e.key==='Enter'){
const v=e.target.value;
setTextInput(null);
mobile ? (
// Mobile: festes Panel unten, kein Keyboard-Verschiebe-Problem
<div style={{position:'fixed',bottom:0,left:0,right:0,zIndex:500,
background:'#1a1a1e',borderTop:'1px solid rgba(255,255,255,0.15)',
padding:'12px 16px',paddingBottom:'calc(12px + env(safe-area-inset-bottom,0px))'}}>
<div style={{fontSize:10,fontFamily:'monospace',color:'rgba(255,255,255,0.35)',marginBottom:6}}>
TEXT EINGEBEN · Enter = bestätigen · Esc = abbrechen
</div>
<div style={{display:'flex',gap:8}}>
<input autoFocus
onKeyDown={e=>{
if(e.key==='Enter'){
const v=e.target.value; setTextInput(null);
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
}
if(e.key==='Escape') setTextInput(null);
}}
placeholder="Text eingeben…"
style={{...S.inp,flex:1,fontSize:14,color}}/>
<button onMouseDown={e=>{
e.preventDefault(); // Verhindert blur auf Input
const inp=e.currentTarget.previousSibling;
const v=inp?.value||''; setTextInput(null);
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
}
if(e.key==='Escape') setTextInput(null);
}}
onBlur={e=>{
const v=e.target.value;
setTextInput(null);
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
}}
style={{background:'transparent',border:'none',outline:'1px dashed rgba(255,255,255,0.4)',
color,fontSize:FONT_SIZES[fontSize],fontFamily:'monospace',minWidth:100}}/>
</div>
}} style={S.btn('#4ECDC4',true)}></button>
<button onMouseDown={e=>{e.preventDefault();setTextInput(null);}} style={S.btn('#888888',true)}></button>
</div>
</div>
) : (
// Desktop: direkt am Klick-Punkt, Canvas-relative Position
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100,pointerEvents:'auto'}}>
<input autoFocus
onKeyDown={e=>{
if(e.key==='Enter'){
const v=e.target.value; setTextInput(null);
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
}
if(e.key==='Escape') setTextInput(null);
}}
onBlur={e=>{
const v=e.target.value; setTextInput(null);
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
}}
style={{background:'rgba(0,0,0,0.7)',border:'none',
outline:`1px dashed ${color}`,borderRadius:4,padding:'2px 6px',
color,fontSize:FONT_SIZES[fontSize],fontFamily:'monospace',minWidth:120}}/>
</div>
)
)}
{/* ── Topbar ───────────────────────────────────────────────────────── */}