From fb805b30a1471baf081997f5efcba27ba9fc9c54 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 25 Oct 2026 01:08:17 +0200 Subject: [PATCH] feat(workspace): add help page, folder drag and drop, pinned notes section, and clean up editor UX --- BRAIN.md | 11 +- frontend/src/App.tsx | 24 +- frontend/src/features/help/HelpView.tsx | 374 ++++++++++++++++++ .../src/features/workspace/WorkspaceView.tsx | 266 ++++++++++--- internal/storage/sqlite.go | 2 +- 5 files changed, 610 insertions(+), 67 deletions(-) create mode 100644 frontend/src/features/help/HelpView.tsx diff --git a/BRAIN.md b/BRAIN.md index 41ced99..facbfd1 100644 --- a/BRAIN.md +++ b/BRAIN.md @@ -58,10 +58,19 @@ - `Shift+Tab`: Block ausrücken. - `Shift+Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks. - `Backspace`: Auf leerem Block rückt aus oder entfernt den Bullet-Point. - - **Zoom-in Hoisting:** Klick auf den Bullet-Dot zoomt tief in diesen Ast hinein (Fokusmodus) mit Breadcrumbs-Navigation und „Zoom aufheben"-Button. + - **Zoom-in Hoisting:** Klick auf den Bullet-Dot zoomt tief in diesen Ast hinein (Fokusmodus) mit Breadcrumbs-Navigation (zurückspringen per Klick auf die grauen Breadcrumbs). - **Drag & Drop:** Bullet-Dots dienen als Anfasser zum Verschieben und Umsortieren von Blöcken und ganzen Unterbäumen. - **Slash-Commands (`/`):** Schnelle Umwandlung in `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/table`, `/callout`, `/progress`, `/divider`. - Physische Speicherung auf Festplatte unter `/data/workspace//...` als Standard-`.md`-Dateien mit portabler 2-Leerzeichen-Einrückung (` - [ ] ` bzw. ` - `), vollständig kompatibel mit externen Outlinern und Markdown-Viewern. +- [x] **Zentrale Hilfeseite & Dokumentation (`HelpView.tsx`, `App.tsx`):** + - Vollständige, übersichtliche Hilfe- und Tastatur-Dokumentation für Outliner, Triage, Aufgaben, geteilte Listen und Tastatur-Shortcuts. + - Direkt erreichbar über das Benutzer-Profil-Menü („Hilfe & Anleitung") sowie den „Hilfe"-Button im Workspace-Header. +- [x] **Workspace UX-Refinements & Datei-Explorer Drag & Drop (`WorkspaceView.tsx`, `sqlite.go`):** + - **Dateien in Ordner ziehen:** Notizen können im Datei-Explorer per Drag & Drop direkt auf beliebige Ordner oder in das Hauptverzeichnis gezogen werden. Das alte Ordner-Dropdown im Menü wurde entfernt. + - **Angepinnte Notizen (Favoriten):** Dokumente mit `isPinned: true` werden direkt ganz oben im Datei-Explorer in einer eigenen Schnellzugriffs-Sektion dargestellt. + - **Bereinigter Editor:** Störende Tasten-Legende unter den Notizen entfernt; klobiger Rahmen um das Notiz-Icon entfernt; Standard-Icon ist der Zettel (`📄`). + - **Breadcrumbs & Zoom:** Überflüssiger „Zoom aufheben"-Button entfernt; alle Breadcrumbs-Elemente inklusive Dokumententitel erscheinen einheitlich dezent in Grau ohne störende Link-Unterstreichung. + - **Globale To-Dos:** Tab- und Bereichs-Titel auf prägnantes „Globale To-Dos" gekürzt, unnötiger Erklärungstext entfernt. - [x] **Globaler Workspace To-Do Rollup Dashboard (`WorkspaceView.tsx`):** - Aggregierte Übersicht aller To-Dos (`- [ ] ` / `- [x] `) aus sämtlichen Notiz-Dateien über alle Ordner hinweg. - Übersichtliche Gruppierung nach Ordner ➔ Dokument ➔ Aufgaben mit einklappbaren Akkordeons und Zähler für offene Aufgaben. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 493114c..e0f7913 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import { ChevronDown, RotateCcw, Palette, + HelpCircle, } from 'lucide-react'; import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types'; import { api, clearToken, getToken, triggerHaptic } from './lib/api'; @@ -28,6 +29,7 @@ import { SharedListsView } from './features/lists/SharedListsView'; import { SetupAndAuthView } from './features/auth/SetupAndAuthView'; import { UserSettingsView } from './features/settings/UserSettingsView'; import { AdminSettingsView } from './features/settings/AdminSettingsView'; +import { HelpView } from './features/help/HelpView'; type ActiveTab = | 'triage' @@ -35,7 +37,8 @@ type ActiveTab = | 'workspace' | 'lists' | 'user_settings' - | 'admin_settings'; + | 'admin_settings' + | 'help'; interface ErrorBoundaryState { hasError: boolean; @@ -375,6 +378,20 @@ export const App: React.FC = () => { Kontoeinstellungen + + {isStaff && ( + +
+

+ + Hilfe & Dokumentation +

+

+ Alles Wichtige zur Tastaturbedienung, Outliner-Notizen, Triage und Zusammenarbeit +

+
+ + + {/* Search */} +
+ + setSearchQuery(e.target.value)} + placeholder="In Hilfe suchen…" + className="w-full h-8 pl-8 pr-3 input-field text-xs bg-canvas" + /> +
+ + + {/* Category Filter Pills */} +
+ {[ + { id: 'all', label: 'Alle Themen', icon: Sparkles }, + { id: 'workspace', label: 'Workspace & Outliner', icon: FileText }, + { id: 'triage', label: 'Triage & Quick-Dump', icon: Layers }, + { id: 'tasks', label: 'Aufgaben & Listen', icon: CheckSquare }, + { id: 'shortcuts', label: 'Tastatur-Kürzel', icon: Keyboard }, + ].map((cat) => { + const Icon = cat.icon; + const isActive = activeCategory === cat.id; + return ( + + ); + })} +
+ + {/* Grid of Documentation Cards */} +
+ {/* SECTION: WORKSPACE & OUTLINER */} + {(activeCategory === 'all' || activeCategory === 'workspace') && ( +
+
+

+ + Workspace: Der hierarchische Logseq-Outliner +

+

+ Jede Notiz in MindSift ist ein flexibler hierarchischer Outliner und wird als Standard-Markdown auf der Festplatte gespeichert. +

+
+ +
+
+

+ + Flüssige Tastatursteuerung +

+
    +
  • + Enter: Erzeugt sofort einen neuen Bullet-Point auf derselben Ebene. +
  • +
  • + Tab: Rückt den aktuellen Bullet-Point ein und ordnet ihn dem vorherigen Punkt unter. +
  • +
  • + Shift + Tab: Rückt den Bullet-Point wieder nach links aus (verringert die Tiefe). +
  • +
  • + Shift + Enter: Erzeugt einen weichen Zeilenumbruch innerhalb desselben Textblocks. +
  • +
  • + Backspace: Auf einem leeren Block rückt er die Ebene aus bzw. löscht den leeren Bullet. +
  • +
+
+ +
+

+ + Slash-Befehle (/) +

+

+ Tippe einfach / in einen leeren Block, um den Typ flexibel zu wechseln: +

+
+ /todo (Checkbox-Aufgabe) + /h1 (Haupt-Überschrift) + /h2 (Abschnitts-Titel) + /h3 (Unter-Überschrift) + /code (Code-Block) + /table (Markdown-Tabelle) + /callout (Hinweis-Box) + /progress (Fortschritt 0-100%) +
+
+ +
+

+ + Ordnerstruktur & Drag & Drop +

+

+ Notizen können im linken Datei-Explorer per Drag & Drop direkt auf beliebige Ordner gezogen werden, um sie zu verschieben. + Ziehst du eine Notiz auf das Hauptverzeichnis, wird sie aus allen Unterordnern herausgenommen. +

+
+ +
+

+ + Zoom-Hoisting & Favoriten anpinnen +

+
    +
  • + Klick auf den Bullet-Punkt (•): Fokussiert tief in diesen Ast hinein (Zoom-in Hoisting). Du kannst über die Breadcrumbs ganz oben jederzeit zurückspringen. +
  • +
  • + Bullet-Punkt ziehen: Verschiebt den Block samt aller eingerückten Unterelemente an eine neue Position. +
  • +
  • + Anpinnen: Wichtige Notizen heften sich ganz oben im Datei-Explorer unter „Angepinnte Notizen" an. +
  • +
+
+
+
+ )} + + {/* SECTION: TRIAGE & QUICK DUMP */} + {(activeCategory === 'all' || activeCategory === 'triage') && ( +
+
+

+ + Triage & Quick Dump: Der Kopf frei in 3 Sekunden +

+

+ Sofortiges Erfassen ohne Reibung. Gedanken werden gesammelt und anschließend im Triage-Deck mit 1 Taste entschieden. +

+
+ +
+
+

1. Blitz-Erfassung

+

+ Drücke überall C oder /, tippe den Gedanken ein und drücke Enter. +

+
+ +
+

2. Triage Entscheidungs-Tasten

+
    +
  • H oder ← : Wegwerfen / Erledigt
  • +
  • L oder → : Als Aufgabe anlegen
  • +
  • T : Aufgabe mit Fälligkeitsdatum
  • +
  • D : In Workspace-Notiz umwandeln
  • +
  • Leertaste : Später (ans Ende schieben)
  • +
+
+ +
+

3. Direkt-Zuordnung

+

+ Über das Dropdown neben der Eingabeleiste kannst du einen Eintrag schon beim Schreiben direkt einer Aufgabenliste, Einkaufsliste oder Notiz zuweisen. +

+
+
+
+ )} + + {/* SECTION: TASKS & LISTS */} + {(activeCategory === 'all' || activeCategory === 'tasks') && ( +
+
+

+ + Aufgaben, Listen & Team-Zusammenarbeit +

+

+ Verwalte Aufgabenlisten, Einkaufslisten und teile sie mit Teams und Räumen in Echtzeit. +

+
+ +
+
+

+ + Aufgabenlisten teilen & Avatar-Stack +

+

+ Jede Aufgabenliste kann über den Button „Teilen" einem gemeinsamen Team-Space zugeordnet werden. + Im Header und auf den Tabs zeigt der Avatar-Stack sofort an, wer Eigentümer ist und welche Teammitglieder Zugriff haben. +

+
+ +
+

+ + Globale To-Dos Rollup +

+

+ Im Workspace-Tab „Globale To-Dos" werden automatisch alle offenen Checkboxen (- [ ]) aus sämtlichen Notizen und Ordnern aggregiert. + Ein Klick auf die Checkbox synchronisiert die Aufgabe in Echtzeit direkt in der physischen Markdown-Datei! +

+
+
+
+ )} + + {/* SECTION: SHORTCUTS CHEAT SHEET */} + {(activeCategory === 'all' || activeCategory === 'shortcuts') && ( +
+
+

+ + Tastatur-Kürzel Gesamtübersicht +

+

+ Schneller arbeiten ohne Maus: Alle Hotkeys auf einen Blick. +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TasteBereichFunktion
C oder /GlobalFokus sofort in die Schnellerfassungsleiste setzen
EnterSchnellerfassungGedanken sofort speichern
EscSchnellerfassungEingabe leeren & Menüs schließen
EnterWorkspace OutlinerNeuen Bullet-Point auf gleicher Ebene anlegen
TabWorkspace OutlinerBullet-Point einrücken (dem vorherigen unterordnen)
Shift + TabWorkspace OutlinerBullet-Point ausrücken (Ebene verringern)
Shift + EnterWorkspace OutlinerWeicher Zeilenumbruch innerhalb des aktuellen Blocks
BackspaceWorkspace OutlinerBei leerem Block: Ebene ausrücken bzw. Bullet löschen
/Workspace OutlinerSlash-Command-Menü öffnen (/todo, /h1, /code, ...)
H oder ←Triage-DeckGedanken verwerfen / erledigen
L oder →Triage-DeckGedanken sofort zur Aufgabe machen
TTriage-DeckAufgabe mit Fälligkeitsdatum anlegen
DTriage-DeckGedanken in Notiz/Dokument umwandeln
LeertasteTriage-DeckKarte überspringen („Später" – ans Ende schieben)
+
+
+ )} +
+ + ); +}; diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index a3633aa..8c7c2b6 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -38,6 +38,7 @@ import { Circle, FolderTree, Plus, + HelpCircle, } from 'lucide-react'; import { BlockType, Document, DocumentBlock } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; @@ -45,9 +46,10 @@ import { api, triggerHaptic } from '../../lib/api'; interface WorkspaceViewProps { initialDocId?: string | null; onStatsChange: () => void; + onOpenHelp?: () => void; } -const ICONS = ['🚀', '💡', '📄', '🏡', '⚡', '🎯', '🍕', '🛠️', '🧠', '🔥', '🏔️', '📦', '📑', '⚙️', '📊']; +const ICONS = ['📄', '📝', '💡', '🚀', '🏡', '⚡', '🎯', '🍕', '🛠️', '🧠', '🔥', '🏔️', '📦', '📑', '⚙️', '📊']; interface FolderTreeItem { path: string; @@ -80,6 +82,7 @@ const SLASH_COMMANDS: SlashCommandItem[] = [ export const WorkspaceView: React.FC = ({ initialDocId, onStatsChange, + onOpenHelp, }) => { // Top Workspace Tab Mode: 'editor' vs 'todos' (Global To-Do Rollup Dashboard) const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos'>('editor'); @@ -105,6 +108,10 @@ export const WorkspaceView: React.FC = ({ const [newFolderParent, setNewFolderParent] = useState(null); const [newFolderName, setNewFolderName] = useState(''); + // Sidebar Document Drag and Drop state + const [draggingDocId, setDraggingDocId] = useState(null); + const [dragOverFolder, setDragOverFolder] = useState(null); + // Active focused block ref const [lastFocusedBlockId, setLastFocusedBlockId] = useState(null); @@ -116,7 +123,7 @@ export const WorkspaceView: React.FC = ({ const [slashSearch, setSlashSearch] = useState(''); const [slashSelectedIndex, setSlashSelectedIndex] = useState(0); - // Drag and Drop reordering state + // Drag and Drop reordering state for blocks const [draggingBlockId, setDraggingBlockId] = useState(null); // --- GLOBAL TO-DO ROLLUP STATE --- @@ -207,7 +214,7 @@ export const WorkspaceView: React.FC = ({ } const created = await api.createDocument({ title: 'Neuer Gedanke / Notiz', - icon: '💡', + icon: '📄', folderPath: path, blocks: [ { @@ -258,6 +265,30 @@ export const WorkspaceView: React.FC = ({ await persistDocument(updatedDoc); }; + const handleDropDocToFolder = async (folderPath: string, e: React.DragEvent) => { + const docId = e.dataTransfer.getData('application/mindsift-doc-id') || draggingDocId; + if (!docId) return; + const doc = docs.find((d) => d.id === docId); + if (!doc) return; + const cleanPath = folderPath.trim(); + if (getDocFolder(doc) === cleanPath) return; + + triggerHaptic(15); + const updatedTags = (doc.tags || []).filter((t) => !t.startsWith('folder:')); + if (cleanPath) { + updatedTags.push(`folder:${cleanPath}`); + } + const updatedDoc: Document = { + ...doc, + folderPath: cleanPath, + tags: updatedTags, + }; + await persistDocument(updatedDoc); + if (cleanPath) { + setExpandedFolders((prev) => new Set([...prev, cleanPath])); + } + }; + const handleStartCreateFolder = (parentPath?: string | null) => { setNewFolderParent(parentPath || null); setNewFolderName(''); @@ -801,25 +832,41 @@ export const WorkspaceView: React.FC = ({ return { rootTree: rootFolders, rootDocs: rootOnlyDocs }; }, [allFolderPaths, filteredDocs]); + const pinnedDocs = useMemo(() => { + return docs.filter((d) => d.isPinned); + }, [docs]); + const renderDocItem = (doc: Document, depth: number) => { const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; + const isDragging = draggingDocId === doc.id; return (
{ + e.stopPropagation(); + e.dataTransfer.setData('application/mindsift-doc-id', doc.id); + e.dataTransfer.setData('text/plain', doc.id); + setDraggingDocId(doc.id); + }} + onDragEnd={() => setDraggingDocId(null)} onClick={() => { setSelectedDoc(doc); setZoomedBlockId(null); setWorkspaceMode('editor'); }} style={{ paddingLeft: `${depth * 14 + 16}px` }} - className={`group flex items-center justify-between py-1.5 pr-2 border-l-2 transition-colors cursor-pointer text-xs ${ + className={`group flex items-center justify-between py-1.5 pr-2 border-l-2 transition-all cursor-pointer text-xs ${ + isDragging ? 'opacity-40 scale-[0.98]' : '' + } ${ isSelected ? 'bg-panel-elevated border-accent text-main font-medium' : 'border-transparent text-muted hover:text-main hover:bg-canvas/50' }`} + title={`${doc.title || 'Notiz'} (Ziehen zum Verschieben in Ordner)`} >
- {doc.icon} + {doc.icon || '📄'} {doc.title || 'Unbenannt'} {doc.isPinned && }
@@ -830,14 +877,41 @@ export const WorkspaceView: React.FC = ({ const renderFolder = (node: FolderTreeItem): React.ReactNode => { const isExpanded = expandedFolders.has(node.path); const isCreatingSubfolder = isCreatingFolder && newFolderParent === node.path; + const isDragOver = dragOverFolder === node.path; const totalItems = node.docs.length + node.subfolders.length; return ( -
+
{ + if (draggingDocId) { + e.preventDefault(); + e.stopPropagation(); + setDragOverFolder(node.path); + } + }} + onDragLeave={(e) => { + e.preventDefault(); + if (dragOverFolder === node.path) { + setDragOverFolder(null); + } + }} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDragOverFolder(null); + handleDropDocToFolder(node.path, e); + }} + >
toggleFolder(node.path)} style={{ paddingLeft: `${node.depth * 14 + 8}px` }} - className="group flex items-center justify-between py-1.5 pr-2 hover:bg-panel-elevated cursor-pointer transition-colors text-xs text-muted" + className={`group flex items-center justify-between py-1.5 pr-2 cursor-pointer transition-colors text-xs ${ + isDragOver + ? 'bg-accent/20 border-l-2 border-accent text-main font-medium' + : 'hover:bg-panel-elevated text-muted' + }`} >
@@ -954,7 +1028,7 @@ export const WorkspaceView: React.FC = ({ }`} > - Dokumente & Outliner + Notizen
- {workspaceMode === 'editor' && selectedDoc && ( -
- - - {saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} - +
+ {workspaceMode === 'editor' && selectedDoc && ( + <> + + + {saveState === 'saving' ? 'Speichert…' : 'Gespeichert'} + + + + )} + + {onOpenHelp && ( -
- )} + )} +
{/* ===================================================================== */} @@ -1097,6 +1185,89 @@ export const WorkspaceView: React.FC = ({
) : (
+ {/* Drag target for Root when dragging a note */} + {draggingDocId && ( +
{ + e.preventDefault(); + e.stopPropagation(); + setDragOverFolder('__root__'); + }} + onDragLeave={(e) => { + e.preventDefault(); + if (dragOverFolder === '__root__') setDragOverFolder(null); + }} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDragOverFolder(null); + handleDropDocToFolder('', e); + }} + className={`mx-2 my-1.5 p-2 border-2 border-dashed rounded-btn text-center text-xs transition-colors ${ + dragOverFolder === '__root__' + ? 'border-accent bg-accent/20 text-main font-semibold' + : 'border-border text-muted bg-canvas' + }`} + > + 📁 In Hauptverzeichnis verschieben (ohne Ordner) +
+ )} + + {/* Angepinnte Notizen (Favorites) */} + {pinnedDocs.length > 0 && !searchQuery && ( +
+
+ + Angepinnte Notizen ({pinnedDocs.length}) +
+
+ {pinnedDocs.map((doc) => { + const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; + return ( +
{ + e.stopPropagation(); + e.dataTransfer.setData('application/mindsift-doc-id', doc.id); + e.dataTransfer.setData('text/plain', doc.id); + setDraggingDocId(doc.id); + }} + onDragEnd={() => setDraggingDocId(null)} + onClick={() => { + setSelectedDoc(doc); + setZoomedBlockId(null); + setWorkspaceMode('editor'); + }} + className={`group flex items-center justify-between px-2 py-1.5 rounded-btn transition-colors cursor-pointer text-xs ${ + isSelected + ? 'bg-panel-elevated border-l-2 border-accent text-main font-medium' + : 'text-muted hover:text-main hover:bg-canvas/50' + }`} + title="Angepinnte Notiz öffnen" + > +
+ {doc.icon || '📄'} + {doc.title || 'Unbenannt'} +
+ +
+ ); + })} +
+
+ )} + {rootTree.map(renderFolder)} {rootDocs.map((doc) => renderDocItem(doc, 0))} {filteredDocs.length === 0 && searchQuery && ( @@ -1117,18 +1288,7 @@ export const WorkspaceView: React.FC = ({
- + {selectedDoc.folderPath || 'Hauptverzeichnis'}
• {selectedDoc.blocks.length} Bullets @@ -1159,14 +1319,14 @@ export const WorkspaceView: React.FC = ({ {/* ZOOM BREADCRUMB BAR (Hoisting Focus Mode) */} {zoomedBlock && ( -
+
@@ -1176,7 +1336,7 @@ export const WorkspaceView: React.FC = ({ @@ -1184,18 +1344,10 @@ export const WorkspaceView: React.FC = ({ ))} - + {zoomedBlock.content || 'Aktiver Ast'}
- -
)} @@ -1205,14 +1357,14 @@ export const WorkspaceView: React.FC = ({ {showIconPicker && ( -
+
{ICONS.map((ic) => (
)} - {/* Quick Hint Bar for Logseq Outliner Controls */} -
-
- Enter Neuer Bullet - Tab Einrücken - Shift+Tab Ausrücken - / Slash-Befehle -
- Dot ziehen = Verschieben · Klick = Reinhoomen -
- {/* =========================================================== */} {/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */} {/* =========================================================== */} @@ -1574,11 +1715,8 @@ export const WorkspaceView: React.FC = ({

- Globales Notizen-To-Do Rollup + Globale To-Dos

-

- Alle Aufgaben aus sämtlichen Markdown-Dateien und Ordnern auf einen Blick. Abhaken synchronisiert direkt zurück in die jeweilige Datei. -

diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index ae9f48b..2e1da3e 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -810,7 +810,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target {ID: domain.NewID("blk"), Type: "h2", Content: "Nächste Schritte"}, {ID: domain.NewID("blk"), Type: "todo", Content: "Ersten Meilenstein definieren", Checked: false}, } - doc, err := s.CreateDocument(userID, "", title, "✨", "", blocks, thought.Tags, false) + doc, err := s.CreateDocument(userID, "", title, "📄", "", blocks, thought.Tags, false) if err == nil && doc != nil { convertedRefID = doc.ID }