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

@@ -15,6 +15,7 @@ import {
MessageSquare,
Hash,
ExternalLink,
Maximize2,
} from 'lucide-react';
import { DocumentBlock, BlockType } from '../../lib/types';
import { RemoteCursor } from '../../lib/useDocumentSync';
@@ -46,6 +47,8 @@ export interface OutlinerBlockProps {
canEdit: boolean;
activeRemoteCursors?: RemoteCursor[];
sendCursorMoved?: (blockId: string | null, selectionStart?: number, selectionEnd?: number) => void;
levelOffset?: number;
onZoomBlock?: (blockId: string) => void;
onFocusBlock: (blockId: string) => void;
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
onCreateBlockBelow: (afterBlockId: string, currentContent: string) => void;
@@ -69,6 +72,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
canEdit,
activeRemoteCursors = [],
sendCursorMoved,
levelOffset = 0,
onZoomBlock,
onFocusBlock,
onUpdateBlock,
onCreateBlockBelow,
@@ -334,6 +339,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
};
const level = Math.max(0, block.level || 0);
const visualLevel = Math.max(0, level - (levelOffset || 0));
// Parse rich tokens (WikiLinks [[Page]] and #Tags)
const tokens = useMemo(() => {
@@ -355,7 +361,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
: 'hover:bg-panel'
}`}
style={{
paddingLeft: `${level * 22 + 4}px`,
paddingLeft: `${visualLevel * 22 + 4}px`,
...(primaryRemoteCursor
? { borderLeft: `2px solid ${primaryRemoteCursor.userColor}` }
: {}),
@@ -379,8 +385,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
)}
{/* Indentation Guide Line */}
{level > 0 &&
Array.from({ length: level }).map((_, lvlIdx) => (
{visualLevel > 0 &&
Array.from({ length: visualLevel }).map((_, lvlIdx) => (
<div
key={lvlIdx}
className="pointer-events-none absolute top-0 bottom-0 w-px border-l border-border"
@@ -440,15 +446,9 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
) : (
<button
type="button"
onClick={() => {
if (canEdit) {
flushDebounce(localContent, {
type: block.type === 'todo' ? 'bullet' : 'todo',
});
}
}}
className="w-4 h-4 flex items-center justify-center rounded-btn hover:bg-panel-hover"
title="Klick: Zu To-Do / Bullet umschalten"
onClick={() => onZoomBlock?.(block.id)}
className="w-4 h-4 flex items-center justify-center rounded-btn hover:bg-panel-hover group/bullet transition-transform"
title="Klick: In diesen Ast hineinzoomen (Fokus-Modus)"
>
<span
className={`w-1.5 h-1.5 rounded-full transition-all ${
@@ -456,7 +456,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
? 'bg-accent ring-2 ring-accent/30'
: isFocused
? 'bg-accent scale-125'
: 'bg-faint group-hover:bg-accent'
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
}`}
/>
</button>
@@ -670,6 +670,16 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
)}
</div>
{/* Zoom / Focus button on hover */}
<button
type="button"
onClick={() => onZoomBlock?.(block.id)}
className="opacity-0 group-hover:opacity-100 focus:opacity-100 p-0.5 text-faint hover:text-accent rounded-btn transition-opacity shrink-0"
title="In diesen Ast hineinzoomen (Fokus-Modus)"
>
<Maximize2 className="w-3.5 h-3.5" />
</button>
{/* Delete button on hover */}
{canEdit && (
<button