fix(workspace): fix outliner cursor focus, enhanced task linking, nav order and Hauptebene drop zone
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 19s

This commit is contained in:
sandrodach committed 2026-10-27 20:26:45 +01:00
1 parent d39d1d1122
commit 94acb2638e
7 files changed
+300 -182

No files matched your search

+111 -75
View File
@@ -44,6 +44,7 @@ const SLASH_COMMANDS: SlashCommand[] = [
];
export interface OutlinerBlockProps {
docId: string;
block: DocumentBlock;
index: number;
isLast: boolean;
@@ -58,6 +59,8 @@ export interface OutlinerBlockProps {
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
onIndentBlock: (blockId: string, delta: 1 | -1) => void;
onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void;
onFocusBlock?: (blockId: string) => void;
onOpenTask?: (taskId: string) => void;
onTagClick?: (tag: string) => void;
onWikiLinkClick?: (pageTitle: string) => void;
onDragStartBlock?: (index: number) => void;
@@ -69,6 +72,7 @@ export interface OutlinerBlockProps {
}
export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
docId,
block,
index,
isLast,
@@ -83,6 +87,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
onDeleteBlock,
onIndentBlock,
onNavigateFocus,
onFocusBlock,
onOpenTask,
onTagClick,
onWikiLinkClick,
onDragStartBlock,
@@ -93,6 +99,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
registerTextareaRef,
}) => {
const [localContent, setLocalContent] = useState<string>(block.content || '');
const [isLocallyFocused, setIsLocallyFocused] = useState<boolean>(false);
const activeFocus = isFocused || isLocallyFocused;
const [slashSearch, setSlashSearch] = useState<string | null>(null);
const [slashIndex, setSlashIndex] = useState(0);
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
@@ -199,7 +207,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
useLayoutEffect(() => {
adjustHeight();
}, [localContent, block.type, block.level, adjustHeight]);
}, [localContent, block.type, block.level, activeFocus, adjustHeight]);
useEffect(() => {
window.addEventListener('resize', adjustHeight);
@@ -299,10 +307,14 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
const handleFocus = () => {
isFocusedRef.current = true;
setIsLocallyFocused(true);
onFocusBlock?.(block.id);
setTimeout(adjustHeight, 0);
};
const handleBlur = () => {
isFocusedRef.current = false;
setIsLocallyFocused(false);
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
@@ -440,7 +452,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
? 'border-t-2 border-accent bg-accent/10'
: isTargeted
? 'bg-accent/15 ring-2 ring-accent'
: isFocused
: activeFocus
? 'bg-panel-elevated'
: 'hover:bg-panel'
}`}
@@ -500,7 +512,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
className={`w-1.5 h-1.5 rounded-full transition-all ${
block.collapsed && hasChildren
? 'bg-accent ring-2 ring-accent/30'
: isFocused
: activeFocus
? 'bg-accent scale-125'
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
}`}
@@ -525,16 +537,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
<Check className="w-3 h-3 stroke-[3]" />
</button>
)}
{block.taskId && (
<span
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1 py-0.2 rounded font-medium shrink-0"
title="Mit erweiterter Aufgabe synchronisiert"
>
<CheckSquare className="w-2.5 h-2.5" />
<span>Erweitert</span>
</span>
)}
</div>
{/* Main Block Content */}
@@ -589,23 +591,101 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
</div>
) : (
<div className="relative w-full">
{!isFocused && localContent && (
<div
className={`absolute inset-0 cursor-text select-text pointer-events-none w-full leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
block.type === 'h1'
? 'text-lg font-bold text-main tracking-tight'
: block.type === 'h2'
? 'text-base font-semibold text-main'
: block.type === 'h3'
? 'text-sm font-semibold text-main'
: block.type === 'todo' && block.checked
? 'text-sm text-faint line-through'
: 'text-sm text-main'
}`}
>
{renderInlineMarkdown(localContent)}
</div>
)}
{/* View Layer with Inline Markdown & Actions (3-dots or Erweitert Badge) directly behind text */}
<div
onClick={() => {
if (!activeFocus && canEdit) {
textareaRef.current?.focus();
}
}}
className={`w-full leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
activeFocus ? 'absolute inset-0 pointer-events-none' : 'relative cursor-text'
} ${
block.type === 'h1'
? 'text-lg font-bold text-main tracking-tight'
: block.type === 'h2'
? 'text-base font-semibold text-main'
: block.type === 'h3'
? 'text-sm font-semibold text-main'
: block.type === 'todo' && block.checked
? 'text-sm text-faint line-through'
: 'text-sm text-main'
}`}
>
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
<span className={activeFocus ? 'invisible' : ''}>
{localContent ? renderInlineMarkdown(localContent) : ''}
</span>
{/* Inline Badge or 3-Dots directly behind the text */}
{block.type === 'todo' && (
<span className="inline-flex items-center align-middle ml-1.5 gap-1 shrink-0 pointer-events-auto z-10 relative select-none">
{block.taskId ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onOpenTask?.(block.taskId!);
}}
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded font-medium cursor-pointer hover:opacity-80 transition-opacity"
title="Zu dieser erweiterten Aufgabe wechseln"
>
<CheckSquare className="w-2.5 h-2.5" />
<span>Erweitert</span>
</button>
) : canEdit ? (
<span className="relative inline-flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setShowTodoMenu((prev) => !prev);
}}
className="p-0.5 text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="Aufgaben-Optionen"
>
<MoreHorizontal className="w-3.5 h-3.5" />
</button>
{showTodoMenu && (
<div
className="absolute left-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={async () => {
setShowTodoMenu(false);
try {
const targetDocId = docId || block.documentId || block.docId || '';
const thought = await api.createEnhancedTask(targetDocId, block.id, {
content: localContent || 'Aufgabe',
checked: Boolean(block.checked),
});
const updatedBlock: DocumentBlock = {
...latestBlockRef.current,
content: localContent,
taskId: thought.id,
type: 'todo',
};
onUpdateBlockRef.current(updatedBlock, true);
onOpenTask?.(thought.id);
} catch (err: any) {
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
}
}}
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
>
<CheckSquare className="w-3.5 h-3.5 text-accent" />
<span>In erweiterte Aufgabe umwandeln</span>
</button>
</div>
)}
</span>
) : null}
</span>
)}
</div>
<textarea
ref={(el) => {
textareaRef.current = el;
@@ -624,7 +704,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
: ''
}
className={`w-full resize-none overflow-hidden bg-transparent border-0 focus:outline-none focus:ring-0 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
!isFocused && localContent ? 'text-transparent selection:text-transparent' : ''
activeFocus ? 'relative z-0 text-main' : 'absolute inset-0 text-transparent selection:text-transparent pointer-events-none'
} ${
block.type === 'h1'
? 'text-lg font-bold text-main tracking-tight'
@@ -749,50 +829,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
)}
</div>
{/* Enhanced Task Option (3-dots) for To-Do Blocks */}
{canEdit && block.type === 'todo' && !block.taskId && (
<div className="relative shrink-0">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setShowTodoMenu((prev) => !prev);
}}
className="opacity-0 group-hover:opacity-100 p-0.5 text-faint hover:text-accent rounded-btn transition-opacity cursor-pointer"
title="Aufgaben-Optionen"
>
<MoreHorizontal className="w-3.5 h-3.5" />
</button>
{showTodoMenu && (
<div
className="absolute right-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={async () => {
setShowTodoMenu(false);
try {
const docId = block.documentId || block.docId || '';
const thought = await api.createEnhancedTask(docId, block.id, {
content: localContent || 'Aufgabe',
checked: Boolean(block.checked),
});
flushDebounce(localContent, { taskId: thought.id });
} catch (err: any) {
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
}
}}
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
>
<CheckSquare className="w-3.5 h-3.5 text-accent" />
<span>In erweiterte Aufgabe umwandeln</span>
</button>
</div>
)}
</div>
)}
{/* Zoom / Focus button on hover */}
<button
type="button"