feat(workspace): add focus mode and hoisting zoom-in for outliner blocks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s

- Click bullet point (•) or hover zoom icon to zoom into any block branch
- Render hoisted block as prominent heading in focus mode with direct editing
- Display clickable breadcrumb path leading back to ancestors or root document
- Render isolated subtree with visual level offset and bound indentation
This commit is contained in:
sandrodach committed 2026-10-26 22:04:53 +01:00
1 parent 95977ba15a
commit 177e24fdc8
3 files changed
+346 -78

No files matched your search

@@ -8,6 +8,8 @@ export interface OutlinerTreeProps {
docId: string;
blocks: DocumentBlock[];
canEdit: boolean;
levelOffset?: number;
onZoomBlock?: (blockId: string) => void;
remoteCursors?: Record<string, RemoteCursor>;
sendCursorMoved?: (blockId: string | null, selectionStart?: number, selectionEnd?: number) => void;
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
@@ -22,6 +24,8 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
docId,
blocks,
canEdit,
levelOffset = 0,
onZoomBlock,
remoteCursors,
sendCursorMoved,
onUpdateBlock,
@@ -105,7 +109,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
const hasChildren = Boolean(nextBlock && nextLvl > lvl);
const prevBlock = i > 0 ? blocks[i - 1] : null;
const maxAllowedLevel = prevBlock ? Math.max(0, prevBlock.level || 0) + 1 : 0;
const maxAllowedLevel = prevBlock ? Math.max(0, prevBlock.level || 0) + 1 : (levelOffset || 0);
result.push({
block: b,
@@ -155,8 +159,8 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
const idx = blocks.findIndex((b) => b.id === blockId);
if (idx === -1) return;
// If this is the only block in the document, clear its content/type instead of leaving 0 blocks
if (blocks.length <= 1) {
// If this is the only block in the document (and not in an isolated zoom subtree), clear instead of deleting
if (blocks.length <= 1 && !levelOffset) {
const only = blocks[0];
onUpdateBlock(
{
@@ -191,10 +195,11 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
const prevLevel = idx > 0 ? Math.max(0, blocks[idx - 1].level || 0) : 0;
const maxLevel = idx === 0 ? 0 : prevLevel + 1;
const minLevel = levelOffset || 0;
const nextLevel =
delta === 1
? Math.min(maxLevel, currentLevel + 1)
: Math.max(0, currentLevel - 1);
: Math.max(minLevel, currentLevel - 1);
if (nextLevel === currentLevel) return;
@@ -283,6 +288,8 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
hasChildren={hasChildren}
isFocused={focusedBlockId === block.id}
canEdit={canEdit}
levelOffset={levelOffset}
onZoomBlock={onZoomBlock}
activeRemoteCursors={cursorsByBlock[block.id] || []}
sendCursorMoved={sendCursorMoved}
onFocusBlock={setFocusedBlockId}
@@ -315,7 +322,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
documentId: docId,
type: 'text',
content: '',
level: 0,
level: levelOffset || 0,
position: 1000,
};
pendingFocusRef.current = { blockId: firstBlock.id, cursor: 'start' };