Dev #3

Merged
sandrodach merged 30 commits from dev into main 2026-10-25 18:00:10 +01:00
Showing only changes of commit 31847ffaa3 - Show all commits
+139 -13
View File
@@ -996,12 +996,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
};
// Insert a new bullet immediately after index, inheriting level
const insertNewBulletAfter = (afterIndex: number, level: number, type: BlockType = 'bullet') => {
const insertNewBulletAfter = (
afterIndex: number,
level: number,
type: BlockType = 'bullet',
initialContent: string = ''
) => {
if (isReadOnly || !selectedDoc) return;
const newBlock: DocumentBlock = {
id: `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
type,
content: '',
content: initialContent,
level,
checked: false,
};
@@ -1016,6 +1021,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const el = blockInputRefs.current[newBlock.id];
if (el) {
el.focus();
if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
(el as any).setSelectionRange(0, 0);
}
}
}, 20);
};
@@ -1179,7 +1187,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
setSlashBlockId(null);
insertNewBulletAfter(index, block.level || 0);
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
let afterContent = '';
if (target && typeof target.selectionStart === 'number') {
const cursor = target.selectionStart;
const fullContent = block.content || '';
const before = fullContent.slice(0, cursor);
const after = fullContent.slice(cursor);
if (after.length > 0) {
updateBlock(block.id, { content: before });
afterContent = after;
}
}
insertNewBulletAfter(index, block.level || 0, block.type === 'todo' ? 'todo' : 'bullet', afterContent);
return;
} else if (e.key === 'Tab') {
e.preventDefault();
if (e.shiftKey) {
@@ -1202,6 +1223,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const handleContentChange = (block: DocumentBlock, newContent: string) => {
if (isReadOnly) return;
ensureLock();
// Markdown shortcut: typing '---' creates a divider and inserts a new bullet below
if (newContent.trim() === '---') {
const blkIdx = selectedDoc ? selectedDoc.blocks.findIndex((b) => b.id === block.id) : -1;
if (selectedDoc && blkIdx !== -1) {
const curLevel = block.level || 0;
const newBlockId = `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const newBullet: DocumentBlock = {
id: newBlockId,
type: 'bullet',
content: '',
level: curLevel,
checked: false,
};
const nextBlocks = [...selectedDoc.blocks];
nextBlocks[blkIdx] = {
...block,
type: 'divider',
content: '',
};
nextBlocks.splice(blkIdx + 1, 0, newBullet);
persistDocument({ ...selectedDoc, blocks: nextBlocks });
setSlashBlockId(null);
focusBlock(newBlockId);
return;
}
}
updateBlock(block.id, { content: newContent });
// Check if user just typed '/'
@@ -1270,6 +1319,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}, 20);
};
const handleCanvasBottomClick = () => {
if (isReadOnly || !selectedDoc) return;
if (selectedDoc.blocks.length === 0) {
insertNewBulletAfter(-1, 0);
return;
}
const lastIndex = selectedDoc.blocks.length - 1;
const lastBlock = selectedDoc.blocks[lastIndex];
if (lastBlock && lastBlock.type !== 'divider' && (!lastBlock.content || lastBlock.content.trim() === '')) {
focusBlock(lastBlock.id);
} else {
insertNewBulletAfter(lastIndex, 0);
}
};
const applySlashCommand = (blockId: string, cmd: SlashCommandItem) => {
if (isReadOnly || !selectedDoc) return;
const block = selectedDoc.blocks.find((b) => b.id === blockId);
@@ -1290,6 +1354,29 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}
if (cmd.type) {
if (cmd.type === 'divider') {
const blkIdx = selectedDoc.blocks.findIndex((b) => b.id === blockId);
const curLevel = block.level || 0;
const newBlockId = `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const newBullet: DocumentBlock = {
id: newBlockId,
type: 'bullet',
content: '',
level: curLevel,
checked: false,
};
const nextBlocks = [...selectedDoc.blocks];
nextBlocks[blkIdx] = {
...block,
type: 'divider',
content: '',
};
nextBlocks.splice(blkIdx + 1, 0, newBullet);
persistDocument({ ...selectedDoc, blocks: nextBlocks });
focusBlock(newBlockId);
return;
}
updateBlock(blockId, { type: cmd.type, content: cleanContent, checked: cmd.type === 'todo' ? false : undefined });
focusBlock(blockId);
}
@@ -2266,7 +2353,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Right Logseq Outliner Editor */}
<div className="lg:col-span-8">
{selectedDoc ? (
<div className="p-4 panel-surface space-y-3 min-h-[500px]">
<div
className="p-4 panel-surface space-y-3 min-h-[500px]"
onClick={(e) => {
if (e.target === e.currentTarget) {
handleCanvasBottomClick();
}
}}
>
{/* Document Header & Metadata */}
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 panel-header">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
@@ -2519,15 +2613,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
{/* =========================================================== */}
<div
className="space-y-1 pt-2 min-h-[300px] cursor-text"
className="space-y-1 pt-2 min-h-[400px] pb-20 cursor-text"
onClick={(e) => {
if (e.target === e.currentTarget) {
if (selectedDoc.blocks.length === 0) {
insertNewBulletAfter(-1, 0);
} else {
const lastBlock = selectedDoc.blocks[selectedDoc.blocks.length - 1];
blockInputRefs.current[lastBlock.id]?.focus();
}
handleCanvasBottomClick();
}
}}
>
@@ -2822,8 +2911,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* TYPE: DIVIDER */}
{block.type === 'divider' && (
<div className="py-2">
<hr className="border-border" />
<div
tabIndex={isReadOnly ? undefined : 0}
onKeyDown={(e) => {
if (!isReadOnly && e.key === 'Enter') {
e.preventDefault();
insertNewBulletAfter(originalIndex, block.level || 0);
} else if (!isReadOnly && (e.key === 'Backspace' || e.key === 'Delete')) {
e.preventDefault();
deleteBlockAndFocusPrev(originalIndex);
}
}}
onClick={() => {
if (!isReadOnly) {
insertNewBulletAfter(originalIndex, block.level || 0);
}
}}
className="py-2.5 my-1 group/divider relative cursor-pointer focus:outline-none"
title={isReadOnly ? undefined : "Klicken oder Enter drücken, um darunter einen neuen Aufzählungspunkt einzufügen"}
>
<div className="h-0.5 w-full bg-border group-hover/divider:bg-accent/70 group-focus/divider:bg-accent transition-colors rounded-full" />
{!isReadOnly && (
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover/divider:opacity-100 transition-opacity pointer-events-none">
<span className="bg-panel border border-border text-[10px] text-muted px-2 py-0.5 rounded-full shadow-xs flex items-center gap-1 font-medium">
<Plus className="w-3 h-3 text-accent" />
<span>Punkt einfügen</span>
</span>
</div>
)}
</div>
)}
@@ -2899,6 +3014,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div>
);
})}
{/* Explicit bottom clickable area */}
{!isReadOnly && (
<div
className="pt-3 pb-8 px-2 text-xs text-faint hover:text-muted cursor-pointer transition-colors flex items-center gap-1.5 opacity-60 hover:opacity-100 select-none"
onClick={handleCanvasBottomClick}
>
<Plus className="w-3.5 h-3.5 text-accent" />
<span>Neuen Punkt hinzufügen...</span>
</div>
)}
</div>
</div>
) : (