feat: complete workspace, triage, outliner and sharing overhaul (tasks 1-26)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s

This commit is contained in:
sandrodach committed 2026-10-27 19:46:46 +01:00
1 parent df2f2bac67
commit d39d1d1122
17 files changed
+1173 -501

No files matched your search

+120 -31
View File
@@ -3,7 +3,10 @@ import {
ChevronRight,
ChevronDown,
Check,
CheckSquare,
X,
Trash2,
MoreHorizontal,
Code,
Heading1,
Heading2,
@@ -19,6 +22,8 @@ import {
} from 'lucide-react';
import { DocumentBlock, BlockType } from '../../lib/types';
import { LinkModal } from './LinkModal';
import { renderInlineMarkdown } from '../../lib/markdown';
import { api } from '../../lib/api';
export interface SlashCommand {
type: BlockType | 'link';
@@ -50,7 +55,7 @@ export interface OutlinerBlockProps {
onZoomBlock?: (blockId: string) => void;
onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void;
onCreateBlockBelow: (afterBlockId: string, currentContent: string) => void;
onDeleteBlock: (blockId: string) => void;
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
onIndentBlock: (blockId: string, delta: 1 | -1) => void;
onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void;
onTagClick?: (tag: string) => void;
@@ -91,6 +96,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
const [slashSearch, setSlashSearch] = useState<string | null>(null);
const [slashIndex, setSlashIndex] = useState(0);
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
const [showTodoMenu, setShowTodoMenu] = useState<boolean>(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const debounceTimerRef = useRef<number | null>(null);
@@ -475,7 +481,34 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
<span className="w-3.5" />
)}
{block.type === 'todo' ? (
{/* Bullet handle for dragging and zoom focus */}
<button
type="button"
draggable={canEdit}
onDragStart={() => canEdit && onDragStartBlock?.(index)}
onClick={() => onZoomBlock?.(block.id)}
className={`w-4 h-4 flex items-center justify-center rounded-btn hover:bg-panel-hover group/bullet transition-transform ${
canEdit ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer'
}`}
title={
canEdit
? 'Ziehen zum Verschieben • Klick: Ast hineinzoomen (Fokus-Modus)'
: 'Klick: In diesen Ast hineinzoomen (Fokus-Modus)'
}
>
<span
className={`w-1.5 h-1.5 rounded-full transition-all ${
block.collapsed && hasChildren
? 'bg-accent ring-2 ring-accent/30'
: isFocused
? 'bg-accent scale-125'
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
}`}
/>
</button>
{/* If To-Do block: checkbox alongside bullet */}
{block.type === 'todo' && (
<button
type="button"
disabled={!canEdit}
@@ -491,31 +524,16 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
>
<Check className="w-3 h-3 stroke-[3]" />
</button>
) : (
<button
type="button"
draggable={canEdit}
onDragStart={() => canEdit && onDragStartBlock?.(index)}
onClick={() => onZoomBlock?.(block.id)}
className={`w-4 h-4 flex items-center justify-center rounded-btn hover:bg-panel-hover group/bullet transition-transform ${
canEdit ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer'
}`}
title={
canEdit
? 'Ziehen zum Verschieben • Klick: Ast hineinzoomen (Fokus-Modus)'
: 'Klick: In diesen Ast hineinzoomen (Fokus-Modus)'
}
)}
{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"
>
<span
className={`w-1.5 h-1.5 rounded-full transition-all ${
block.collapsed && hasChildren
? 'bg-accent ring-2 ring-accent/30'
: isFocused
? 'bg-accent scale-125'
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
}`}
/>
</button>
<CheckSquare className="w-2.5 h-2.5" />
<span>Erweitert</span>
</span>
)}
</div>
@@ -571,6 +589,23 @@ 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>
)}
<textarea
ref={(el) => {
textareaRef.current = el;
@@ -588,7 +623,9 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
? 'Schreibe einen Punkt… (Enter = Neu, Tab = Einrücken, Shift+Tab = Ausrücken, / für Befehle)'
: ''
}
className={`w-full resize-none overflow-hidden bg-transparent border-0 focus:outline-none focus:ring-0 leading-relaxed whitespace-pre-wrap break-words ${
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' : ''
} ${
block.type === 'h1'
? 'text-lg font-bold text-main tracking-tight'
: block.type === 'h2'
@@ -712,6 +749,50 @@ 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"
@@ -722,15 +803,23 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
<Maximize2 className="w-3.5 h-3.5" />
</button>
{/* Delete button on hover */}
{/* Delete button on hover (Trash2 icon) */}
{canEdit && (
<button
type="button"
onClick={() => onDeleteBlockRef.current(block.id)}
className="opacity-0 group-hover:opacity-100 focus:opacity-100 p-0.5 text-faint hover:text-status-err rounded-btn transition-opacity shrink-0"
onClick={() => {
if (hasChildren) {
if (window.confirm('Bist du sicher? Alle Unterpunkte dieses Asts werden ebenfalls gelöscht.')) {
onDeleteBlockRef.current(block.id, true);
}
} else {
onDeleteBlockRef.current(block.id, false);
}
}}
className="opacity-0 group-hover:opacity-100 focus:opacity-100 p-0.5 text-faint hover:text-status-err rounded-btn transition-opacity shrink-0 cursor-pointer"
title="Block löschen"
>
<X className="w-3.5 h-3.5" />
<Trash2 className="w-3.5 h-3.5" />
</button>
)}