From b4c8577a6159618c1d23427d1d208d74b4bc61a8 Mon Sep 17 00:00:00 2001 From: Sandro Date: Wed, 28 Oct 2026 08:53:35 +0100 Subject: [PATCH] fix(workspace): make Backspace merge work for untyped blocks and blocks with children The merge only applied to a fixed list of block types, so legacy blocks without a type were ignored, and blocks with children were rejected. Now only code/divider/table/progress are excluded and children move up under the previous block. The focused bullet dot is also neutral now. Co-Authored-By: Claude Sonnet 5.5 --- .../src/features/workspace/OutlinerBlock.tsx | 2 +- .../src/features/workspace/OutlinerTree.tsx | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/workspace/OutlinerBlock.tsx b/frontend/src/features/workspace/OutlinerBlock.tsx index 700cf3b..6fba3ae 100644 --- a/frontend/src/features/workspace/OutlinerBlock.tsx +++ b/frontend/src/features/workspace/OutlinerBlock.tsx @@ -598,7 +598,7 @@ export const OutlinerBlock: React.FC = ({ block.collapsed && hasChildren ? 'bg-accent ring-2 ring-accent/30' : activeFocus - ? 'bg-accent scale-125' + ? 'bg-main scale-125' : 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125' } ${ isEmpty diff --git a/frontend/src/features/workspace/OutlinerTree.tsx b/frontend/src/features/workspace/OutlinerTree.tsx index e7c8910..6254449 100644 --- a/frontend/src/features/workspace/OutlinerTree.tsx +++ b/frontend/src/features/workspace/OutlinerTree.tsx @@ -525,24 +525,37 @@ export const OutlinerTree = forwardRef(({ const handleMergeWithPrevious = useCallback( (blockId: string, content: string) => { if (!canEdit) return false; - const mergeable = ['text', 'paragraph', 'bullet', 'todo', 'h1', 'h2', 'h3', 'callout']; + const notMergeable = ['code', 'divider', 'table', 'progress']; const visIdx = visibleEntries.findIndex((e) => e.block.id === blockId); if (visIdx <= 0) return false; const prev = visibleEntries[visIdx - 1].block; const idx = blocks.findIndex((b) => b.id === blockId); const cur = blocks[idx]; - if (!cur || !mergeable.includes(prev.type) || !mergeable.includes(cur.type)) return false; - // Blocks with children would leave them orphaned at a wrong indent level - const next = blocks[idx + 1]; - if (next && (next.level || 0) > (cur.level || 0)) return false; + if (!cur || notMergeable.includes(prev.type) || notMergeable.includes(cur.type)) return false; + + // Children of the merged block move up so they become children of the previous block + const curLevel = Math.max(0, cur.level || 0); + const delta = Math.max(0, prev.level || 0) - curLevel; + const movedChildren: DocumentBlock[] = []; + for (let j = idx + 1; j < blocks.length; j++) { + const lvl = Math.max(0, blocks[j].level || 0); + if (lvl <= curLevel) break; + movedChildren.push({ ...blocks[j], level: Math.max(levelOffset || 0, lvl + delta) }); + } const joinPos = (prev.content || '').length; - onUpdateBlock({ ...prev, content: (prev.content || '') + content }, true); + const mergedPrev: DocumentBlock = { ...prev, content: (prev.content || '') + content }; + if (movedChildren.length > 0 && onUpdateBlocks) { + onUpdateBlocks([mergedPrev, ...movedChildren]); + } else { + onUpdateBlock(mergedPrev, true); + movedChildren.forEach((c) => onUpdateBlock(c)); + } onDeleteBlock(cur.id); focusBlockInput(prev.id, joinPos); return true; }, - [blocks, canEdit, focusBlockInput, onDeleteBlock, onUpdateBlock, visibleEntries] + [blocks, canEdit, focusBlockInput, levelOffset, onDeleteBlock, onUpdateBlock, onUpdateBlocks, visibleEntries] ); const handleIndentBlock = useCallback(