From d408e7d307305f39b184531fd148c276be19a878 Mon Sep 17 00:00:00 2001 From: Sandro Date: Fri, 30 Oct 2026 20:22:39 +0100 Subject: [PATCH] feat(workspace): note actions in the workspace bar on desktop, one-row sidebar header 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 --- CHANGELOG.md | 2 + .../src/features/workspace/DocumentEditor.tsx | 369 ++++++++++-------- .../src/features/workspace/WorkspaceView.tsx | 97 ++--- 3 files changed, 252 insertions(+), 216 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2855b51..6e10368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] ### 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). - 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. diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index 24bcce9..0d57749 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -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 { Pin, Users, @@ -63,6 +64,20 @@ export interface DocumentEditorProps { onDocRemoved?: (docId: string) => void; /** 'journal': one day inside the journal list – no own scrolling, header or backlinks, fixed title */ 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'; @@ -214,8 +229,11 @@ export const DocumentEditor: React.FC = ({ onDocCreated, onDocRemoved, variant = 'page', + actionsTarget, }) => { const inJournal = variant === 'journal'; + const large = useSyncExternalStore(subscribeLarge, isLargeScreen, () => false); + const desktopBar = !inJournal && large && Boolean(actionsTarget); const [doc, setDoc] = useState(initialDoc || null); const [title, setTitle] = useState(initialDoc?.title || ''); const [blocks, setBlocks] = useState(() => @@ -787,14 +805,189 @@ export const DocumentEditor: React.FC = ({ ); } + const breadcrumbNav = ( + + ); + + // Undo/redo, share and the menu; on large screens they sit in the workspace tab bar instead + const actionsBlock = ( +
+ {!inTrash && ( + <> + + + + )} + + {doc && !inTrash && ( + + )} + +
+ + + {showActionsMenu && ( +
+ {!inTrash && ( + + )} + + {onRequestMerge && doc?.isOwner && !inTrash && ( + + )} + + + + {canEdit && (doc?.isOwner || doc?.permission === 'owner') && ( + <> +
+ + + )} +
+ )} +
+
+ ); + return (
- {/* Top Document Action Bar (the journal has one for all days) */} - {!inJournal && ( + {desktopBar && actionsTarget && createPortal(actionsBlock, actionsTarget)} + + {/* Top Document Action Bar (the journal has one for all days; large screens use the tab bar) */} + {!inJournal && !desktopBar && (
{onBackMobile && ( @@ -810,48 +1003,7 @@ export const DocumentEditor: React.FC = ({ {/* Breadcrumbs Navigation (Fokus-Modus) or Doc Indicator */} {zoomedBlock && breadcrumbs.length > 0 ? ( - + breadcrumbNav ) : (
= ({ )}
- {/* Right Action Buttons */} -
- {!inTrash && ( - <> - - - - )} - - {doc && !inTrash && ( - - )} - -
- - - {showActionsMenu && ( -
- {!inTrash && ( - - )} - - {onRequestMerge && doc?.isOwner && !inTrash && ( - - )} - - - - {canEdit && (doc?.isOwner || doc?.permission === 'owner') && ( - <> -
- - - )} -
- )} -
-
+ {!desktopBar && actionsBlock}
)} @@ -1064,6 +1092,7 @@ export const DocumentEditor: React.FC = ({ inJournal ? 'py-3' : 'py-5' }`} > + {desktopBar && zoomedBlock && breadcrumbs.length > 0 &&
{breadcrumbNav}
} {zoomedBlock ? ( /* ========================================================================= FOKUS-MODUS & ZOOM-IN (HOISTED SUBTREE VIEW) diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index af60e1f..894fa54 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -427,6 +427,7 @@ export const WorkspaceView: React.FC = ({ const [docSaveStatus, setDocSaveStatus] = useState('saved'); const [docSaveError, setDocSaveError] = useState(null); const [docHeaderStatus, setDocHeaderStatus] = useState(null); + const [docActionsTarget, setDocActionsTarget] = useState(null); // Drag and Drop state @@ -1416,6 +1417,14 @@ export const WorkspaceView: React.FC = ({
{subView === 'editor' && selectedDocId ? ( <> + {/* Fixed order: people first (they come and go without moving the buttons), status, actions */} + {docHeaderStatus && ( + + )} + {/* Live connection pill */} {docHeaderStatus?.showLive && ( = ({ )} - {/* Who else has this note open */} - {docHeaderStatus && ( - - )} - {/* Share pill */} {selectedDocObj && !selectedDocObj.isArchived && @@ -1469,7 +1470,7 @@ export const WorkspaceView: React.FC = ({
@@ -1551,47 +1560,42 @@ export const WorkspaceView: React.FC = ({ : 'flex' } 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 */} -
-
-

- Notizen & Outliner -

- {/* On phones these are in the menu at the top (and the + button), so not repeated here */} -
- - -
-
- + {/* Sidebar header: search and the create buttons in one row */} +
{/* Opens the global full-text search, which also finds the content of every bullet */} {onOpenSearch && ( )} + {/* On phones these are in the menu at the top (and the + button), so not repeated here */} +
+ + +
{/* Scrollable Navigation Tree */} @@ -1882,6 +1886,7 @@ export const WorkspaceView: React.FC = ({ setDocSaveError(err || null); }} onHeaderStatusChange={setDocHeaderStatus} + actionsTarget={docActionsTarget} /> ) : subView === 'tags' ? ( /* =========================================================================