onDragOverBlock?.(e, index)}
onDrop={() => onDropBlock?.(index)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button, a, input, select')) return;
if (!activeFocus && canFocus && textareaRef.current) {
textareaRef.current.focus();
const len = textareaRef.current.value.length;
textareaRef.current.setSelectionRange(len, len);
}
}}
>
{/* Bullet Controls: Collapse Toggle & Bullet (Drag-Handle & Zoom) / Checkbox */}
{hasChildren ? (
) : (
)}
{/* Bullet handle for dragging and zoom focus */}
{/* If To-Do block: checkbox alongside bullet */}
{block.type === 'todo' && (
)}
{/* Main Block Content */}
{previewUrl && (
{
const content = hidePreviewContent(localContent, previewUrl, title);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
)}
{mediaPicker === 'image' ? (
{
setMediaPicker(null);
const content = mediaContent('image', media.id);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content, { type: 'image' });
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : mediaPicker === 'dictation' ? (
{
setMediaPicker(null);
setLocalContent(text);
localContentRef.current = text;
flushDebounce(text);
requestAnimationFrame(() => {
const ta = textareaRef.current;
if (!ta) return;
ta.focus();
ta.setSelectionRange(ta.value.length, ta.value.length);
});
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : mediaPicker === 'audio' ? (
{
setMediaPicker(null);
const content = mediaContent('audio', media.id);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content, { type: 'audio' });
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : block.type === 'audio' ? (
{
const ref = parseMediaContent(localContent);
if (!ref) return;
const content = mediaContent('audio', ref.id, caption);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
) : block.type === 'image' ? (
{
const ref = parseMediaContent(localContent);
if (!ref) return;
const content = mediaContent('image', ref.id, caption);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
) : block.type === 'divider' ? (
) : block.type === 'code' ? (
{
setLocalContent(newContent);
localContentRef.current = newContent;
flushDebounce(newContent);
}}
onChangeLang={(lang) => flushDebounce(localContent, { lang })}
onConvertToText={() => flushDebounce(localContent, { type: 'bullet' })}
handleFocus={handleFocus}
handleBlur={handleBlur}
handleContentChange={handleContentChange}
handleKeyDown={handleKeyDown}
/>
) : block.type === 'callout' ? (
) : (
{/* View Layer with Inline Markdown & Actions (3-dots or Erweitert Badge) directly behind text */}
{
if (!activeFocus && canFocus && textareaRef.current) {
// Keep the row's click handler from moving the caret back to the end
e.stopPropagation();
focusAtPoint(e.clientX, e.clientY);
}
}}
className={`w-full min-h-[1.5rem] leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
activeFocus ? 'absolute inset-0 pointer-events-none' : 'relative cursor-text'
} ${
block.type === 'h1'
? 'text-lg font-bold text-main tracking-tight'
: block.type === 'h2'
? 'text-base font-semibold text-main'
: block.type === 'h3'
? 'text-sm font-semibold text-main'
: block.type === 'todo' && block.checked
? 'text-sm text-faint line-through'
: 'text-sm text-main'
}`}
>
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
{localContent ? renderInlineMarkdown(localContent, { onTagClick }) : '\u200b'}
{/* Inline Badge and 3-Dots directly behind the text */}
{block.type === 'todo' && (
{block.taskId && (
)}
{canEdit && (
{showTodoMenu && (
e.stopPropagation()}
>
{!block.taskId ? (
) : (
)}
)}
)}
)}
)}
{/* Conflict copy: text that lost against someone else's change */}
{block.conflictOf && (
{block.conflictOf === 'title' ? 'Konflikt beim Titel' : 'Konflikt'}
{block.conflictAuthor ? ` – Version von ${block.conflictAuthor}` : ''}
{onResolveConflict && (
<>
>
)}
)}
{/* Rich Tokens Bar (Wiki-Links & #Tags) if present */}
{hasSpecialTokens && !isFocused && (
{tokens.map((part, idx) => {
if (part.startsWith('[[') && part.endsWith(']]')) {
const page = part.slice(2, -2).trim();
return (
);
}
const mdMatch = part.match(/^\[(.*?)\]\((https?:\/\/.*?)\)$/);
if (mdMatch && safeHref(mdMatch[2])) {
const label = mdMatch[1];
const url = safeHref(mdMatch[2]) as string;
return (
{label || url}
);
}
// The preview card above already shows this address
if (/^https?:\/\//.test(part) && safeHref(part) && part !== previewUrl) {
return (
{part}
);
}
return null;
})}
)}
{/* Floating Slash Commands Menu */}
{slashSearch !== null && canEdit && (
Befehl auswählen
{filteredSlashCommands.map((cmd, i) => (
))}
{filteredSlashCommands.length === 0 && (
Kein Befehl gefunden
)}
)}
{/* Branch into its own note (desktop; phones use /extract) */}
{canEdit && onExtractBlock && localContent.trim() !== '' && !mediaPicker && (
)}
{/* Zoom / Focus button on hover */}
{/* Someone else is editing this block */}
{lockedBy && (
{lockedBy}
)}
{/* Delete button on hover (Trash2 icon); on phones only in the row being edited */}
{canEdit && (
)}
{/* Link Insertion Modal */}
{showLinkModal && (
setShowLinkModal(false)}
onInsertLink={handleInsertLink}
/>
)}
);
};