feat(workspace): add help page, folder drag and drop, pinned notes section, and clean up editor UX
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 52s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 52s
This commit is contained in:
1 parent
fe1d574787
commit
fb805b30a1
5 files changed
+610
-67
No files matched your search
@@ -58,10 +58,19 @@
|
|||||||
- `Shift+Tab`: Block ausrücken.
|
- `Shift+Tab`: Block ausrücken.
|
||||||
- `Shift+Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks.
|
- `Shift+Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks.
|
||||||
- `Backspace`: Auf leerem Block rückt aus oder entfernt den Bullet-Point.
|
- `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.
|
- **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`.
|
- **Slash-Commands (`/`):** Schnelle Umwandlung in `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/table`, `/callout`, `/progress`, `/divider`.
|
||||||
- Physische Speicherung auf Festplatte unter `/data/workspace/<user_id>/...` als Standard-`.md`-Dateien mit portabler 2-Leerzeichen-Einrückung (` - [ ] ` bzw. ` - `), vollständig kompatibel mit externen Outlinern und Markdown-Viewern.
|
- Physische Speicherung auf Festplatte unter `/data/workspace/<user_id>/...` 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`):**
|
- [x] **Globaler Workspace To-Do Rollup Dashboard (`WorkspaceView.tsx`):**
|
||||||
- Aggregierte Übersicht aller To-Dos (`- [ ] ` / `- [x] `) aus sämtlichen Notiz-Dateien über alle Ordner hinweg.
|
- 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.
|
- Übersichtliche Gruppierung nach Ordner ➔ Dokument ➔ Aufgaben mit einklappbaren Akkordeons und Zähler für offene Aufgaben.
|
||||||
|
|||||||
+23
-1
@@ -11,6 +11,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Palette,
|
Palette,
|
||||||
|
HelpCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
||||||
import { api, clearToken, getToken, triggerHaptic } from './lib/api';
|
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 { SetupAndAuthView } from './features/auth/SetupAndAuthView';
|
||||||
import { UserSettingsView } from './features/settings/UserSettingsView';
|
import { UserSettingsView } from './features/settings/UserSettingsView';
|
||||||
import { AdminSettingsView } from './features/settings/AdminSettingsView';
|
import { AdminSettingsView } from './features/settings/AdminSettingsView';
|
||||||
|
import { HelpView } from './features/help/HelpView';
|
||||||
|
|
||||||
type ActiveTab =
|
type ActiveTab =
|
||||||
| 'triage'
|
| 'triage'
|
||||||
@@ -35,7 +37,8 @@ type ActiveTab =
|
|||||||
| 'workspace'
|
| 'workspace'
|
||||||
| 'lists'
|
| 'lists'
|
||||||
| 'user_settings'
|
| 'user_settings'
|
||||||
| 'admin_settings';
|
| 'admin_settings'
|
||||||
|
| 'help';
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
@@ -375,6 +378,20 @@ export const App: React.FC = () => {
|
|||||||
<span>Kontoeinstellungen</span>
|
<span>Kontoeinstellungen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setShowProfileMenu(false);
|
||||||
|
setActiveTab('help');
|
||||||
|
}}
|
||||||
|
className={`w-full px-3.5 py-2.5 text-left flex items-center gap-2.5 hover:bg-panel-elevated transition-colors ${
|
||||||
|
activeTab === 'help' ? 'text-accent font-semibold' : 'text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-4 h-4 text-accent" />
|
||||||
|
<span>Hilfe & Anleitung</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{isStaff && (
|
{isStaff && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -516,9 +533,14 @@ export const App: React.FC = () => {
|
|||||||
<WorkspaceView
|
<WorkspaceView
|
||||||
initialDocId={selectedDocId}
|
initialDocId={selectedDocId}
|
||||||
onStatsChange={refreshAll}
|
onStatsChange={refreshAll}
|
||||||
|
onOpenHelp={() => setActiveTab('help')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'help' && (
|
||||||
|
<HelpView onBack={() => setActiveTab('workspace')} />
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === 'lists' && (
|
{activeTab === 'lists' && (
|
||||||
<SharedListsView onRefreshGlobal={refreshAll} />
|
<SharedListsView onRefreshGlobal={refreshAll} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Search,
|
||||||
|
Layers,
|
||||||
|
FileText,
|
||||||
|
CheckSquare,
|
||||||
|
Keyboard,
|
||||||
|
Pin,
|
||||||
|
FolderTree,
|
||||||
|
Folder,
|
||||||
|
Sparkles,
|
||||||
|
Command,
|
||||||
|
CornerDownLeft,
|
||||||
|
CornerUpLeft,
|
||||||
|
MoveVertical,
|
||||||
|
Check,
|
||||||
|
Share2,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface HelpViewProps {
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelpCategory = 'all' | 'workspace' | 'triage' | 'tasks' | 'shortcuts';
|
||||||
|
|
||||||
|
export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
|
||||||
|
const [activeCategory, setActiveCategory] = useState<HelpCategory>('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-5xl mx-auto space-y-4 pb-12">
|
||||||
|
{/* Header Bar */}
|
||||||
|
<div className="panel-surface p-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBack}
|
||||||
|
className="btn-action text-xs flex items-center gap-1.5"
|
||||||
|
title="Zurück zur vorherigen Ansicht"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
|
<span>Zurück</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-base sm:text-lg font-bold text-main flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-accent" />
|
||||||
|
<span>Hilfe & Dokumentation</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
Alles Wichtige zur Tastaturbedienung, Outliner-Notizen, Triage und Zusammenarbeit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative w-full sm:w-64">
|
||||||
|
<Search className="w-3.5 h-3.5 text-muted absolute left-2.5 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="In Hilfe suchen…"
|
||||||
|
className="w-full h-8 pl-8 pr-3 input-field text-xs bg-canvas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter Pills */}
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
{[
|
||||||
|
{ 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 (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveCategory(cat.id as HelpCategory)}
|
||||||
|
className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-panel-elevated border-accent text-main font-semibold'
|
||||||
|
: 'bg-panel border-border text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>{cat.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Documentation Cards */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* SECTION: WORKSPACE & OUTLINER */}
|
||||||
|
{(activeCategory === 'all' || activeCategory === 'workspace') && (
|
||||||
|
<div className="panel-surface p-5 space-y-4">
|
||||||
|
<div className="border-b border-border pb-2.5">
|
||||||
|
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-accent" />
|
||||||
|
<span>Workspace: Der hierarchische Logseq-Outliner</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted mt-0.5">
|
||||||
|
Jede Notiz in MindSift ist ein flexibler hierarchischer Outliner und wird als Standard-Markdown auf der Festplatte gespeichert.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5 text-xs">
|
||||||
|
<Keyboard className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Flüssige Tastatursteuerung</span>
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5 text-muted leading-relaxed">
|
||||||
|
<li>
|
||||||
|
<strong className="text-main">Enter:</strong> Erzeugt sofort einen neuen Bullet-Point auf derselben Ebene.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong className="text-main">Tab:</strong> Rückt den aktuellen Bullet-Point ein und ordnet ihn dem vorherigen Punkt unter.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong className="text-main">Shift + Tab:</strong> Rückt den Bullet-Point wieder nach links aus (verringert die Tiefe).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong className="text-main">Shift + Enter:</strong> Erzeugt einen weichen Zeilenumbruch innerhalb desselben Textblocks.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong className="text-main">Backspace:</strong> Auf einem leeren Block rückt er die Ebene aus bzw. löscht den leeren Bullet.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5 text-xs">
|
||||||
|
<Command className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Slash-Befehle (/)</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Tippe einfach <code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">/</code> in einen leeren Block, um den Typ flexibel zu wechseln:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-1 text-[11px] font-mono text-muted">
|
||||||
|
<span>/todo (Checkbox-Aufgabe)</span>
|
||||||
|
<span>/h1 (Haupt-Überschrift)</span>
|
||||||
|
<span>/h2 (Abschnitts-Titel)</span>
|
||||||
|
<span>/h3 (Unter-Überschrift)</span>
|
||||||
|
<span>/code (Code-Block)</span>
|
||||||
|
<span>/table (Markdown-Tabelle)</span>
|
||||||
|
<span>/callout (Hinweis-Box)</span>
|
||||||
|
<span>/progress (Fortschritt 0-100%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5 text-xs">
|
||||||
|
<FolderTree className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Ordnerstruktur & Drag & Drop</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Notizen können im linken Datei-Explorer per <strong>Drag & Drop</strong> direkt auf beliebige Ordner gezogen werden, um sie zu verschieben.
|
||||||
|
Ziehst du eine Notiz auf das Hauptverzeichnis, wird sie aus allen Unterordnern herausgenommen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5 text-xs">
|
||||||
|
<Pin className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Zoom-Hoisting & Favoriten anpinnen</span>
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5 text-muted leading-relaxed">
|
||||||
|
<li>
|
||||||
|
<strong>Klick auf den Bullet-Punkt (•):</strong> Fokussiert tief in diesen Ast hinein (Zoom-in Hoisting). Du kannst über die Breadcrumbs ganz oben jederzeit zurückspringen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Bullet-Punkt ziehen:</strong> Verschiebt den Block samt aller eingerückten Unterelemente an eine neue Position.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Anpinnen:</strong> Wichtige Notizen heften sich ganz oben im Datei-Explorer unter „Angepinnte Notizen" an.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SECTION: TRIAGE & QUICK DUMP */}
|
||||||
|
{(activeCategory === 'all' || activeCategory === 'triage') && (
|
||||||
|
<div className="panel-surface p-5 space-y-4">
|
||||||
|
<div className="border-b border-border pb-2.5">
|
||||||
|
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
||||||
|
<Layers className="w-4 h-4 text-accent" />
|
||||||
|
<span>Triage & Quick Dump: Der Kopf frei in 3 Sekunden</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted mt-0.5">
|
||||||
|
Sofortiges Erfassen ohne Reibung. Gedanken werden gesammelt und anschließend im Triage-Deck mit 1 Taste entschieden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-1.5">
|
||||||
|
<h4 className="font-semibold text-main">1. Blitz-Erfassung</h4>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Drücke überall <kbd className="px-1 py-0.5 bg-panel border border-border rounded font-mono text-[10px]">C</kbd> oder <kbd className="px-1 py-0.5 bg-panel border border-border rounded font-mono text-[10px]">/</kbd>, tippe den Gedanken ein und drücke <kbd className="px-1 py-0.5 bg-panel border border-border rounded font-mono text-[10px]">Enter</kbd>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-1.5">
|
||||||
|
<h4 className="font-semibold text-main">2. Triage Entscheidungs-Tasten</h4>
|
||||||
|
<ul className="text-muted space-y-1">
|
||||||
|
<li><strong className="text-status-err">H</strong> oder <strong>←</strong> : Wegwerfen / Erledigt</li>
|
||||||
|
<li><strong className="text-accent">L</strong> oder <strong>→</strong> : Als Aufgabe anlegen</li>
|
||||||
|
<li><strong>T</strong> : Aufgabe mit Fälligkeitsdatum</li>
|
||||||
|
<li><strong>D</strong> : In Workspace-Notiz umwandeln</li>
|
||||||
|
<li><strong>Leertaste</strong> : Später (ans Ende schieben)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-1.5">
|
||||||
|
<h4 className="font-semibold text-main">3. Direkt-Zuordnung</h4>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Über das Dropdown neben der Eingabeleiste kannst du einen Eintrag schon beim Schreiben direkt einer Aufgabenliste, Einkaufsliste oder Notiz zuweisen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SECTION: TASKS & LISTS */}
|
||||||
|
{(activeCategory === 'all' || activeCategory === 'tasks') && (
|
||||||
|
<div className="panel-surface p-5 space-y-4">
|
||||||
|
<div className="border-b border-border pb-2.5">
|
||||||
|
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
||||||
|
<CheckSquare className="w-4 h-4 text-accent" />
|
||||||
|
<span>Aufgaben, Listen & Team-Zusammenarbeit</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted mt-0.5">
|
||||||
|
Verwalte Aufgabenlisten, Einkaufslisten und teile sie mit Teams und Räumen in Echtzeit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5">
|
||||||
|
<Share2 className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Aufgabenlisten teilen & Avatar-Stack</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Jede Aufgabenliste kann über den Button <strong>„Teilen"</strong> 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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-canvas border border-border rounded-card space-y-2">
|
||||||
|
<h3 className="font-semibold text-main flex items-center gap-1.5">
|
||||||
|
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Globale To-Dos Rollup</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted leading-relaxed">
|
||||||
|
Im Workspace-Tab <strong>„Globale To-Dos"</strong> werden automatisch alle offenen Checkboxen (<code className="text-accent bg-panel px-1 py-0.5 rounded font-mono">- [ ]</code>) aus sämtlichen Notizen und Ordnern aggregiert.
|
||||||
|
Ein Klick auf die Checkbox synchronisiert die Aufgabe in Echtzeit direkt in der physischen Markdown-Datei!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SECTION: SHORTCUTS CHEAT SHEET */}
|
||||||
|
{(activeCategory === 'all' || activeCategory === 'shortcuts') && (
|
||||||
|
<div className="panel-surface p-5 space-y-4">
|
||||||
|
<div className="border-b border-border pb-2.5">
|
||||||
|
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
||||||
|
<Keyboard className="w-4 h-4 text-accent" />
|
||||||
|
<span>Tastatur-Kürzel Gesamtübersicht</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted mt-0.5">
|
||||||
|
Schneller arbeiten ohne Maus: Alle Hotkeys auf einen Blick.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-border text-muted">
|
||||||
|
<th className="py-2 px-3 font-semibold">Taste</th>
|
||||||
|
<th className="py-2 px-3 font-semibold">Bereich</th>
|
||||||
|
<th className="py-2 px-3 font-semibold">Funktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border text-main">
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">C oder /</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Global</td>
|
||||||
|
<td className="py-2 px-3">Fokus sofort in die Schnellerfassungsleiste setzen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Enter</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Schnellerfassung</td>
|
||||||
|
<td className="py-2 px-3">Gedanken sofort speichern</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Esc</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Schnellerfassung</td>
|
||||||
|
<td className="py-2 px-3">Eingabe leeren & Menüs schließen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Enter</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Neuen Bullet-Point auf gleicher Ebene anlegen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Tab</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Bullet-Point einrücken (dem vorherigen unterordnen)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Shift + Tab</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Bullet-Point ausrücken (Ebene verringern)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Shift + Enter</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Weicher Zeilenumbruch innerhalb des aktuellen Blocks</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Backspace</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Bei leerem Block: Ebene ausrücken bzw. Bullet löschen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">/</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Workspace Outliner</td>
|
||||||
|
<td className="py-2 px-3">Slash-Command-Menü öffnen (/todo, /h1, /code, ...)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">H oder ←</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Triage-Deck</td>
|
||||||
|
<td className="py-2 px-3">Gedanken verwerfen / erledigen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">L oder →</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Triage-Deck</td>
|
||||||
|
<td className="py-2 px-3">Gedanken sofort zur Aufgabe machen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">T</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Triage-Deck</td>
|
||||||
|
<td className="py-2 px-3">Aufgabe mit Fälligkeitsdatum anlegen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">D</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Triage-Deck</td>
|
||||||
|
<td className="py-2 px-3">Gedanken in Notiz/Dokument umwandeln</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 px-3 font-mono text-accent">Leertaste</td>
|
||||||
|
<td className="py-2 px-3 text-muted">Triage-Deck</td>
|
||||||
|
<td className="py-2 px-3">Karte überspringen („Später" – ans Ende schieben)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
Circle,
|
Circle,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
Plus,
|
Plus,
|
||||||
|
HelpCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { BlockType, Document, DocumentBlock } from '../../lib/types';
|
import { BlockType, Document, DocumentBlock } from '../../lib/types';
|
||||||
import { api, triggerHaptic } from '../../lib/api';
|
import { api, triggerHaptic } from '../../lib/api';
|
||||||
@@ -45,9 +46,10 @@ import { api, triggerHaptic } from '../../lib/api';
|
|||||||
interface WorkspaceViewProps {
|
interface WorkspaceViewProps {
|
||||||
initialDocId?: string | null;
|
initialDocId?: string | null;
|
||||||
onStatsChange: () => void;
|
onStatsChange: () => void;
|
||||||
|
onOpenHelp?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ICONS = ['🚀', '💡', '📄', '🏡', '⚡', '🎯', '🍕', '🛠️', '🧠', '🔥', '🏔️', '📦', '📑', '⚙️', '📊'];
|
const ICONS = ['📄', '📝', '💡', '🚀', '🏡', '⚡', '🎯', '🍕', '🛠️', '🧠', '🔥', '🏔️', '📦', '📑', '⚙️', '📊'];
|
||||||
|
|
||||||
interface FolderTreeItem {
|
interface FolderTreeItem {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -80,6 +82,7 @@ const SLASH_COMMANDS: SlashCommandItem[] = [
|
|||||||
export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||||
initialDocId,
|
initialDocId,
|
||||||
onStatsChange,
|
onStatsChange,
|
||||||
|
onOpenHelp,
|
||||||
}) => {
|
}) => {
|
||||||
// Top Workspace Tab Mode: 'editor' vs 'todos' (Global To-Do Rollup Dashboard)
|
// Top Workspace Tab Mode: 'editor' vs 'todos' (Global To-Do Rollup Dashboard)
|
||||||
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos'>('editor');
|
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos'>('editor');
|
||||||
@@ -105,6 +108,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
||||||
const [newFolderName, setNewFolderName] = useState('');
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
|
|
||||||
|
// Sidebar Document Drag and Drop state
|
||||||
|
const [draggingDocId, setDraggingDocId] = useState<string | null>(null);
|
||||||
|
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||||
|
|
||||||
// Active focused block ref
|
// Active focused block ref
|
||||||
const [lastFocusedBlockId, setLastFocusedBlockId] = useState<string | null>(null);
|
const [lastFocusedBlockId, setLastFocusedBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -116,7 +123,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const [slashSearch, setSlashSearch] = useState('');
|
const [slashSearch, setSlashSearch] = useState('');
|
||||||
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0);
|
const [slashSelectedIndex, setSlashSelectedIndex] = useState(0);
|
||||||
|
|
||||||
// Drag and Drop reordering state
|
// Drag and Drop reordering state for blocks
|
||||||
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
// --- GLOBAL TO-DO ROLLUP STATE ---
|
// --- GLOBAL TO-DO ROLLUP STATE ---
|
||||||
@@ -207,7 +214,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
const created = await api.createDocument({
|
const created = await api.createDocument({
|
||||||
title: 'Neuer Gedanke / Notiz',
|
title: 'Neuer Gedanke / Notiz',
|
||||||
icon: '💡',
|
icon: '📄',
|
||||||
folderPath: path,
|
folderPath: path,
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
@@ -258,6 +265,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
await persistDocument(updatedDoc);
|
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) => {
|
const handleStartCreateFolder = (parentPath?: string | null) => {
|
||||||
setNewFolderParent(parentPath || null);
|
setNewFolderParent(parentPath || null);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
@@ -801,25 +832,41 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return { rootTree: rootFolders, rootDocs: rootOnlyDocs };
|
return { rootTree: rootFolders, rootDocs: rootOnlyDocs };
|
||||||
}, [allFolderPaths, filteredDocs]);
|
}, [allFolderPaths, filteredDocs]);
|
||||||
|
|
||||||
|
const pinnedDocs = useMemo(() => {
|
||||||
|
return docs.filter((d) => d.isPinned);
|
||||||
|
}, [docs]);
|
||||||
|
|
||||||
const renderDocItem = (doc: Document, depth: number) => {
|
const renderDocItem = (doc: Document, depth: number) => {
|
||||||
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
|
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
|
||||||
|
const isDragging = draggingDocId === doc.id;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={doc.id}
|
key={doc.id}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
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={() => {
|
onClick={() => {
|
||||||
setSelectedDoc(doc);
|
setSelectedDoc(doc);
|
||||||
setZoomedBlockId(null);
|
setZoomedBlockId(null);
|
||||||
setWorkspaceMode('editor');
|
setWorkspaceMode('editor');
|
||||||
}}
|
}}
|
||||||
style={{ paddingLeft: `${depth * 14 + 16}px` }}
|
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
|
isSelected
|
||||||
? 'bg-panel-elevated border-accent text-main font-medium'
|
? 'bg-panel-elevated border-accent text-main font-medium'
|
||||||
: 'border-transparent text-muted hover:text-main hover:bg-canvas/50'
|
: 'border-transparent text-muted hover:text-main hover:bg-canvas/50'
|
||||||
}`}
|
}`}
|
||||||
|
title={`${doc.title || 'Notiz'} (Ziehen zum Verschieben in Ordner)`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<span className="text-sm flex-shrink-0">{doc.icon}</span>
|
<span className="text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
||||||
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
||||||
{doc.isPinned && <Pin className="w-2.5 h-2.5 text-accent flex-shrink-0" />}
|
{doc.isPinned && <Pin className="w-2.5 h-2.5 text-accent flex-shrink-0" />}
|
||||||
</div>
|
</div>
|
||||||
@@ -830,14 +877,41 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const renderFolder = (node: FolderTreeItem): React.ReactNode => {
|
const renderFolder = (node: FolderTreeItem): React.ReactNode => {
|
||||||
const isExpanded = expandedFolders.has(node.path);
|
const isExpanded = expandedFolders.has(node.path);
|
||||||
const isCreatingSubfolder = isCreatingFolder && newFolderParent === node.path;
|
const isCreatingSubfolder = isCreatingFolder && newFolderParent === node.path;
|
||||||
|
const isDragOver = dragOverFolder === node.path;
|
||||||
const totalItems = node.docs.length + node.subfolders.length;
|
const totalItems = node.docs.length + node.subfolders.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={node.path} className="select-none">
|
<div
|
||||||
|
key={node.path}
|
||||||
|
className="select-none"
|
||||||
|
onDragOver={(e) => {
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => toggleFolder(node.path)}
|
onClick={() => toggleFolder(node.path)}
|
||||||
style={{ paddingLeft: `${node.depth * 14 + 8}px` }}
|
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'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className="text-faint hover:text-muted">
|
<span className="text-faint hover:text-muted">
|
||||||
@@ -954,7 +1028,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Layers className="w-3.5 h-3.5 text-accent" />
|
<Layers className="w-3.5 h-3.5 text-accent" />
|
||||||
<span>Dokumente & Outliner</span>
|
<span>Notizen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -967,30 +1041,44 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
||||||
<span>Globales Notizen-To-Do Rollup</span>
|
<span>Globale To-Dos</span>
|
||||||
<span className="badge-neutral font-mono text-[10px]">
|
<span className="badge-neutral font-mono text-[10px]">
|
||||||
{allWorkspaceTodos.filter((t) => !t.block.checked).length}
|
{allWorkspaceTodos.filter((t) => !t.block.checked).length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{workspaceMode === 'editor' && selectedDoc && (
|
<div className="flex items-center gap-2 text-xs text-muted">
|
||||||
<div className="flex items-center gap-2 text-xs text-muted">
|
{workspaceMode === 'editor' && selectedDoc && (
|
||||||
<span className="badge-accent">
|
<>
|
||||||
<Check className="w-3 h-3" />
|
<span className="badge-accent">
|
||||||
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'}
|
<Check className="w-3 h-3" />
|
||||||
</span>
|
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleExportMarkdown}
|
||||||
|
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
|
||||||
|
title="Als Logseq/Markdown-Datei (.md) herunterladen"
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Export (.md)</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onOpenHelp && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleExportMarkdown}
|
onClick={onOpenHelp}
|
||||||
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
|
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
|
||||||
title="Als Logseq/Markdown-Datei (.md) herunterladen"
|
title="Hilfe & Dokumentation öffnen"
|
||||||
>
|
>
|
||||||
<Download className="w-3.5 h-3.5 text-accent" />
|
<HelpCircle className="w-3.5 h-3.5 text-accent" />
|
||||||
<span>Export (.md)</span>
|
<span>Hilfe</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===================================================================== */}
|
{/* ===================================================================== */}
|
||||||
@@ -1097,6 +1185,89 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-1 max-h-[calc(100vh-240px)] overflow-y-auto">
|
<div className="py-1 max-h-[calc(100vh-240px)] overflow-y-auto">
|
||||||
|
{/* Drag target for Root when dragging a note */}
|
||||||
|
{draggingDocId && (
|
||||||
|
<div
|
||||||
|
onDragOver={(e) => {
|
||||||
|
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)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Angepinnte Notizen (Favorites) */}
|
||||||
|
{pinnedDocs.length > 0 && !searchQuery && (
|
||||||
|
<div className="mb-2 pb-1.5 border-b border-border">
|
||||||
|
<div className="px-2.5 py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Pin className="w-3 h-3 text-accent" />
|
||||||
|
<span>Angepinnte Notizen ({pinnedDocs.length})</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 px-1">
|
||||||
|
{pinnedDocs.map((doc) => {
|
||||||
|
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`pinned-${doc.id}`}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
|
<span className="text-sm flex-shrink-0">{doc.icon || '📄'}</span>
|
||||||
|
<span className="truncate">{doc.title || 'Unbenannt'}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
persistDocument({ ...doc, isPinned: false });
|
||||||
|
}}
|
||||||
|
className="opacity-0 group-hover:opacity-100 p-0.5 text-muted hover:text-accent rounded transition-opacity"
|
||||||
|
title="Von Angepinnt entfernen"
|
||||||
|
>
|
||||||
|
<Pin className="w-3 h-3 text-accent" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{rootTree.map(renderFolder)}
|
{rootTree.map(renderFolder)}
|
||||||
{rootDocs.map((doc) => renderDocItem(doc, 0))}
|
{rootDocs.map((doc) => renderDocItem(doc, 0))}
|
||||||
{filteredDocs.length === 0 && searchQuery && (
|
{filteredDocs.length === 0 && searchQuery && (
|
||||||
@@ -1117,18 +1288,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Folder className="w-3.5 h-3.5 text-faint" />
|
<Folder className="w-3.5 h-3.5 text-faint" />
|
||||||
<select
|
<span>{selectedDoc.folderPath || 'Hauptverzeichnis'}</span>
|
||||||
value={getDocFolder(selectedDoc)}
|
|
||||||
onChange={(e) => handleChangeDocFolder(e.target.value)}
|
|
||||||
className="input-field px-2 py-0.5 text-xs bg-canvas"
|
|
||||||
>
|
|
||||||
<option value="">(Kein Ordner / Root)</option>
|
|
||||||
{allFolderPaths.map((fp) => (
|
|
||||||
<option key={fp} value={fp}>
|
|
||||||
{fp}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>• {selectedDoc.blocks.length} Bullets</span>
|
<span>• {selectedDoc.blocks.length} Bullets</span>
|
||||||
@@ -1159,14 +1319,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* ZOOM BREADCRUMB BAR (Hoisting Focus Mode) */}
|
{/* ZOOM BREADCRUMB BAR (Hoisting Focus Mode) */}
|
||||||
{zoomedBlock && (
|
{zoomedBlock && (
|
||||||
<div className="p-2.5 bg-canvas border border-accent/40 rounded-card flex items-center justify-between gap-2 text-xs">
|
<div className="p-2.5 bg-canvas border border-border rounded-card flex items-center gap-2 text-xs">
|
||||||
<div className="flex flex-wrap items-center gap-1.5 min-w-0">
|
<div className="flex flex-wrap items-center gap-1.5 min-w-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setZoomedBlockId(null)}
|
onClick={() => setZoomedBlockId(null)}
|
||||||
className="text-accent hover:underline flex items-center gap-1 font-medium"
|
className="text-muted hover:text-main flex items-center gap-1 font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<span>{selectedDoc.icon}</span>
|
<span>{selectedDoc.icon || '📄'}</span>
|
||||||
<span className="truncate">{selectedDoc.title}</span>
|
<span className="truncate">{selectedDoc.title}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -1176,7 +1336,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setZoomedBlockId(b.id)}
|
onClick={() => setZoomedBlockId(b.id)}
|
||||||
className="text-muted hover:text-main truncate max-w-[140px]"
|
className="text-muted hover:text-main truncate max-w-[140px] font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{b.content || 'Bullet'}
|
{b.content || 'Bullet'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1184,18 +1344,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<ChevronRight className="w-3 h-3 text-faint flex-shrink-0" />
|
<ChevronRight className="w-3 h-3 text-faint flex-shrink-0" />
|
||||||
<span className="text-main font-semibold truncate max-w-[180px]">
|
<span className="text-muted font-medium truncate max-w-[180px]">
|
||||||
{zoomedBlock.content || 'Aktiver Ast'}
|
{zoomedBlock.content || 'Aktiver Ast'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setZoomedBlockId(null)}
|
|
||||||
className="btn-action text-xs px-2 py-0.5 flex-shrink-0"
|
|
||||||
>
|
|
||||||
← Zoom aufheben
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1205,14 +1357,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowIconPicker((prev) => !prev)}
|
onClick={() => setShowIconPicker((prev) => !prev)}
|
||||||
className="w-10 h-10 panel-surface hover:bg-panel-elevated text-xl flex items-center justify-center flex-shrink-0 transition-colors rounded-card"
|
className="text-2xl p-0.5 hover:scale-110 flex items-center justify-center flex-shrink-0 transition-transform cursor-pointer"
|
||||||
title="Icon ändern"
|
title="Icon ändern"
|
||||||
>
|
>
|
||||||
{selectedDoc.icon}
|
{selectedDoc.icon || '📄'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showIconPicker && (
|
{showIconPicker && (
|
||||||
<div className="absolute top-12 left-0 z-20 p-2.5 panel-surface flex flex-wrap gap-1 w-64 shadow-panel border border-border rounded-card">
|
<div className="absolute top-10 left-0 z-20 p-2.5 panel-surface flex flex-wrap gap-1 w-64 shadow-panel border border-border rounded-card">
|
||||||
{ICONS.map((ic) => (
|
{ICONS.map((ic) => (
|
||||||
<button
|
<button
|
||||||
key={ic}
|
key={ic}
|
||||||
@@ -1249,17 +1401,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick Hint Bar for Logseq Outliner Controls */}
|
|
||||||
<div className="flex flex-wrap items-center justify-between text-[11px] text-faint px-1 pt-0.5">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span><kbd className="px-1 py-0.2 bg-canvas border border-border rounded text-[10px]">Enter</kbd> Neuer Bullet</span>
|
|
||||||
<span><kbd className="px-1 py-0.2 bg-canvas border border-border rounded text-[10px]">Tab</kbd> Einrücken</span>
|
|
||||||
<span><kbd className="px-1 py-0.2 bg-canvas border border-border rounded text-[10px]">Shift+Tab</kbd> Ausrücken</span>
|
|
||||||
<span><kbd className="px-1 py-0.2 bg-canvas border border-border rounded text-[10px]">/</kbd> Slash-Befehle</span>
|
|
||||||
</div>
|
|
||||||
<span>Dot ziehen = Verschieben · Klick = Reinhoomen</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* =========================================================== */}
|
{/* =========================================================== */}
|
||||||
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
|
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
|
||||||
{/* =========================================================== */}
|
{/* =========================================================== */}
|
||||||
@@ -1574,11 +1715,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
<h2 className="text-sm font-bold text-main flex items-center gap-2">
|
||||||
<CheckSquare className="w-4 h-4 text-accent" />
|
<CheckSquare className="w-4 h-4 text-accent" />
|
||||||
<span>Globales Notizen-To-Do Rollup</span>
|
<span>Globale To-Dos</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-muted mt-0.5">
|
|
||||||
Alle Aufgaben aus sämtlichen Markdown-Dateien und Ordnern auf einen Blick. Abhaken synchronisiert direkt zurück in die jeweilige Datei.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -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: "h2", Content: "Nächste Schritte"},
|
||||||
{ID: domain.NewID("blk"), Type: "todo", Content: "Ersten Meilenstein definieren", Checked: false},
|
{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 {
|
if err == nil && doc != nil {
|
||||||
convertedRefID = doc.ID
|
convertedRefID = doc.ID
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user