feat(workspace): sliding sidebar in focus mode, calmer workspace bar, smoother journal
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m47s

- focus mode: folders slide in from the left (button, title click, or the
  left edge on wide screens); picking a note keeps focus mode
- workspace bar: borderless live/shared/save pills in one style, divider
  before the note actions; footer without top line
- journal panel and calendar open with an animation, the view fades in
- paste and markdown import nest a list under the paragraph introducing it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 22:04:30 +01:00
1 parent 226c863a2a
commit b136e5dfad
9 files changed
+183 -60

No files matched your search

+102 -46
View File
@@ -431,6 +431,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [docActionsTarget, setDocActionsTarget] = useState<HTMLDivElement | null>(null);
// Distraction-free mode: only the note, full window, reading width
const [focusMode, setFocusMode] = useState(false);
// Focus mode: the folder sidebar slides in from the left (button, title click or mouse at the edge)
const [zenSidebar, setZenSidebar] = useState<'closed' | 'click' | 'hover'>('closed');
const [zenActionsTarget, setZenActionsTarget] = useState<HTMLDivElement | null>(null);
@@ -1043,18 +1045,23 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
useEffect(() => {
if (!canFocus) setFocusMode(false);
}, [canFocus]);
// Leaving focus mode or picking another note closes the sliding sidebar
useEffect(() => {
setZenSidebar('closed');
}, [focusMode, selectedDocId]);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'f' && canFocus) {
e.preventDefault();
setFocusMode((v) => !v);
} else if (e.key === 'Escape' && focusMode && !e.defaultPrevented) {
setFocusMode(false);
if (zenSidebar !== 'closed') setZenSidebar('closed');
else setFocusMode(false);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [canFocus, focusMode]);
}, [canFocus, focusMode, zenSidebar]);
const allFolderPaths = useMemo(() => {
const set = new Set<string>(folders);
@@ -1451,7 +1458,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Live connection pill */}
{docHeaderStatus?.showLive && (
<span
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-btn text-[11px] text-muted bg-panel border border-border"
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel text-muted"
title={
docHeaderStatus.channel === 'ws'
? 'Live-Verbindung über WebSocket'
@@ -1493,7 +1500,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button
type="button"
onClick={() => openDocShareModal(selectedDocObj)}
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"
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel font-semibold text-accent hover:bg-panel-elevated transition-colors cursor-pointer"
title="Teilen-Menü öffnen: Geteilt mit Nutzern/Gruppen anzeigen & verwalten"
>
<Users className="w-2.5 h-2.5" />
@@ -1503,7 +1510,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<button
type="button"
onClick={() => openDocShareModal(selectedDocObj)}
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"
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel font-medium text-muted hover:bg-panel-elevated hover:text-main transition-colors cursor-pointer"
title="Teilen & Berechtigungen öffnen"
>
<Users className="w-2.5 h-2.5" />
@@ -1524,14 +1531,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
? 'Ungespeichert'
: 'Fehler beim Speichern'
}
className={`inline-flex items-center justify-center h-7 w-7 rounded-btn border transition-colors ${
className={`inline-flex items-center justify-center h-7 w-7 rounded-btn transition-colors ${
docSaveStatus === 'saved'
? 'bg-canvas border-border text-muted'
? 'bg-panel'
: docSaveStatus === 'saving'
? 'bg-panel border-accent/40 text-accent animate-pulse'
? 'bg-panel animate-pulse'
: docSaveStatus === 'unsaved'
? 'bg-status-warn/10 border-status-warn/30 text-status-warn'
: 'bg-status-err/10 border-status-err/30 text-status-err'
? 'bg-status-warn/10'
: 'bg-status-err/10'
}`}
title={
docSaveStatus === 'saved'
@@ -1567,6 +1574,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</>
) : null}
{/* The open note renders undo/redo, share and its menu here on large screens */}
{subView === 'editor' && selectedDocId && (
<span className="hidden lg:block w-px h-5 bg-border mx-0.5" aria-hidden />
)}
<div ref={setDocActionsTarget} className="hidden lg:flex items-center" />
</div>
</div>
@@ -1584,9 +1594,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Header slides in when the mouse touches the top edge (large screens) */}
<div className="hidden lg:block fixed top-0 inset-x-0 h-3 z-[46] group">
<div className="absolute top-0 inset-x-0 flex items-center gap-2 px-6 py-2 bg-panel/95 backdrop-blur-sm border-b border-border shadow-panel -translate-y-full group-hover:translate-y-0 group-focus-within:translate-y-0 transition-transform duration-200">
<span className="flex-1 min-w-0 truncate text-xs font-medium text-main">
<button
type="button"
onClick={() => setZenSidebar((v) => (v === 'closed' ? 'click' : 'closed'))}
className="btn-icon h-7 w-7 shrink-0"
title="Ordner und Notizen einblenden"
aria-label="Ordner und Notizen einblenden"
aria-expanded={zenSidebar !== 'closed'}
>
<FolderOpen className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => setZenSidebar((v) => (v === 'closed' ? 'click' : 'closed'))}
className="flex-1 min-w-0 truncate text-left text-xs font-medium text-main hover:text-accent cursor-pointer"
title="Andere Notiz wählen"
>
{selectedDocObj?.icon} {selectedDocObj?.title}
</span>
</button>
<div ref={setZenActionsTarget} className="flex items-center" />
<button
type="button"
@@ -1599,6 +1624,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</button>
</div>
</div>
{/* Wide screens: touching the left edge opens the sidebar (the note sits far from it) */}
<div
className="hidden xl:block fixed left-0 inset-y-0 w-2 z-[46]"
onMouseEnter={() => setZenSidebar((v) => (v === 'closed' ? 'hover' : v))}
aria-hidden
/>
{zenSidebar === 'click' && (
<div className="fixed inset-0 z-[46] bg-overlay/30" onClick={() => setZenSidebar('closed')} aria-hidden />
)}
<button
type="button"
onClick={() => setFocusMode(false)}
@@ -1612,13 +1646,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
)}
{/* LEFT MASTER SIDEBAR */}
<aside
className={`${
(subView !== 'editor' && subView !== 'trash' && subView !== 'journal') || isSidebarCollapsed || focusMode
? 'hidden'
: mobileShowDetail
? 'hidden lg:flex'
: 'flex'
} w-full lg:w-72 xl:w-80 shrink-0 flex-col bg-sidebar border-r border-border transition-all duration-200`}
onMouseLeave={() => setZenSidebar((v) => (v === 'hover' ? 'closed' : v))}
className={
focusMode
? `fixed left-0 inset-y-0 z-[47] w-80 max-w-[85vw] flex flex-col bg-sidebar border-r border-border shadow-panel transition-transform duration-300 ease-out ${
zenSidebar !== 'closed' ? 'translate-x-0' : '-translate-x-full'
}`
: `${
(subView !== 'editor' && subView !== 'trash' && subView !== 'journal') || isSidebarCollapsed
? 'hidden'
: mobileShowDetail
? 'hidden lg:flex'
: '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: search and the create buttons in one row */}
<div className="p-3 border-b border-border flex items-center gap-1.5">
@@ -1660,38 +1701,53 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Scrollable Navigation Tree */}
<div className="flex-1 overflow-y-auto p-2 space-y-3">
{/* Journal on top; open, it becomes a panel with the calendar (click the heading to close) */}
{/* Journal on top; open, it becomes a panel with the calendar (click the heading to close).
Panel and calendar animate open (grid rows 0fr → 1fr) instead of jumping. */}
{journalEnabled && (
<div
className={
subView === 'journal'
? 'rounded-card border-2 border-accent/40 bg-panel-elevated p-2.5'
: 'border-b border-border pb-1.5 -mb-1.5'
}
>
<button
type="button"
onClick={toggleJournal}
aria-expanded={subView === 'journal'}
title={subView === 'journal' ? 'Journal schließen' : 'Journal öffnen'}
className={`w-full flex items-center gap-2 text-left cursor-pointer transition-colors ${
subView === 'journal'
? 'h-11 sm:h-7 px-0.5 text-sm font-bold text-main'
: 'h-11 sm:h-8 px-2 rounded-btn text-xs text-main border border-transparent hover:bg-panel-elevated'
<div>
<div
className={`rounded-card border-2 transition-[background-color,border-color,padding] duration-300 ease-out ${
subView === 'journal' ? 'border-accent/40 bg-panel-elevated p-2.5' : 'border-transparent p-0'
}`}
>
<CalendarDays className={subView === 'journal' ? 'w-4 h-4 text-accent' : 'w-3.5 h-3.5 text-accent'} />
<span>Journal</span>
</button>
{subView === 'journal' && (
<div className="hidden lg:block pt-2">
<JournalCalendar
activeDate={journalActive}
entryDates={new Set(journalDays.filter((d) => !d.empty).map((d) => d.date))}
onSelect={jumpToJournalDay}
<button
type="button"
onClick={toggleJournal}
aria-expanded={subView === 'journal'}
title={subView === 'journal' ? 'Journal schließen' : 'Journal öffnen'}
className={`w-full flex items-center gap-2 text-left cursor-pointer rounded-btn transition-all duration-300 ${
subView === 'journal'
? 'h-11 sm:h-7 px-0.5 text-sm font-bold text-main'
: 'h-11 sm:h-8 px-2 text-xs text-main hover:bg-panel-elevated'
}`}
>
<CalendarDays
className={`text-accent transition-all duration-300 ${subView === 'journal' ? 'w-4 h-4' : 'w-3.5 h-3.5'}`}
/>
<span>Journal</span>
</button>
<div
className={`hidden lg:grid transition-[grid-template-rows,opacity] duration-300 ease-out ${
subView === 'journal' ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
}`}
aria-hidden={subView !== 'journal'}
>
<div className="overflow-hidden min-h-0">
<div className="pt-2">
<JournalCalendar
activeDate={journalActive}
entryDates={new Set(journalDays.filter((d) => !d.empty).map((d) => d.date))}
onSelect={jumpToJournalDay}
/>
</div>
</div>
</div>
)}
</div>
<div
className={`h-px bg-border mt-1.5 transition-opacity duration-300 ${
subView === 'journal' ? 'opacity-0' : 'opacity-100'
}`}
/>
</div>
)}
{pinnedDocs.length === 0 && folderTree.length === 0 && rootDocs.length === 0 ? (