feat(workspace): journal panel toggle, view kept across tabs, sections nest under headings
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m9s

- the journal opens as a framed panel and closes on a second click,
  returning to the previous note; journal state and the last regular note
  survive tab switches, the fallback note is never a journal day
- pasted and imported markdown nests content under its heading (H2 below
  H1), Enter on a heading creates a child, '- ' / '* ' / '+ ' start a bullet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-30 20:11:09 +01:00
1 parent bc7bdc481e
commit 375dda982b
9 files changed
+145 -36

No files matched your search

@@ -53,7 +53,7 @@ import { PresenceAvatars } from './PresenceAvatars';
import { TrashView } from './TrashView';
import { JournalView } from './JournalView';
import { JournalCalendar } from './JournalCalendar';
import { isJournalEnabled, todayISO } from '../../lib/journal';
import { isJournalEnabled, readWorkspaceSession, todayISO, writeWorkspaceSession } from '../../lib/journal';
import { MergeDialog } from './MergeDialog';
import { CountBadge } from '../../components/CountBadge';
import { extractHashtags } from '../../lib/hashtags';
@@ -319,10 +319,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return list.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
}, [docs, serverTagsSummary]);
const [selectedDocId, setSelectedDocId] = useState<string | null>(initialDocId || null);
const [selectedDocId, setSelectedDocId] = useState<string | null>(
initialDocId || readWorkspaceSession().docId || null
);
const [targetBlockId, setTargetBlockId] = useState<string | null>(initialBlockId || null);
const [newlyCreatedDocId, setNewlyCreatedDocId] = useState<string | null>(null);
const [subView, setSubView] = useState<WorkspaceSubView>('editor');
// Journal open and the last regular note survive switching to other tabs (the view remounts)
const [subView, setSubView] = useState<WorkspaceSubView>(() =>
!initialDocId && isJournalEnabled() && readWorkspaceSession().journal ? 'journal' : 'editor'
);
const [trash, setTrash] = useState<TrashedDocument[]>([]);
// Bumped after a restore so the open editor reloads the note without the trash banner
const [editorNonce, setEditorNonce] = useState(0);
@@ -398,7 +403,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}
setShowNewFolderModal(true);
};
const [mobileShowDetail, setMobileShowDetail] = useState<boolean>(Boolean(initialDocId));
const [mobileShowDetail, setMobileShowDetail] = useState<boolean>(
Boolean(initialDocId) || subView === 'journal'
);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState<boolean>(() => {
try {
return localStorage.getItem('mindsift:workspace_sidebar_collapsed') === 'true';
@@ -473,12 +480,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const loadWorkspaceData = useCallback(async () => {
try {
const [docsRes, foldersRes, sharedFoldersRes, tagsRes] = await Promise.all([
const [docsRes, foldersRes, sharedFoldersRes, tagsRes, journalRes] = await Promise.all([
api.getDocuments(),
api.getFolders(),
api.getSharedFolders().catch(() => []),
api.getWorkspaceTags().catch(() => []),
isJournalEnabled() ? api.getJournal().catch(() => [] as JournalDay[]) : Promise.resolve([] as JournalDay[]),
]);
const journalIds = new Set(journalRes.map((d) => d.docId));
setDocs(docsRes);
setFolders(foldersRes);
setSharedFolders(sharedFoldersRes);
@@ -488,7 +497,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (prevId && docsRes.some((d) => d.id === prevId)) {
return prevId;
}
return docsRes.length > 0 ? docsRes[0].id : null;
// Not a journal day: those open in the journal
const first = docsRes.find((d) => !(d.isOwner && journalIds.has(d.id)));
return first ? first.id : null;
});
} catch {
// ignore
@@ -620,6 +631,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setMobileShowDetail(true);
}, [reloadJournal]);
// A second click closes the journal and shows the regular note again
const toggleJournal = useCallback(() => {
if (subView === 'journal') {
setSubView('editor');
setSelectedDocId((prev) => prev ?? treeDocsRef.current[0]?.id ?? null);
} else {
openJournal();
}
}, [subView, openJournal]);
const openTrash = useCallback(() => {
reloadTrash();
setSubView('trash');
@@ -1004,6 +1025,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
() => (journalDocIds.size ? docs.filter((d) => !(d.isOwner && journalDocIds.has(d.id))) : docs),
[docs, journalDocIds]
);
const treeDocsRef = useRef(treeDocs);
treeDocsRef.current = treeDocs;
useEffect(() => {
writeWorkspaceSession({ journal: subView === 'journal', docId: selectedDocId });
}, [subView, selectedDocId]);
const allFolderPaths = useMemo(() => {
const set = new Set<string>(folders);
@@ -1569,28 +1596,31 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* Scrollable Navigation Tree */}
<div className="flex-1 overflow-y-auto p-2 space-y-3">
{/* Journal on top; while open, its calendar stays below it */}
{/* Journal on top; open, it becomes a panel with the calendar (click the heading to close) */}
{journalEnabled && (
<div className="border-b border-border pb-1.5 -mb-1.5">
<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={openJournal}
className={`w-full flex items-center gap-2 px-2 h-11 sm:h-8 rounded-btn text-xs border transition-colors cursor-pointer ${
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'
? 'bg-panel-elevated text-main font-semibold border-border/80'
: 'border-transparent text-main hover:bg-panel-elevated'
? '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'
}`}
>
<CalendarDays className="w-3.5 h-3.5 text-accent" />
<span className="flex-1 text-left">Journal</span>
{subView === 'journal' ? (
<ChevronDown className="w-3.5 h-3.5 text-muted" />
) : (
<ChevronRight className="w-3.5 h-3.5 text-muted" />
)}
<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 px-1 pt-2 pb-1">
<div className="hidden lg:block pt-2">
<JournalCalendar
activeDate={journalActive}
entryDates={new Set(journalDays.filter((d) => !d.empty).map((d) => d.date))}