Whiteboard: Text Live-Vorschau beim Tippen auf Canvas; Linkliste: doppelter Kanban-Eintrag entfernt

This commit is contained in:
2026-06-25 21:54:24 +02:00
parent b940823d91
commit d53d0f6b77
2 changed files with 39 additions and 12 deletions

View File

@@ -10,7 +10,6 @@ const TOOL_ENTRIES = [
{ url:'tool://bestellungen', label:'📦 Bestellungen', 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://whiteboard', label:'🖊 Whiteboard', group:'Werkzeuge' },
{ url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' },
{ url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' },

View File

@@ -283,6 +283,7 @@ export default function Whiteboard({ toast, mobile }) {
const [saving, setSaving] = useState(false);
const [shareModal,setShareModal]= useState(false);
const [textInput, setTextInput] = useState(null);
const [textPreview, setTextPreview] = useState(null); // Live-Vorschau während Tippen
const [selectedId,setSelectedId]= useState(null);
// Kollaboration
@@ -303,6 +304,7 @@ export default function Whiteboard({ toast, mobile }) {
const vpRef = useRef({x:0,y:0,zoom:1});
const elementsRef = useRef([]);
const selectedRef = useRef(null);
const textPreviewRef = useRef(null);
// ── Hilfsfunktionen ──────────────────────────────────────────────────────
const syncVp = (vp) => { vpRef.current=vp; setViewport({...vp}); };
@@ -463,7 +465,25 @@ export default function Whiteboard({ toast, mobile }) {
ctx.save(); ctx.fillStyle=c.color; ctx.font='10px monospace';
ctx.fillText('▶ '+c.username, sx+6, sy-4); ctx.restore();
}
}, [collab]);
// Text-Vorschau während Tippen
if (textPreviewRef.current) {
const { text, x, y, fontSize, color } = textPreviewRef.current;
const vp = vpRef.current;
const sx = x * vp.zoom + vp.x;
const sy = y * vp.zoom + vp.y;
ctx.save();
ctx.font = `${fontSize * vp.zoom}px monospace`;
ctx.fillStyle = color;
ctx.globalAlpha = 0.85;
ctx.fillText(text || '|', sx, sy);
// Cursor-Blinken simulieren: kleiner Strich hinter dem Text
if (text) {
const tw = ctx.measureText(text).width;
ctx.fillRect(sx + tw + 1, sy - fontSize * vp.zoom * 0.8, 2, fontSize * vp.zoom);
}
ctx.restore();
}
}, [collab, textPreview]);
useEffect(()=>{ render(); }, [elements, viewport, selectedId, collab, render]);
@@ -507,6 +527,8 @@ export default function Whiteboard({ toast, mobile }) {
// Text
if (tool==='text') {
textPreviewRef.current = { text:'', x, y, fontSize:FONT_SIZES[fontSize], color };
render();
setTextInput({ sx, sy, x, y }); return;
}
@@ -717,7 +739,6 @@ export default function Whiteboard({ toast, mobile }) {
{textInput&&(
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))'}}>
@@ -726,37 +747,44 @@ export default function Whiteboard({ toast, mobile }) {
</div>
<div style={{display:'flex',gap:8}}>
<input autoFocus
onChange={e=>{
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
render();
}}
onKeyDown={e=>{
if(e.key==='Enter'){
const v=e.target.value; setTextInput(null);
const v=e.target.value; textPreviewRef.current=null; 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);
if(e.key==='Escape'){ textPreviewRef.current=null; setTextInput(null); render(); }
}}
placeholder="Text eingeben…"
style={{...S.inp,flex:1,fontSize:14,color}}/>
<button onMouseDown={e=>{
e.preventDefault(); // Verhindert blur auf Input
e.preventDefault();
const inp=e.currentTarget.previousSibling;
const v=inp?.value||''; setTextInput(null);
const v=inp?.value||''; textPreviewRef.current=null; setTextInput(null);
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>
<button onMouseDown={e=>{e.preventDefault();setTextInput(null);}} style={S.btn('#888888',true)}></button>
<button onMouseDown={e=>{e.preventDefault();textPreviewRef.current=null;setTextInput(null);render();}} 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
onChange={e=>{
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
render();
}}
onKeyDown={e=>{
if(e.key==='Enter'){
const v=e.target.value; setTextInput(null);
const v=e.target.value; textPreviewRef.current=null; 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);
if(e.key==='Escape'){ textPreviewRef.current=null; setTextInput(null); render(); }
}}
onBlur={e=>{
const v=e.target.value; setTextInput(null);
const v=e.target.value; textPreviewRef.current=null; 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',