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

@@ -1584,6 +1584,8 @@ const SEARCH_TOOL_INDEX = [
{type:'tool',label:'Linkliste',icon:'🔗',sub:'Werkzeuge',id:'linkliste',keywords:['link','url','lesezeichen','bookmark','webseite']}, {type:'tool',label:'Linkliste',icon:'🔗',sub:'Werkzeuge',id:'linkliste',keywords:['link','url','lesezeichen','bookmark','webseite']},
{type:'tool',label:'Code-Schnipsel',icon:'📄',sub:'Werkzeuge',id:'codeschnipsel',keywords:['code','snippet','skript','programmierung','source']}, {type:'tool',label:'Code-Schnipsel',icon:'📄',sub:'Werkzeuge',id:'codeschnipsel',keywords:['code','snippet','skript','programmierung','source']},
{type:'tool',label:'CAD-Skizzen',icon:'📐',sub:'Werkzeuge',id:'skizze',keywords:['cad','skizze','zeichnung','sketch']}, {type:'tool',label:'CAD-Skizzen',icon:'📐',sub:'Werkzeuge',id:'skizze',keywords:['cad','skizze','zeichnung','sketch']},
{type:'tool',label:'Kanban',icon:'🗂',sub:'Werkzeuge',id:'kanban',keywords:['kanban','board','aufgaben','task','todo','karte','spalte','planung']},
{type:'tool',label:'Whiteboard',icon:'🖊',sub:'Werkzeuge',id:'whiteboard',keywords:['whiteboard','zeichnen','canvas','board','draw','skizze','malen','zeichen']},
{type:'tool',label:'Dev-Tools',icon:'🔧',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']}, {type:'tool',label:'Dev-Tools',icon:'🔧',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']},
{type:'tool',label:'Paywall-Killer',icon:'🔓',sub:'Werkzeuge',id:'paywallkiller',keywords:['paywall','archiv','archive','artikel','zeitung','bild','waz','heise','spiegel','zeit','bypass','umgehen','lesen','gesperrt','bezahlschranke']}, {type:'tool',label:'Paywall-Killer',icon:'🔓',sub:'Werkzeuge',id:'paywallkiller',keywords:['paywall','archiv','archive','artikel','zeitung','bild','waz','heise','spiegel','zeit','bypass','umgehen','lesen','gesperrt','bezahlschranke']},
{type:'tool',label:'Media',icon:'🎬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnächst','favoriten','cinema']}, {type:'tool',label:'Media',icon:'🎬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnächst','favoriten','cinema']},

View File

@@ -10,6 +10,8 @@ const TOOL_ENTRIES = [
{ url:'tool://bestellungen', label:'📦 Bestellungen', group:'3D-Druck' }, { url:'tool://bestellungen', label:'📦 Bestellungen', group:'3D-Druck' },
{ url:'tool://kalkulator3d', label:'🧮 Kostenrechner', group:'3D-Druck' }, { url:'tool://kalkulator3d', label:'🧮 Kostenrechner', group:'3D-Druck' },
{ url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' }, { url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' },
{ url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' },
{ url:'tool://whiteboard', label:'🖊 Whiteboard', group:'Werkzeuge' },
{ url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' }, { url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' },
{ url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' }, { url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' },
{ url:'tool://linkliste', label:'🔗 Linkliste', group:'Werkzeuge' }, { url:'tool://linkliste', label:'🔗 Linkliste', group:'Werkzeuge' },

View File

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