feat(workspace): note actions in the workspace bar on desktop, one-row sidebar header
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m5s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m5s
On large screens the editor header is gone: undo/redo, share and the menu render into the workspace tab bar (portal) after avatars, live, shared and an icon-only save status; zoom breadcrumbs move above the title. Phones and tablets keep the editor header. The sidebar header drops its heading and puts search, new folder and new note in one row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
375dda982b
commit
d408e7d307
3 files changed
+252
-216
No files matched your search
@@ -7,6 +7,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Workspace am Desktop (ab 1024 px): Die eigene Kopfleiste der Notiz entfällt. Rückgängig, Wiederholen, Teilen und das Drei-Punkte-Menü stehen oben in der Workspace-Leiste, in fester Reihenfolge: Avatare, Live, Geteilt, Speicherstatus (nur noch als Symbol, Text als Tooltip), dann die Knöpfe. Beim Fokus-Modus stehen die Breadcrumbs über dem Titel. Am Handy und Tablet bleibt die Kopfleiste.
|
||||||
|
- Notizen-Seitenleiste: Überschrift „Notizen & Outliner“ entfernt, Suche und die Knöpfe für neuen Ordner und neue Notiz stehen in einer Zeile.
|
||||||
- Journal: Geöffnet ist es ein hinterlegtes Panel mit Rahmen, „Journal“ ist dessen Überschrift; ein zweiter Klick schließt es und zeigt wieder die vorherige Notiz. Ob das Journal offen war und welche Notiz offen war, bleibt beim Wechsel zwischen den Tabs erhalten (Journal-Tage öffnen sich nicht mehr als einzelne Notiz).
|
- Journal: Geöffnet ist es ein hinterlegtes Panel mit Rahmen, „Journal“ ist dessen Überschrift; ein zweiter Klick schließt es und zeigt wieder die vorherige Notiz. Ob das Journal offen war und welche Notiz offen war, bleibt beim Wechsel zwischen den Tabs erhalten (Journal-Tage öffnen sich nicht mehr als einzelne Notiz).
|
||||||
- Notizen: Beim Einfügen und beim Markdown-Import werden Absätze und Listen unter einer Überschrift eingerückt (H2 unter H1 eine Ebene tiefer), so lassen sich Abschnitte einklappen und fokussieren. Enter am Ende einer Überschrift legt den neuen Punkt als Unterpunkt an; `- `, `* ` oder `+ ` in einem leeren Punkt machen ihn zum normalen Aufzählungspunkt.
|
- Notizen: Beim Einfügen und beim Markdown-Import werden Absätze und Listen unter einer Überschrift eingerückt (H2 unter H1 eine Ebene tiefer), so lassen sich Abschnitte einklappen und fokussieren. Enter am Ende einer Überschrift legt den neuen Punkt als Unterpunkt an; `- `, `* ` oder `+ ` in einem leeren Punkt machen ihn zum normalen Aufzählungspunkt.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo, useSyncExternalStore } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
Pin,
|
Pin,
|
||||||
Users,
|
Users,
|
||||||
@@ -63,6 +64,20 @@ export interface DocumentEditorProps {
|
|||||||
onDocRemoved?: (docId: string) => void;
|
onDocRemoved?: (docId: string) => void;
|
||||||
/** 'journal': one day inside the journal list – no own scrolling, header or backlinks, fixed title */
|
/** 'journal': one day inside the journal list – no own scrolling, header or backlinks, fixed title */
|
||||||
variant?: 'page' | 'journal';
|
variant?: 'page' | 'journal';
|
||||||
|
/** Large screens: the action buttons render into this element (the workspace tab bar) */
|
||||||
|
actionsTarget?: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large screens (lg) put the note's actions into the workspace tab bar and drop the editor header
|
||||||
|
const LARGE_QUERY = '(min-width: 1024px)';
|
||||||
|
function subscribeLarge(onChange: () => void) {
|
||||||
|
if (typeof window === 'undefined' || !window.matchMedia) return () => {};
|
||||||
|
const mq = window.matchMedia(LARGE_QUERY);
|
||||||
|
mq.addEventListener('change', onChange);
|
||||||
|
return () => mq.removeEventListener('change', onChange);
|
||||||
|
}
|
||||||
|
function isLargeScreen() {
|
||||||
|
return typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(LARGE_QUERY).matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { EmojiPickerModal } from './EmojiPickerModal';
|
import { EmojiPickerModal } from './EmojiPickerModal';
|
||||||
@@ -214,8 +229,11 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onDocCreated,
|
onDocCreated,
|
||||||
onDocRemoved,
|
onDocRemoved,
|
||||||
variant = 'page',
|
variant = 'page',
|
||||||
|
actionsTarget,
|
||||||
}) => {
|
}) => {
|
||||||
const inJournal = variant === 'journal';
|
const inJournal = variant === 'journal';
|
||||||
|
const large = useSyncExternalStore(subscribeLarge, isLargeScreen, () => false);
|
||||||
|
const desktopBar = !inJournal && large && Boolean(actionsTarget);
|
||||||
const [doc, setDoc] = useState<Document | null>(initialDoc || null);
|
const [doc, setDoc] = useState<Document | null>(initialDoc || null);
|
||||||
const [title, setTitle] = useState<string>(initialDoc?.title || '');
|
const [title, setTitle] = useState<string>(initialDoc?.title || '');
|
||||||
const [blocks, setBlocks] = useState<DocumentBlock[]>(() =>
|
const [blocks, setBlocks] = useState<DocumentBlock[]>(() =>
|
||||||
@@ -787,14 +805,189 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breadcrumbNav = (
|
||||||
|
<nav
|
||||||
|
aria-label="Breadcrumb Navigation"
|
||||||
|
className="flex items-center flex-wrap gap-1 text-xs text-muted"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setZoomedBlockId(null)}
|
||||||
|
className="hover:text-accent inline-flex items-center gap-1 font-medium transition-colors cursor-pointer"
|
||||||
|
title="Zurück zum Hauptdokument"
|
||||||
|
>
|
||||||
|
<Home className="w-3.5 h-3.5 text-accent shrink-0" />
|
||||||
|
<span className="truncate max-w-[130px]">
|
||||||
|
{title || doc?.title || 'Dokument'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{breadcrumbs.slice(1).map((crumb, idx) => {
|
||||||
|
const isCurrent = idx === breadcrumbs.length - 2;
|
||||||
|
return (
|
||||||
|
<React.Fragment key={crumb.id || idx}>
|
||||||
|
<ChevronRight className="w-3 h-3 text-faint shrink-0" />
|
||||||
|
{isCurrent ? (
|
||||||
|
<span
|
||||||
|
className="font-bold text-accent truncate max-w-[160px]"
|
||||||
|
title={crumb.title}
|
||||||
|
>
|
||||||
|
{crumb.title || 'Punkt'}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setZoomedBlockId(crumb.id)}
|
||||||
|
className="hover:text-main truncate max-w-[120px] transition-colors cursor-pointer"
|
||||||
|
title={`Zoom zu "${crumb.title}"`}
|
||||||
|
>
|
||||||
|
{crumb.title || 'Punkt'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Undo/redo, share and the menu; on large screens they sit in the workspace tab bar instead
|
||||||
|
const actionsBlock = (
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
|
{!inTrash && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleUndo}
|
||||||
|
disabled={!canEdit || !engineState?.canUndo}
|
||||||
|
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
title="Rückgängig (Strg+Z)"
|
||||||
|
aria-label="Rückgängig (Strg+Z)"
|
||||||
|
>
|
||||||
|
<Undo2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRedo}
|
||||||
|
disabled={!canEdit || !engineState?.canRedo}
|
||||||
|
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
title="Wiederholen (Strg+Y)"
|
||||||
|
aria-label="Wiederholen (Strg+Y)"
|
||||||
|
>
|
||||||
|
<Redo2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{doc && !inTrash && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onOpenShareModal(doc)}
|
||||||
|
className="btn-icon h-11 w-11 sm:h-7 sm:w-7"
|
||||||
|
title="Notiz teilen und Berechtigungen"
|
||||||
|
aria-label="Notiz teilen und Berechtigungen"
|
||||||
|
>
|
||||||
|
<Users className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative" ref={actionsMenuRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowActionsMenu((prev) => !prev)}
|
||||||
|
aria-expanded={showActionsMenu}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Weitere Aktionen"
|
||||||
|
title="Weitere Aktionen"
|
||||||
|
className="btn-icon h-11 w-11 sm:h-7 sm:w-7"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showActionsMenu && (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
className="absolute right-0 top-full mt-1.5 w-60 rounded-card bg-panel border border-border shadow-lg p-1 z-30 text-xs"
|
||||||
|
>
|
||||||
|
{!inTrash && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setShowActionsMenu(false);
|
||||||
|
void handleTogglePin();
|
||||||
|
}}
|
||||||
|
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Pin
|
||||||
|
className={`w-3.5 h-3.5 shrink-0 ${
|
||||||
|
doc?.isPinned ? 'text-status-warn' : 'text-accent'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span>{doc?.isPinned ? 'Anheftung lösen' : 'Notiz anheften'}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onRequestMerge && doc?.isOwner && !inTrash && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setShowActionsMenu(false);
|
||||||
|
onRequestMerge(doc);
|
||||||
|
}}
|
||||||
|
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<GitMerge className="w-3.5 h-3.5 text-accent shrink-0" />
|
||||||
|
<span>Mit anderer Notiz zusammenführen…</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setShowActionsMenu(false);
|
||||||
|
handleExportMarkdown();
|
||||||
|
}}
|
||||||
|
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5 text-accent shrink-0" />
|
||||||
|
<span>Als Markdown (.md) exportieren</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{canEdit && (doc?.isOwner || doc?.permission === 'owner') && (
|
||||||
|
<>
|
||||||
|
<div className="my-1 border-t border-border" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setShowActionsMenu(false);
|
||||||
|
onDeleteDoc(docId);
|
||||||
|
}}
|
||||||
|
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-status-err/10 text-left text-status-err font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5 text-status-err shrink-0" />
|
||||||
|
<span>Notiz löschen</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className={inJournal ? 'flex flex-col bg-editor' : 'flex-1 flex flex-col h-full min-h-0 bg-editor overflow-y-auto'}
|
className={inJournal ? 'flex flex-col bg-editor' : 'flex-1 flex flex-col h-full min-h-0 bg-editor overflow-y-auto'}
|
||||||
data-sync-channel={engineState?.channel}
|
data-sync-channel={engineState?.channel}
|
||||||
>
|
>
|
||||||
{/* Top Document Action Bar (the journal has one for all days) */}
|
{desktopBar && actionsTarget && createPortal(actionsBlock, actionsTarget)}
|
||||||
{!inJournal && (
|
|
||||||
|
{/* Top Document Action Bar (the journal has one for all days; large screens use the tab bar) */}
|
||||||
|
{!inJournal && !desktopBar && (
|
||||||
<div className="sticky top-0 z-20 flex items-center justify-between gap-2 px-4 sm:px-6 py-2 bg-editor border-b border-border">
|
<div className="sticky top-0 z-20 flex items-center justify-between gap-2 px-4 sm:px-6 py-2 bg-editor border-b border-border">
|
||||||
<div className="flex flex-1 items-center gap-2 min-w-0">
|
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||||
{onBackMobile && (
|
{onBackMobile && (
|
||||||
@@ -810,48 +1003,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
|
|
||||||
{/* Breadcrumbs Navigation (Fokus-Modus) or Doc Indicator */}
|
{/* Breadcrumbs Navigation (Fokus-Modus) or Doc Indicator */}
|
||||||
{zoomedBlock && breadcrumbs.length > 0 ? (
|
{zoomedBlock && breadcrumbs.length > 0 ? (
|
||||||
<nav
|
breadcrumbNav
|
||||||
aria-label="Breadcrumb Navigation"
|
|
||||||
className="flex items-center flex-wrap gap-1 text-xs text-muted"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setZoomedBlockId(null)}
|
|
||||||
className="hover:text-accent inline-flex items-center gap-1 font-medium transition-colors cursor-pointer"
|
|
||||||
title="Zurück zum Hauptdokument"
|
|
||||||
>
|
|
||||||
<Home className="w-3.5 h-3.5 text-accent shrink-0" />
|
|
||||||
<span className="truncate max-w-[130px]">
|
|
||||||
{title || doc?.title || 'Dokument'}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{breadcrumbs.slice(1).map((crumb, idx) => {
|
|
||||||
const isCurrent = idx === breadcrumbs.length - 2;
|
|
||||||
return (
|
|
||||||
<React.Fragment key={crumb.id || idx}>
|
|
||||||
<ChevronRight className="w-3 h-3 text-faint shrink-0" />
|
|
||||||
{isCurrent ? (
|
|
||||||
<span
|
|
||||||
className="font-bold text-accent truncate max-w-[160px]"
|
|
||||||
title={crumb.title}
|
|
||||||
>
|
|
||||||
{crumb.title || 'Punkt'}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setZoomedBlockId(crumb.id)}
|
|
||||||
className="hover:text-main truncate max-w-[120px] transition-colors cursor-pointer"
|
|
||||||
title={`Zoom zu "${crumb.title}"`}
|
|
||||||
>
|
|
||||||
{crumb.title || 'Punkt'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`flex min-w-0 items-center gap-1.5 text-xs text-muted transition-opacity duration-200 ${
|
className={`flex min-w-0 items-center gap-1.5 text-xs text-muted transition-opacity duration-200 ${
|
||||||
@@ -902,131 +1054,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Action Buttons */}
|
{!desktopBar && actionsBlock}
|
||||||
<div className="flex shrink-0 items-center gap-1.5">
|
|
||||||
{!inTrash && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleUndo}
|
|
||||||
disabled={!canEdit || !engineState?.canUndo}
|
|
||||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
||||||
title="Rückgängig (Strg+Z)"
|
|
||||||
aria-label="Rückgängig (Strg+Z)"
|
|
||||||
>
|
|
||||||
<Undo2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleRedo}
|
|
||||||
disabled={!canEdit || !engineState?.canRedo}
|
|
||||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
||||||
title="Wiederholen (Strg+Y)"
|
|
||||||
aria-label="Wiederholen (Strg+Y)"
|
|
||||||
>
|
|
||||||
<Redo2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{doc && !inTrash && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onOpenShareModal(doc)}
|
|
||||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7"
|
|
||||||
title="Notiz teilen und Berechtigungen"
|
|
||||||
aria-label="Notiz teilen und Berechtigungen"
|
|
||||||
>
|
|
||||||
<Users className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative" ref={actionsMenuRef}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowActionsMenu((prev) => !prev)}
|
|
||||||
aria-expanded={showActionsMenu}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-label="Weitere Aktionen"
|
|
||||||
title="Weitere Aktionen"
|
|
||||||
className="btn-icon h-11 w-11 sm:h-7 sm:w-7"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showActionsMenu && (
|
|
||||||
<div
|
|
||||||
role="menu"
|
|
||||||
className="absolute right-0 top-full mt-1.5 w-60 rounded-card bg-panel border border-border shadow-lg p-1 z-30 text-xs"
|
|
||||||
>
|
|
||||||
{!inTrash && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
onClick={() => {
|
|
||||||
setShowActionsMenu(false);
|
|
||||||
void handleTogglePin();
|
|
||||||
}}
|
|
||||||
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Pin
|
|
||||||
className={`w-3.5 h-3.5 shrink-0 ${
|
|
||||||
doc?.isPinned ? 'text-status-warn' : 'text-accent'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<span>{doc?.isPinned ? 'Anheftung lösen' : 'Notiz anheften'}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{onRequestMerge && doc?.isOwner && !inTrash && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
onClick={() => {
|
|
||||||
setShowActionsMenu(false);
|
|
||||||
onRequestMerge(doc);
|
|
||||||
}}
|
|
||||||
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<GitMerge className="w-3.5 h-3.5 text-accent shrink-0" />
|
|
||||||
<span>Mit anderer Notiz zusammenführen…</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
onClick={() => {
|
|
||||||
setShowActionsMenu(false);
|
|
||||||
handleExportMarkdown();
|
|
||||||
}}
|
|
||||||
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-panel-elevated text-left text-main font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download className="w-3.5 h-3.5 text-accent shrink-0" />
|
|
||||||
<span>Als Markdown (.md) exportieren</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{canEdit && (doc?.isOwner || doc?.permission === 'owner') && (
|
|
||||||
<>
|
|
||||||
<div className="my-1 border-t border-border" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="menuitem"
|
|
||||||
onClick={() => {
|
|
||||||
setShowActionsMenu(false);
|
|
||||||
onDeleteDoc(docId);
|
|
||||||
}}
|
|
||||||
className="w-full min-h-[44px] sm:min-h-0 px-2.5 py-2 sm:py-1.5 rounded-btn hover:bg-status-err/10 text-left text-status-err font-medium flex items-center gap-2 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5 text-status-err shrink-0" />
|
|
||||||
<span>Notiz löschen</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1064,6 +1092,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
inJournal ? 'py-3' : 'py-5'
|
inJournal ? 'py-3' : 'py-5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{desktopBar && zoomedBlock && breadcrumbs.length > 0 && <div className="mb-3">{breadcrumbNav}</div>}
|
||||||
{zoomedBlock ? (
|
{zoomedBlock ? (
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
FOKUS-MODUS & ZOOM-IN (HOISTED SUBTREE VIEW)
|
FOKUS-MODUS & ZOOM-IN (HOISTED SUBTREE VIEW)
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const [docSaveStatus, setDocSaveStatus] = useState<DocSaveStatus>('saved');
|
const [docSaveStatus, setDocSaveStatus] = useState<DocSaveStatus>('saved');
|
||||||
const [docSaveError, setDocSaveError] = useState<string | null>(null);
|
const [docSaveError, setDocSaveError] = useState<string | null>(null);
|
||||||
const [docHeaderStatus, setDocHeaderStatus] = useState<DocHeaderStatus | null>(null);
|
const [docHeaderStatus, setDocHeaderStatus] = useState<DocHeaderStatus | null>(null);
|
||||||
|
const [docActionsTarget, setDocActionsTarget] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
|
||||||
// Drag and Drop state
|
// Drag and Drop state
|
||||||
@@ -1416,6 +1417,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<div className="hidden sm:flex items-center gap-2">
|
<div className="hidden sm:flex items-center gap-2">
|
||||||
{subView === 'editor' && selectedDocId ? (
|
{subView === 'editor' && selectedDocId ? (
|
||||||
<>
|
<>
|
||||||
|
{/* Fixed order: people first (they come and go without moving the buttons), status, actions */}
|
||||||
|
{docHeaderStatus && (
|
||||||
|
<PresenceAvatars
|
||||||
|
people={docHeaderStatus.otherPeople}
|
||||||
|
avatars={docHeaderStatus.avatarsById}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Live connection pill */}
|
{/* Live connection pill */}
|
||||||
{docHeaderStatus?.showLive && (
|
{docHeaderStatus?.showLive && (
|
||||||
<span
|
<span
|
||||||
@@ -1443,14 +1452,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Who else has this note open */}
|
|
||||||
{docHeaderStatus && (
|
|
||||||
<PresenceAvatars
|
|
||||||
people={docHeaderStatus.otherPeople}
|
|
||||||
avatars={docHeaderStatus.avatarsById}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Share pill */}
|
{/* Share pill */}
|
||||||
{selectedDocObj &&
|
{selectedDocObj &&
|
||||||
!selectedDocObj.isArchived &&
|
!selectedDocObj.isArchived &&
|
||||||
@@ -1469,7 +1470,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openDocShareModal(selectedDocObj)}
|
onClick={() => openDocShareModal(selectedDocObj)}
|
||||||
className="px-2 py-0.5 rounded text-[10px] font-semibold bg-accent/15 text-accent border border-accent/30 flex items-center gap-1 hover:bg-accent/25 hover:border-accent/50 transition-colors cursor-pointer"
|
className="px-2 py-0.5 rounded-btn text-[11px] font-semibold bg-accent/15 text-accent border border-accent/30 flex items-center gap-1 hover:bg-accent/25 hover:border-accent/50 transition-colors cursor-pointer"
|
||||||
title="Teilen-Menü öffnen: Geteilt mit Nutzern/Gruppen anzeigen & verwalten"
|
title="Teilen-Menü öffnen: Geteilt mit Nutzern/Gruppen anzeigen & verwalten"
|
||||||
>
|
>
|
||||||
<Users className="w-2.5 h-2.5" />
|
<Users className="w-2.5 h-2.5" />
|
||||||
@@ -1479,7 +1480,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openDocShareModal(selectedDocObj)}
|
onClick={() => openDocShareModal(selectedDocObj)}
|
||||||
className="px-2 py-0.5 rounded text-[10px] font-medium border border-border text-muted flex items-center gap-1 bg-surface-subtle hover:bg-panel-elevated hover:text-main transition-colors cursor-pointer"
|
className="px-2 py-0.5 rounded-btn text-[11px] font-medium border border-border text-muted flex items-center gap-1 bg-surface-subtle hover:bg-panel-elevated hover:text-main transition-colors cursor-pointer"
|
||||||
title="Teilen & Berechtigungen öffnen"
|
title="Teilen & Berechtigungen öffnen"
|
||||||
>
|
>
|
||||||
<Users className="w-2.5 h-2.5" />
|
<Users className="w-2.5 h-2.5" />
|
||||||
@@ -1488,9 +1489,19 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Save Status Pill */}
|
{/* Save status: icon only, the text is the tooltip */}
|
||||||
<div
|
<div
|
||||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium border transition-colors ${
|
role="img"
|
||||||
|
aria-label={
|
||||||
|
docSaveStatus === 'saved'
|
||||||
|
? 'Gespeichert'
|
||||||
|
: docSaveStatus === 'saving'
|
||||||
|
? 'Speichert…'
|
||||||
|
: docSaveStatus === 'unsaved'
|
||||||
|
? 'Ungespeichert'
|
||||||
|
: 'Fehler beim Speichern'
|
||||||
|
}
|
||||||
|
className={`inline-flex items-center justify-center h-7 w-7 rounded-btn border transition-colors ${
|
||||||
docSaveStatus === 'saved'
|
docSaveStatus === 'saved'
|
||||||
? 'bg-canvas border-border text-muted'
|
? 'bg-canvas border-border text-muted'
|
||||||
: docSaveStatus === 'saving'
|
: docSaveStatus === 'saving'
|
||||||
@@ -1512,30 +1523,28 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{docSaveStatus === 'saved' && (
|
{docSaveStatus === 'saved' && (
|
||||||
<>
|
<>
|
||||||
<Check className="w-3.5 h-3.5 text-status-ok" />
|
<Check className="w-3.5 h-3.5 text-status-ok" />
|
||||||
<span>Gespeichert</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{docSaveStatus === 'saving' && (
|
{docSaveStatus === 'saving' && (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-accent" />
|
<Loader2 className="w-3.5 h-3.5 animate-spin text-accent" />
|
||||||
<span>Speichert…</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{docSaveStatus === 'unsaved' && (
|
{docSaveStatus === 'unsaved' && (
|
||||||
<>
|
<>
|
||||||
<Clock className="w-3.5 h-3.5 text-status-warn" />
|
<Clock className="w-3.5 h-3.5 text-status-warn" />
|
||||||
<span>Ungespeichert</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{docSaveStatus === 'error' && (
|
{docSaveStatus === 'error' && (
|
||||||
<>
|
<>
|
||||||
<AlertTriangle className="w-3.5 h-3.5 text-status-err" />
|
<AlertTriangle className="w-3.5 h-3.5 text-status-err" />
|
||||||
<span>Fehler</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* The open note renders undo/redo, share and its menu here on large screens */}
|
||||||
|
<div ref={setDocActionsTarget} className="hidden lg:flex items-center" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1551,47 +1560,42 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
: 'flex'
|
: 'flex'
|
||||||
} w-full lg:w-72 xl:w-80 shrink-0 flex-col bg-sidebar border-r border-border transition-all duration-200`}
|
} w-full lg:w-72 xl:w-80 shrink-0 flex-col bg-sidebar border-r border-border transition-all duration-200`}
|
||||||
>
|
>
|
||||||
{/* Sidebar Header & Actions */}
|
{/* Sidebar header: search and the create buttons in one row */}
|
||||||
<div className="p-3 border-b border-border space-y-2">
|
<div className="p-3 border-b border-border flex items-center gap-1.5">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-xs font-bold text-main uppercase tracking-wider">
|
|
||||||
Notizen & Outliner
|
|
||||||
</h2>
|
|
||||||
{/* On phones these are in the menu at the top (and the + button), so not repeated here */}
|
|
||||||
<div className="hidden sm:flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleOpenNewFolder('', e)}
|
|
||||||
className="btn-action p-1 text-muted hover:text-main"
|
|
||||||
title="Neuer Ordner"
|
|
||||||
>
|
|
||||||
<FolderPlus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleCreateDocument('')}
|
|
||||||
className="h-7 px-2.5 bg-canvas hover:bg-panel border border-border border-dashed rounded-btn text-xs text-muted hover:text-main inline-flex items-center gap-1 transition-colors cursor-pointer"
|
|
||||||
title="Neue Notiz anlegen"
|
|
||||||
>
|
|
||||||
<Plus className="w-3.5 h-3.5" />
|
|
||||||
<span>Notiz</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Opens the global full-text search, which also finds the content of every bullet */}
|
{/* Opens the global full-text search, which also finds the content of every bullet */}
|
||||||
{onOpenSearch && (
|
{onOpenSearch && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenSearch}
|
onClick={onOpenSearch}
|
||||||
className="input-field w-full h-11 sm:h-7 px-2.5 flex items-center gap-2 text-xs text-faint hover:text-main cursor-pointer"
|
className="input-field flex-1 min-w-0 h-11 sm:h-8 px-2.5 flex items-center gap-2 text-xs text-faint hover:text-main cursor-pointer"
|
||||||
title="Globale Suche (Strg+K / ⌘K oder /)"
|
title="Globale Suche (Strg+K / ⌘K oder /)"
|
||||||
>
|
>
|
||||||
<Search className="w-3.5 h-3.5 shrink-0" />
|
<Search className="w-3.5 h-3.5 shrink-0" />
|
||||||
<span className="flex-1 text-left truncate">Notizen durchsuchen…</span>
|
<span className="flex-1 text-left truncate">Notizen durchsuchen…</span>
|
||||||
<span className="hidden sm:inline font-mono text-[10px]">⌘K</span>
|
<span className="hidden xl:inline font-mono text-[10px]">⌘K</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{/* On phones these are in the menu at the top (and the + button), so not repeated here */}
|
||||||
|
<div className="hidden sm:flex shrink-0 items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleOpenNewFolder('', e)}
|
||||||
|
className="btn-icon h-8 w-8"
|
||||||
|
title="Neuer Ordner"
|
||||||
|
aria-label="Neuer Ordner"
|
||||||
|
>
|
||||||
|
<FolderPlus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCreateDocument('')}
|
||||||
|
className="btn-icon h-8 w-8"
|
||||||
|
title="Neue Notiz anlegen"
|
||||||
|
aria-label="Neue Notiz anlegen"
|
||||||
|
>
|
||||||
|
<FilePlus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Navigation Tree */}
|
{/* Scrollable Navigation Tree */}
|
||||||
@@ -1882,6 +1886,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setDocSaveError(err || null);
|
setDocSaveError(err || null);
|
||||||
}}
|
}}
|
||||||
onHeaderStatusChange={setDocHeaderStatus}
|
onHeaderStatusChange={setDocHeaderStatus}
|
||||||
|
actionsTarget={docActionsTarget}
|
||||||
/>
|
/>
|
||||||
) : subView === 'tags' ? (
|
) : subView === 'tags' ? (
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
|
|||||||
Reference in new issue
Block a user