Dev #3

Merged
sandrodach merged 30 commits from dev into main 2026-10-25 18:00:10 +01:00
Showing only changes of commit eafe75faf0 - Show all commits
+311 -91
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useMemo, useRef } from 'react'; import React, { useEffect, useLayoutEffect, useState, useMemo, useRef } from 'react';
import { import {
FilePlus, FilePlus,
FolderPlus, FolderPlus,
@@ -154,8 +154,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [linkWebTitle, setLinkWebTitle] = useState(''); const [linkWebTitle, setLinkWebTitle] = useState('');
const [linkWebUrl, setLinkWebUrl] = useState(''); const [linkWebUrl, setLinkWebUrl] = useState('');
const [activeEditingBlockId, setActiveEditingBlockId] = useState<string | null>(null); const [activeEditingBlockId, setActiveEditingBlockId] = useState<string | null>(null);
const [activeCursorOffset, setActiveCursorOffset] = useState<number>(0);
const activeCursorOffsetRef = useRef<number>(0);
const [editingLinkOriginalRaw, setEditingLinkOriginalRaw] = useState<string | null>(null); const [editingLinkOriginalRaw, setEditingLinkOriginalRaw] = useState<string | null>(null);
const setBlockCursor = (blockId: string | null, offset: number) => {
activeCursorOffsetRef.current = offset;
setActiveEditingBlockId(blockId);
setActiveCursorOffset(offset);
};
// Drag and Drop reordering state for blocks // Drag and Drop reordering state for blocks
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null); const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
@@ -1045,6 +1053,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
nextBlocks.splice(afterIndex + 1, 0, newBlock); nextBlocks.splice(afterIndex + 1, 0, newBlock);
persistDocument({ ...selectedDoc, blocks: nextBlocks }); persistDocument({ ...selectedDoc, blocks: nextBlocks });
setLastFocusedBlockId(newBlock.id); setLastFocusedBlockId(newBlock.id);
setBlockCursor(newBlock.id, 0);
// Focus into the new element next tick // Focus into the new element next tick
setTimeout(() => { setTimeout(() => {
@@ -1134,6 +1143,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (prevBlock) { if (prevBlock) {
setLastFocusedBlockId(prevBlock.id); setLastFocusedBlockId(prevBlock.id);
setBlockCursor(prevBlock.id, (prevBlock.content || '').length);
setTimeout(() => { setTimeout(() => {
const el = blockInputRefs.current[prevBlock.id]; const el = blockInputRefs.current[prevBlock.id];
if (el) { if (el) {
@@ -1172,11 +1182,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
}; };
useLayoutEffect(() => {
if (!activeEditingBlockId) return;
const el = blockInputRefs.current[activeEditingBlockId];
if (!el) return;
const segStartAttr = el.getAttribute('data-seg-start');
if (segStartAttr !== null && 'setSelectionRange' in el && typeof el.value === 'string') {
const segStart = parseInt(segStartAttr, 10) || 0;
const desiredOffset = activeCursorOffsetRef.current;
const localPos = Math.max(0, Math.min(el.value.length, desiredOffset - segStart));
if (document.activeElement !== el) {
el.focus();
}
try {
if (el.selectionStart !== localPos || el.selectionEnd !== localPos) {
el.setSelectionRange(localPos, localPos);
}
} catch {
// Ignore selection range errors on unsupported inputs
}
}
}, [activeEditingBlockId, activeCursorOffset, selectedDoc]);
// Keyboard Navigation: Enter, Tab, Shift+Tab, Backspace, / Slash Commands // Keyboard Navigation: Enter, Tab, Shift+Tab, Backspace, / Slash Commands
const handleKeyDown = ( const handleKeyDown = (
e: React.KeyboardEvent, e: React.KeyboardEvent,
block: DocumentBlock, block: DocumentBlock,
index: number index: number,
segStart = 0
) => { ) => {
if (isReadOnly) return; if (isReadOnly) return;
ensureLock(); ensureLock();
@@ -1220,7 +1253,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const target = e.target as HTMLTextAreaElement | HTMLInputElement; const target = e.target as HTMLTextAreaElement | HTMLInputElement;
let afterContent = ''; let afterContent = '';
if (target && typeof target.selectionStart === 'number') { if (target && typeof target.selectionStart === 'number') {
const cursor = target.selectionStart; const cursor = segStart + target.selectionStart;
const fullContent = block.content || ''; const fullContent = block.content || '';
const before = fullContent.slice(0, cursor); const before = fullContent.slice(0, cursor);
const after = fullContent.slice(cursor); const after = fullContent.slice(cursor);
@@ -1238,7 +1271,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} else { } else {
indentBlock(index); indentBlock(index);
} }
} else if (e.key === 'Backspace' && (!block.content || block.content === '')) { } else if (e.key === 'Backspace') {
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
if (!block.content || block.content === '') {
if ((block.level || 0) > 0) { if ((block.level || 0) > 0) {
e.preventDefault(); e.preventDefault();
outdentBlock(block.id); outdentBlock(block.id);
@@ -1247,6 +1282,25 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSlashBlockId(null); setSlashBlockId(null);
deleteBlockAndFocusPrev(index); deleteBlockAndFocusPrev(index);
} }
} else if (
segStart > 0 &&
target &&
target.selectionStart === 0 &&
target.selectionEnd === 0
) {
e.preventDefault();
setBlockCursor(block.id, segStart);
}
} else if (
!e.shiftKey &&
(e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Home' || e.key === 'End')
) {
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
requestAnimationFrame(() => {
if (target && typeof target.selectionStart === 'number') {
setBlockCursor(block.id, segStart + target.selectionStart);
}
});
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
setActiveEditingBlockId(null); setActiveEditingBlockId(null);
(e.target as HTMLElement)?.blur(); (e.target as HTMLElement)?.blur();
@@ -1330,16 +1384,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
}, [slashSelectedIndex, slashBlockId]); }, [slashSelectedIndex, slashBlockId]);
const focusBlock = (blockId: string) => { const focusBlock = (blockId: string, customOffset?: number) => {
setLastFocusedBlockId(blockId); setLastFocusedBlockId(blockId);
setActiveEditingBlockId(blockId); const blk = selectedDoc?.blocks.find((b) => b.id === blockId);
const targetOffset = customOffset !== undefined ? customOffset : (blk?.content || '').length;
setBlockCursor(blockId, targetOffset);
const tryFocus = () => { const tryFocus = () => {
const el = blockInputRefs.current[blockId]; const el = blockInputRefs.current[blockId];
if (el) { if (el) {
el.focus(); el.focus();
if ('setSelectionRange' in el && typeof (el as any).value === 'string') { if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
const segStartAttr = el.getAttribute('data-seg-start');
const segStart = segStartAttr !== null ? parseInt(segStartAttr, 10) || 0 : 0;
const len = (el as any).value.length; const len = (el as any).value.length;
(el as any).setSelectionRange(len, len); const localPos = Math.max(0, Math.min(len, targetOffset - segStart));
(el as any).setSelectionRange(localPos, localPos);
} }
return true; return true;
} }
@@ -1424,25 +1484,31 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const handleInsertNoteLink = (docTitle: string) => { const handleInsertNoteLink = (docTitle: string) => {
if (!linkModalBlockId || !selectedDoc) return; if (!linkModalBlockId || !selectedDoc) return;
const block = selectedDoc.blocks.find((b) => b.id === linkModalBlockId); const targetBlockId = linkModalBlockId;
const block = selectedDoc.blocks.find((b) => b.id === targetBlockId);
let nextCursor = 0;
if (block) { if (block) {
let newContent: string; let newContent: string;
if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) { if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) {
newContent = block.content.replace(editingLinkOriginalRaw, `[[${docTitle}]]`); newContent = block.content.replace(editingLinkOriginalRaw, `[[${docTitle}]]`);
if (!newContent.endsWith(' ')) newContent += ' ';
} else { } else {
const space = block.content && !block.content.endsWith(' ') ? ' ' : ''; const base = block.content || '';
newContent = `${block.content}${space}[[${docTitle}]] `; const space = base.length > 0 && !base.endsWith(' ') ? ' ' : '';
newContent = `${base}${space}[[${docTitle}]] `;
} }
updateBlock(linkModalBlockId, { content: newContent }); nextCursor = newContent.length;
updateBlock(targetBlockId, { content: newContent });
} }
setLinkModalBlockId(null); setLinkModalBlockId(null);
setEditingLinkOriginalRaw(null); setEditingLinkOriginalRaw(null);
setActiveEditingBlockId(null); focusBlock(targetBlockId, nextCursor);
}; };
const handleInsertWebLink = () => { const handleInsertWebLink = () => {
if (!linkModalBlockId || !selectedDoc) return; if (!linkModalBlockId || !selectedDoc) return;
const block = selectedDoc.blocks.find((b) => b.id === linkModalBlockId); const targetBlockId = linkModalBlockId;
const block = selectedDoc.blocks.find((b) => b.id === targetBlockId);
let title = linkWebTitle.trim(); let title = linkWebTitle.trim();
let url = linkWebUrl.trim(); let url = linkWebUrl.trim();
if (!url) return; if (!url) return;
@@ -1456,21 +1522,25 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
title = url; title = url;
} }
} }
let nextCursor = 0;
if (block) { if (block) {
let newContent: string; let newContent: string;
if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) { if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) {
newContent = block.content.replace(editingLinkOriginalRaw, `[${title}](${url})`); newContent = block.content.replace(editingLinkOriginalRaw, `[${title}](${url})`);
if (!newContent.endsWith(' ')) newContent += ' ';
} else { } else {
const space = block.content && !block.content.endsWith(' ') ? ' ' : ''; const base = block.content || '';
newContent = `${block.content}${space}[${title}](${url}) `; const space = base.length > 0 && !base.endsWith(' ') ? ' ' : '';
newContent = `${base}${space}[${title}](${url}) `;
} }
updateBlock(linkModalBlockId, { content: newContent }); nextCursor = newContent.length;
updateBlock(targetBlockId, { content: newContent });
} }
setLinkModalBlockId(null); setLinkModalBlockId(null);
setEditingLinkOriginalRaw(null); setEditingLinkOriginalRaw(null);
setLinkWebTitle(''); setLinkWebTitle('');
setLinkWebUrl(''); setLinkWebUrl('');
setActiveEditingBlockId(null); focusBlock(targetBlockId, nextCursor);
}; };
const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => { const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => {
@@ -1629,19 +1699,28 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const hasRichTokens = (text: string): boolean => const findRichTokens = (text: string): { start: number; end: number; raw: string }[] => {
Boolean(text && (text.includes('[[') || text.includes('#') || (text.includes('[') && text.includes('](')))); if (!text) return [];
const regex = /(\[\[[^\]]+\]\]|\[[^\]]*\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g;
const tokens: { start: number; end: number; raw: string }[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
tokens.push({
start: match.index,
end: match.index + match[0].length,
raw: match[0],
});
}
return tokens;
};
// Helper to render text with clickable Wiki-Links, Web-Links and Hashtags const renderSingleRichToken = (part: string, blockId: string | undefined, key: React.Key) => {
const renderFormattedText = (text: string, blockId?: string) => {
const parts = text.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
return parts.map((part, i) => {
if (part.startsWith('[[') && part.endsWith(']]')) { if (part.startsWith('[[') && part.endsWith(']]')) {
const linkTitle = part.slice(2, -2).trim(); const linkTitle = part.slice(2, -2).trim();
const target = docs.find((d) => d.title.toLowerCase() === linkTitle.toLowerCase()); const target = docs.find((d) => d.title.toLowerCase() === linkTitle.toLowerCase());
return ( return (
<span <span
key={i} key={key}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 border text-xs font-medium rounded-btn transition-colors align-baseline ${ className={`inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 border text-xs font-medium rounded-btn transition-colors align-baseline ${
target target
? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30' ? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30'
@@ -1667,6 +1746,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{!isReadOnly && blockId && ( {!isReadOnly && blockId && (
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setEditingLinkOriginalRaw(part); setEditingLinkOriginalRaw(part);
@@ -1683,13 +1763,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</span> </span>
); );
} }
const webMatch = part.match(/^\[(.*?)\]\((https?:\/\/[^\s)]+)\)$/); const webMatch = part.match(/^\[(.*?)\]\((https?:\/\/[^\s)]+)\)$/);
if (webMatch) { if (webMatch) {
const title = webMatch[1] || webMatch[2]; const title = webMatch[1] || webMatch[2];
const url = webMatch[2]; const url = webMatch[2];
return ( return (
<span <span
key={i} key={key}
className="inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 bg-sky-500/10 border border-sky-500/30 text-sky-500 dark:text-sky-400 hover:bg-sky-500/20 text-xs font-medium rounded-btn transition-colors align-baseline" className="inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 bg-sky-500/10 border border-sky-500/30 text-sky-500 dark:text-sky-400 hover:bg-sky-500/20 text-xs font-medium rounded-btn transition-colors align-baseline"
> >
<a <a
@@ -1707,6 +1788,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{!isReadOnly && blockId && ( {!isReadOnly && blockId && (
<button <button
type="button" type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setEditingLinkOriginalRaw(part); setEditingLinkOriginalRaw(part);
@@ -1724,11 +1806,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</span> </span>
); );
} }
if (part.startsWith('#') && part.length > 1) { if (part.startsWith('#') && part.length > 1) {
const tagWord = part.slice(1); const tagWord = part.slice(1);
return ( return (
<span <span
key={i} key={key}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
openTagPage(tagWord); openTagPage(tagWord);
@@ -1741,8 +1824,205 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</span> </span>
); );
} }
return <span key={i} className="whitespace-pre-wrap">{part}</span>;
return <span key={key} className="whitespace-pre-wrap">{part}</span>;
};
// Helper to render text with clickable Wiki-Links, Web-Links and Hashtags
const renderFormattedText = (text: string, blockId?: string) => {
const parts = text.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
return parts.map((part, i) => renderSingleRichToken(part, blockId, i));
};
// Live inline token-aware editor: renders completed #hashtags and links inline as pills on the
// exact same line while keeping any token currently touched by the cursor unwrapped as raw text.
const renderLiveInlineBlock = (
block: DocumentBlock,
originalIndex: number,
extraTextClass = 'text-main'
) => {
const text = block.content || '';
if (isReadOnly) {
return (
<div className={`w-full text-sm leading-relaxed break-words min-h-[22px] cursor-default ${extraTextClass}`}>
{renderFormattedText(text, block.id)}
</div>
);
}
const allTokens = findRichTokens(text);
const isEditing = activeEditingBlockId === block.id;
if (!isEditing && allTokens.length > 0) {
const segments: (
| { kind: 'text'; start: number; end: number; text: string }
| { kind: 'token'; start: number; end: number; raw: string }
)[] = [];
let lastIdx = 0;
for (const t of allTokens) {
if (t.start > lastIdx) {
segments.push({ kind: 'text', start: lastIdx, end: t.start, text: text.slice(lastIdx, t.start) });
}
segments.push({ kind: 'token', start: t.start, end: t.end, raw: t.raw });
lastIdx = t.end;
}
if (lastIdx < text.length) {
segments.push({ kind: 'text', start: lastIdx, end: text.length, text: text.slice(lastIdx) });
}
return (
<div
onClick={() => focusBlock(block.id, text.length)}
className={`flex flex-wrap items-center gap-y-0.5 w-full text-sm leading-relaxed break-words min-h-[22px] cursor-text ${extraTextClass}`}
>
{segments.map((seg, i) =>
seg.kind === 'token' ? (
renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${i}`)
) : (
<span
key={`txt-${seg.start}-${i}`}
onClick={(e) => {
e.stopPropagation();
const sel = window.getSelection();
const localOffset =
sel && sel.anchorNode && e.currentTarget.contains(sel.anchorNode)
? sel.anchorOffset
: seg.text.length;
focusBlock(block.id, seg.start + localOffset);
}}
className="whitespace-pre-wrap"
>
{seg.text}
</span>
)
)}
</div>
);
}
const cursor = isEditing ? Math.max(0, Math.min(text.length, activeCursorOffset)) : text.length;
const renderedTokens = isEditing
? allTokens.filter((t) => !(cursor >= t.start && cursor <= t.end))
: [];
type InlineSegment =
| { kind: 'text'; start: number; end: number; text: string }
| { kind: 'token'; start: number; end: number; raw: string };
const segments: InlineSegment[] = [];
let lastIdx = 0;
for (const t of renderedTokens) {
segments.push({
kind: 'text',
start: lastIdx,
end: t.start,
text: text.slice(lastIdx, t.start),
}); });
segments.push({
kind: 'token',
start: t.start,
end: t.end,
raw: t.raw,
});
lastIdx = t.end;
}
segments.push({
kind: 'text',
start: lastIdx,
end: text.length,
text: text.slice(lastIdx),
});
let activeSegIdx = segments.findIndex(
(s) => s.kind === 'text' && cursor >= s.start && cursor <= s.end
);
if (activeSegIdx === -1) {
activeSegIdx = segments.length - 1;
}
return (
<div
onMouseDown={(e) => {
if (e.target === e.currentTarget) {
e.preventDefault();
focusBlock(block.id, text.length);
}
}}
className={`flex flex-wrap items-center gap-y-0.5 w-full min-h-[22px] cursor-text ${extraTextClass}`}
>
{segments.map((seg, idx) => {
if (seg.kind === 'token') {
return renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${idx}`);
}
if (idx === activeSegIdx) {
const isLastSeg = idx === segments.length - 1;
return (
<input
key="live-input"
ref={(el) => (blockInputRefs.current[block.id] = el)}
data-seg-start={seg.start}
type="text"
readOnly={isReadOnly}
value={seg.text}
style={
!isLastSeg
? { width: `${Math.max(1, seg.text.length + 0.5)}ch` }
: undefined
}
onFocus={(e) => {
const localPos =
typeof e.currentTarget.selectionStart === 'number'
? e.currentTarget.selectionStart
: seg.text.length;
setBlockCursor(block.id, seg.start + localPos);
handleBlockFocus();
}}
onClick={(e) => {
if (
e.currentTarget.selectionStart === e.currentTarget.selectionEnd &&
typeof e.currentTarget.selectionStart === 'number'
) {
setBlockCursor(block.id, seg.start + e.currentTarget.selectionStart);
}
}}
onBlur={() => {
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
handleBlockBlur();
}}
onChange={(e) => {
const newSegVal = e.target.value;
const localCursor =
typeof e.target.selectionStart === 'number'
? e.target.selectionStart
: newSegVal.length;
const newFull = text.slice(0, seg.start) + newSegVal + text.slice(seg.end);
const newGlobalCursor = seg.start + localCursor;
setBlockCursor(block.id, newGlobalCursor);
handleContentChange(block, newFull);
}}
onKeyDown={(e) => handleKeyDown(e, block, originalIndex, seg.start)}
className={`${
isLastSeg ? 'flex-1 min-w-[60px]' : 'flex-shrink-0'
} bg-transparent text-sm leading-relaxed focus:outline-none placeholder:text-faint ${extraTextClass}`}
/>
);
}
if (!seg.text) return null;
return (
<span
key={`txt-${seg.start}-${idx}`}
onMouseDown={(e) => {
e.preventDefault();
focusBlock(block.id, seg.end);
}}
className="whitespace-pre-wrap text-sm leading-relaxed cursor-text"
>
{seg.text}
</span>
);
})}
</div>
);
}; };
// --- GLOBAL TO-DO ROLLUP AGGREGATOR --- // --- GLOBAL TO-DO ROLLUP AGGREGATOR ---
@@ -2815,39 +3095,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
}`} }`}
/> />
{(!hasRichTokens(block.content) || activeEditingBlockId === block.id) && !isReadOnly ? ( {renderLiveInlineBlock(
<input block,
ref={(el) => (blockInputRefs.current[block.id] = el)} originalIndex,
type="text"
readOnly={isReadOnly}
value={block.content}
onFocus={() => {
setActiveEditingBlockId(block.id);
handleBlockFocus();
}}
onBlur={() => {
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
handleBlockBlur();
}}
onChange={(e) => handleContentChange(block, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
className={`w-full bg-transparent text-sm focus:outline-none ${
block.checked ? 'line-through text-faint' : 'text-main' block.checked ? 'line-through text-faint' : 'text-main'
}`}
/>
) : (
<div
onClick={() => {
if (!isReadOnly) {
focusBlock(block.id);
}
}}
className={`w-full text-sm leading-relaxed break-words min-h-[20px] ${
block.checked ? 'line-through text-faint' : 'text-main'
} ${isReadOnly ? 'cursor-default' : 'cursor-text'}`}
>
{renderFormattedText(block.content, block.id)}
</div>
)} )}
{!isReadOnly && ( {!isReadOnly && (
<button <button
@@ -3073,38 +3324,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* TYPE: DEFAULT BULLET / PARAGRAPH */} {/* TYPE: DEFAULT BULLET / PARAGRAPH */}
{(block.type === 'bullet' || block.type === 'paragraph') && ( {(block.type === 'bullet' || block.type === 'paragraph') && (
<div className="space-y-0.5"> <div className="space-y-0.5">
{(!hasRichTokens(block.content) || activeEditingBlockId === block.id) && !isReadOnly ? ( {renderLiveInlineBlock(block, originalIndex, 'text-main')}
<textarea
ref={(el) => (blockInputRefs.current[block.id] = el)}
readOnly={isReadOnly}
rows={Math.max(1, Math.ceil((block.content || '').length / 80))}
value={block.content}
onFocus={() => {
setActiveEditingBlockId(block.id);
handleBlockFocus();
}}
onBlur={() => {
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
handleBlockBlur();
}}
onChange={(e) => handleContentChange(block, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
className="w-full bg-transparent text-sm text-main leading-relaxed focus:outline-none resize-none placeholder:text-faint"
/>
) : (
<div
onClick={() => {
if (!isReadOnly) {
focusBlock(block.id);
}
}}
className={`w-full text-sm text-main leading-relaxed break-words min-h-[22px] ${
isReadOnly ? 'cursor-default' : 'cursor-text'
}`}
>
{renderFormattedText(block.content, block.id)}
</div>
)}
</div> </div>
)} )}