fix(workspace): support multi-line bullets via Shift+Enter and preserve lines on Enter
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s
This commit is contained in:
1 parent
b0403e09bc
commit
518a0903dd
2 files changed
+236
-51
No files matched your search
@@ -1081,7 +1081,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
afterIndex: number,
|
afterIndex: number,
|
||||||
level: number,
|
level: number,
|
||||||
type: BlockType = 'bullet',
|
type: BlockType = 'bullet',
|
||||||
initialContent: string = ''
|
initialContent: string = '',
|
||||||
|
updatedCurrentBlock?: { id: string; content: string }
|
||||||
) => {
|
) => {
|
||||||
if (isReadOnly || !selectedDoc) return;
|
if (isReadOnly || !selectedDoc) return;
|
||||||
const newBlock: DocumentBlock = {
|
const newBlock: DocumentBlock = {
|
||||||
@@ -1092,7 +1093,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
checked: false,
|
checked: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextBlocks = [...selectedDoc.blocks];
|
const nextBlocks = selectedDoc.blocks.map((b) =>
|
||||||
|
updatedCurrentBlock && b.id === updatedCurrentBlock.id
|
||||||
|
? { ...b, content: updatedCurrentBlock.content }
|
||||||
|
: b
|
||||||
|
);
|
||||||
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);
|
||||||
@@ -1247,7 +1252,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}, [activeEditingBlockId, activeCursorOffset, selectedDoc]);
|
}, [activeEditingBlockId, activeCursorOffset, selectedDoc]);
|
||||||
|
|
||||||
// Keyboard Navigation: Enter, Tab, Shift+Tab, Backspace, / Slash Commands
|
// Keyboard Navigation: Enter, Shift+Enter, Tab, Shift+Tab, Backspace, / Slash Commands
|
||||||
const handleKeyDown = (
|
const handleKeyDown = (
|
||||||
e: React.KeyboardEvent,
|
e: React.KeyboardEvent,
|
||||||
block: DocumentBlock,
|
block: DocumentBlock,
|
||||||
@@ -1290,22 +1295,57 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Enter' && e.shiftKey) {
|
||||||
|
// Shift+Enter: insert a newline within the current bullet point
|
||||||
|
if (block.type === 'bullet' || block.type === 'paragraph' || block.type === 'todo') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSlashBlockId(null);
|
||||||
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
const localPos =
|
||||||
|
target && typeof target.selectionStart === 'number'
|
||||||
|
? target.selectionStart
|
||||||
|
: (target?.value || '').length;
|
||||||
|
const cursor = segStart + localPos;
|
||||||
|
const fullContent = block.content || '';
|
||||||
|
const nextContent = fullContent.slice(0, cursor) + '\n' + fullContent.slice(cursor);
|
||||||
|
const nextCursor = cursor + 1;
|
||||||
|
setBlockCursor(block.id, nextCursor);
|
||||||
|
updateBlock(block.id, { content: nextContent });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSlashBlockId(null);
|
setSlashBlockId(null);
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
let afterContent = '';
|
|
||||||
if (target && typeof target.selectionStart === 'number') {
|
|
||||||
const cursor = segStart + target.selectionStart;
|
|
||||||
const fullContent = block.content || '';
|
const fullContent = block.content || '';
|
||||||
|
let afterContent = '';
|
||||||
|
let updatedCurrent: { id: string; content: string } | undefined = undefined;
|
||||||
|
|
||||||
|
if (!fullContent.includes('\n') && target && typeof target.selectionStart === 'number') {
|
||||||
|
const cursor = segStart + target.selectionStart;
|
||||||
const before = fullContent.slice(0, cursor);
|
const before = fullContent.slice(0, cursor);
|
||||||
const after = fullContent.slice(cursor);
|
const after = fullContent.slice(cursor);
|
||||||
if (after.length > 0) {
|
if (after.length > 0) {
|
||||||
updateBlock(block.id, { content: before });
|
updatedCurrent = { id: block.id, content: before };
|
||||||
afterContent = after;
|
afterContent = after;
|
||||||
}
|
}
|
||||||
|
} else if (fullContent.endsWith('\n')) {
|
||||||
|
// Clean up trailing empty line if user pressed Shift+Enter then immediately Enter
|
||||||
|
const trimmedTrailing = fullContent.replace(/\n+$/, '');
|
||||||
|
if (trimmedTrailing !== fullContent) {
|
||||||
|
updatedCurrent = { id: block.id, content: trimmedTrailing };
|
||||||
}
|
}
|
||||||
insertNewBulletAfter(index, block.level || 0, block.type === 'todo' ? 'todo' : 'bullet', afterContent);
|
}
|
||||||
|
|
||||||
|
insertNewBulletAfter(
|
||||||
|
index,
|
||||||
|
block.level || 0,
|
||||||
|
block.type === 'todo' ? 'todo' : 'bullet',
|
||||||
|
afterContent,
|
||||||
|
updatedCurrent
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1316,7 +1356,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
} else if (e.key === 'Backspace') {
|
} else if (e.key === 'Backspace') {
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
if (!block.content || block.content === '') {
|
const fullContent = block.content || '';
|
||||||
|
if (!fullContent || fullContent === '') {
|
||||||
if ((block.level || 0) > 0) {
|
if ((block.level || 0) > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
outdentBlock(block.id);
|
outdentBlock(block.id);
|
||||||
@@ -1332,12 +1373,77 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
target.selectionEnd === 0
|
target.selectionEnd === 0
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setBlockCursor(block.id, segStart);
|
if (fullContent[segStart - 1] === '\n') {
|
||||||
|
// Join current line with previous line within the same bullet
|
||||||
|
const joined = fullContent.slice(0, segStart - 1) + fullContent.slice(segStart);
|
||||||
|
setBlockCursor(block.id, segStart - 1);
|
||||||
|
handleContentChange(block, joined);
|
||||||
|
} else {
|
||||||
|
// Move cursor into preceding inline token to unwrap it
|
||||||
|
setBlockCursor(block.id, segStart - 1);
|
||||||
}
|
}
|
||||||
} else if (
|
}
|
||||||
!e.shiftKey &&
|
} else if (!e.shiftKey && e.key === 'ArrowLeft') {
|
||||||
(e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Home' || e.key === 'End')
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
if (segStart > 0 && target && target.selectionStart === 0 && target.selectionEnd === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setBlockCursor(block.id, segStart - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
|
setBlockCursor(block.id, segStart + target.selectionStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!e.shiftKey && e.key === 'ArrowRight') {
|
||||||
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
const fullContent = block.content || '';
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
typeof target.selectionStart === 'number' &&
|
||||||
|
target.selectionStart === target.value.length &&
|
||||||
|
segStart + target.value.length < fullContent.length
|
||||||
) {
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
setBlockCursor(block.id, segStart + target.value.length + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
|
setBlockCursor(block.id, segStart + target.selectionStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||||
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
const fullContent = block.content || '';
|
||||||
|
if (fullContent.includes('\n') && target && typeof target.selectionStart === 'number') {
|
||||||
|
const cursor = segStart + target.selectionStart;
|
||||||
|
const rawLines = fullContent.split('\n');
|
||||||
|
let run = 0;
|
||||||
|
const lineMeta = rawLines.map((lt) => {
|
||||||
|
const start = run;
|
||||||
|
const end = start + lt.length;
|
||||||
|
run = end + 1;
|
||||||
|
return { start, end, len: lt.length };
|
||||||
|
});
|
||||||
|
const curLineIdx = lineMeta.findIndex((m) => cursor >= m.start && cursor <= m.end);
|
||||||
|
if (curLineIdx !== -1) {
|
||||||
|
const col = cursor - lineMeta[curLineIdx].start;
|
||||||
|
if (e.key === 'ArrowUp' && curLineIdx > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const prevLine = lineMeta[curLineIdx - 1];
|
||||||
|
setBlockCursor(block.id, prevLine.start + Math.min(col, prevLine.len));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowDown' && curLineIdx < lineMeta.length - 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
const nextLine = lineMeta[curLineIdx + 1];
|
||||||
|
setBlockCursor(block.id, nextLine.start + Math.min(col, nextLine.len));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!e.shiftKey && (e.key === 'Home' || e.key === 'End')) {
|
||||||
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (target && typeof target.selectionStart === 'number') {
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
@@ -1871,14 +1977,33 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return <span key={key} 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
|
// Helper to render text with clickable Wiki-Links, Web-Links and Hashtags across multiple lines
|
||||||
const renderFormattedText = (text: string, blockId?: string) => {
|
const renderFormattedText = (text: string, blockId?: string) => {
|
||||||
const parts = text.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
const rawLines = (text || '').split('\n');
|
||||||
|
if (rawLines.length <= 1) {
|
||||||
|
const parts = (text || '').split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||||
return parts.map((part, i) => renderSingleRichToken(part, blockId, i));
|
return parts.map((part, i) => renderSingleRichToken(part, blockId, i));
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-0.5">
|
||||||
|
{rawLines.map((lineText, lineIdx) => {
|
||||||
|
const parts = lineText.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||||
|
return (
|
||||||
|
<div key={lineIdx} className="flex flex-wrap items-center gap-y-0.5 w-full min-h-[22px]">
|
||||||
|
{lineText.length === 0 ? (
|
||||||
|
<span> </span>
|
||||||
|
) : (
|
||||||
|
parts.map((part, i) => renderSingleRichToken(part, blockId, `${lineIdx}-${i}`))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Live inline token-aware editor: renders completed #hashtags and links inline as pills on the
|
// Live inline token-aware editor: splits multi-line bullets (Shift+Enter) into visual rows and renders
|
||||||
// exact same line while keeping any token currently touched by the cursor unwrapped as raw text.
|
// completed #hashtags and links inline as pills on each line while unwrapping any token touched by the cursor.
|
||||||
const renderLiveInlineBlock = (
|
const renderLiveInlineBlock = (
|
||||||
block: DocumentBlock,
|
block: DocumentBlock,
|
||||||
originalIndex: number,
|
originalIndex: number,
|
||||||
@@ -1893,32 +2018,90 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTokens = findRichTokens(text);
|
|
||||||
const isEditing = activeEditingBlockId === block.id;
|
const isEditing = activeEditingBlockId === block.id;
|
||||||
|
const cursor = isEditing ? Math.max(0, Math.min(text.length, activeCursorOffset)) : text.length;
|
||||||
|
|
||||||
if (!isEditing && allTokens.length > 0) {
|
const rawLines = text.split('\n');
|
||||||
const segments: (
|
let runningOffset = 0;
|
||||||
|
const lines = rawLines.map((lineText, lineIdx) => {
|
||||||
|
const lineStart = runningOffset;
|
||||||
|
const lineEnd = lineStart + lineText.length;
|
||||||
|
runningOffset = lineEnd + 1; // +1 for the '\n' delimiter
|
||||||
|
return { lineIdx, lineText, lineStart, lineEnd };
|
||||||
|
});
|
||||||
|
|
||||||
|
let activeLineIdx = lines.findIndex((l) => cursor >= l.lineStart && cursor <= l.lineEnd);
|
||||||
|
if (activeLineIdx === -1) {
|
||||||
|
activeLineIdx = lines.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InlineSegment =
|
||||||
| { kind: 'text'; start: number; end: number; text: string }
|
| { kind: 'text'; start: number; end: number; text: string }
|
||||||
| { kind: 'token'; start: number; end: number; raw: string }
|
| { kind: 'token'; start: number; end: number; raw: string };
|
||||||
)[] = [];
|
|
||||||
let lastIdx = 0;
|
return (
|
||||||
for (const t of allTokens) {
|
<div className={`w-full space-y-0.5 ${extraTextClass}`}>
|
||||||
|
{lines.map((line) => {
|
||||||
|
const isLineActive = isEditing && line.lineIdx === activeLineIdx;
|
||||||
|
const lineTokens = findRichTokens(line.lineText).map((t) => ({
|
||||||
|
start: line.lineStart + t.start,
|
||||||
|
end: line.lineStart + t.end,
|
||||||
|
raw: t.raw,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Non-active line (or block not currently focused)
|
||||||
|
if (!isLineActive) {
|
||||||
|
const segments: InlineSegment[] = [];
|
||||||
|
let lastIdx = line.lineStart;
|
||||||
|
for (const t of lineTokens) {
|
||||||
if (t.start > lastIdx) {
|
if (t.start > lastIdx) {
|
||||||
segments.push({ kind: 'text', start: lastIdx, end: t.start, text: text.slice(lastIdx, t.start) });
|
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 });
|
segments.push({ kind: 'token', start: t.start, end: t.end, raw: t.raw });
|
||||||
lastIdx = t.end;
|
lastIdx = t.end;
|
||||||
}
|
}
|
||||||
if (lastIdx < text.length) {
|
if (lastIdx < line.lineEnd) {
|
||||||
segments.push({ kind: 'text', start: lastIdx, end: text.length, text: text.slice(lastIdx) });
|
segments.push({
|
||||||
|
kind: 'text',
|
||||||
|
start: lastIdx,
|
||||||
|
end: line.lineEnd,
|
||||||
|
text: text.slice(lastIdx, line.lineEnd),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => focusBlock(block.id, text.length)}
|
key={`line-${line.lineIdx}`}
|
||||||
className={`flex flex-wrap items-center gap-y-0.5 w-full text-sm leading-relaxed break-words min-h-[22px] cursor-text ${extraTextClass}`}
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusBlock(block.id, line.lineEnd);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
focusBlock(block.id, line.lineEnd);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex flex-wrap items-center gap-y-0.5 w-full text-sm leading-relaxed break-words min-h-[22px] cursor-text"
|
||||||
>
|
>
|
||||||
{segments.map((seg, i) =>
|
{segments.length === 0 ? (
|
||||||
|
<span
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
focusBlock(block.id, line.lineStart);
|
||||||
|
}}
|
||||||
|
className="w-full min-h-[22px] inline-block"
|
||||||
|
>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
segments.map((seg, i) =>
|
||||||
seg.kind === 'token' ? (
|
seg.kind === 'token' ? (
|
||||||
renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${i}`)
|
renderSingleRichToken(seg.raw, block.id, `tok-${seg.start}-${i}`)
|
||||||
) : (
|
) : (
|
||||||
@@ -1938,22 +2121,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{seg.text}
|
{seg.text}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursor = isEditing ? Math.max(0, Math.min(text.length, activeCursorOffset)) : text.length;
|
// Active line being edited
|
||||||
const renderedTokens = isEditing
|
const renderedTokens = lineTokens.filter((t) => !(cursor >= t.start && cursor <= t.end));
|
||||||
? 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[] = [];
|
const segments: InlineSegment[] = [];
|
||||||
let lastIdx = 0;
|
let lastIdx = line.lineStart;
|
||||||
for (const t of renderedTokens) {
|
for (const t of renderedTokens) {
|
||||||
segments.push({
|
segments.push({
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
@@ -1972,8 +2149,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
segments.push({
|
segments.push({
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
start: lastIdx,
|
start: lastIdx,
|
||||||
end: text.length,
|
end: line.lineEnd,
|
||||||
text: text.slice(lastIdx),
|
text: text.slice(lastIdx, line.lineEnd),
|
||||||
});
|
});
|
||||||
|
|
||||||
let activeSegIdx = segments.findIndex(
|
let activeSegIdx = segments.findIndex(
|
||||||
@@ -1985,13 +2162,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
key={`line-${line.lineIdx}`}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
focusBlock(block.id, text.length);
|
focusBlock(block.id, line.lineEnd);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`flex flex-wrap items-center gap-y-0.5 w-full min-h-[22px] cursor-text ${extraTextClass}`}
|
className="flex flex-wrap items-center gap-y-0.5 w-full min-h-[22px] cursor-text"
|
||||||
>
|
>
|
||||||
{segments.map((seg, idx) => {
|
{segments.map((seg, idx) => {
|
||||||
if (seg.kind === 'token') {
|
if (seg.kind === 'token') {
|
||||||
@@ -2066,6 +2244,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- GLOBAL TO-DO ROLLUP AGGREGATOR ---
|
// --- GLOBAL TO-DO ROLLUP AGGREGATOR ---
|
||||||
@@ -3120,7 +3301,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{/* TYPE: TODO */}
|
{/* TYPE: TODO */}
|
||||||
{block.type === 'todo' && (
|
{block.type === 'todo' && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2 group/todo">
|
<div className="flex items-start gap-2 group/todo">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
disabled={isReadOnly}
|
disabled={isReadOnly}
|
||||||
@@ -3134,7 +3315,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
onStatsChange();
|
onStatsChange();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-4 h-4 checkbox-square flex-shrink-0 ${
|
className={`w-4 h-4 mt-1 checkbox-square flex-shrink-0 ${
|
||||||
isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -3247,7 +3428,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<textarea
|
<textarea
|
||||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
rows={Math.max(1, Math.ceil((block.content || '').length / 70))}
|
rows={Math.max(1, (block.content || '').split('\n').length, Math.ceil((block.content || '').length / 70))}
|
||||||
value={block.content}
|
value={block.content}
|
||||||
onFocus={handleBlockFocus}
|
onFocus={handleBlockFocus}
|
||||||
onBlur={handleBlockBlur}
|
onBlur={handleBlockBlur}
|
||||||
|
|||||||
@@ -1178,13 +1178,16 @@ func (s *Store) saveDocumentToDisk(doc *domain.Document) {
|
|||||||
if b.Checked {
|
if b.Checked {
|
||||||
chk = "x"
|
chk = "x"
|
||||||
}
|
}
|
||||||
md.WriteString(fmt.Sprintf("%s- [%s] %s\n", indent, chk, b.Content))
|
formatted := strings.ReplaceAll(b.Content, "\n", "\n"+indent+" ")
|
||||||
|
md.WriteString(fmt.Sprintf("%s- [%s] %s\n", indent, chk, formatted))
|
||||||
case "bullet", "paragraph":
|
case "bullet", "paragraph":
|
||||||
md.WriteString(fmt.Sprintf("%s- %s\n", indent, b.Content))
|
formatted := strings.ReplaceAll(b.Content, "\n", "\n"+indent+" ")
|
||||||
|
md.WriteString(fmt.Sprintf("%s- %s\n", indent, formatted))
|
||||||
case "code":
|
case "code":
|
||||||
md.WriteString(fmt.Sprintf("%s- ```%s\n%s %s\n%s ```\n", indent, b.Lang, indent, b.Content, indent))
|
md.WriteString(fmt.Sprintf("%s- ```%s\n%s %s\n%s ```\n", indent, b.Lang, indent, b.Content, indent))
|
||||||
case "callout":
|
case "callout":
|
||||||
md.WriteString(fmt.Sprintf("%s- > %s\n", indent, b.Content))
|
formatted := strings.ReplaceAll(b.Content, "\n", "\n"+indent+" > ")
|
||||||
|
md.WriteString(fmt.Sprintf("%s- > %s\n", indent, formatted))
|
||||||
case "divider":
|
case "divider":
|
||||||
md.WriteString(fmt.Sprintf("%s- ---\n", indent))
|
md.WriteString(fmt.Sprintf("%s- ---\n", indent))
|
||||||
case "table":
|
case "table":
|
||||||
@@ -1192,7 +1195,8 @@ func (s *Store) saveDocumentToDisk(doc *domain.Document) {
|
|||||||
case "progress":
|
case "progress":
|
||||||
md.WriteString(fmt.Sprintf("%s- <!-- progress: %s -->\n", indent, b.Content))
|
md.WriteString(fmt.Sprintf("%s- <!-- progress: %s -->\n", indent, b.Content))
|
||||||
default:
|
default:
|
||||||
md.WriteString(fmt.Sprintf("%s- %s\n", indent, b.Content))
|
formatted := strings.ReplaceAll(b.Content, "\n", "\n"+indent+" ")
|
||||||
|
md.WriteString(fmt.Sprintf("%s- %s\n", indent, formatted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if doc.Content != "" {
|
} else if doc.Content != "" {
|
||||||
|
|||||||
Reference in new issue
Block a user