fix(workspace): center floating file explorer dialog on mobile
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
This commit is contained in:
1 parent
f2ead7513a
commit
3fcd164fd8
1 file changed
+299
-290
@@ -3189,6 +3189,278 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
const isEmpty = docs.length === 0 && allFolderPaths.length === 0;
|
||||
|
||||
const renderExplorerBody = () => (
|
||||
<>
|
||||
<div className="p-2.5 sm:p-2 panel-header space-y-2 flex-shrink-0">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 sm:w-3.5 sm: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="Dateien durchsuchen…"
|
||||
className="w-full h-9 sm:h-7 pl-8 sm:pl-7 pr-2 input-field text-sm sm:text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
handleCreateDoc(null);
|
||||
}}
|
||||
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center cursor-pointer"
|
||||
title="Neue Datei erstellen"
|
||||
>
|
||||
<FilePlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||
<span>Neue Datei</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartCreateFolder(null)}
|
||||
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center cursor-pointer"
|
||||
title="Neuen Ordner erstellen"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreatingFolder && newFolderParent === null && (
|
||||
<div className="p-2 panel-header bg-canvas flex items-center gap-1.5 flex-shrink-0">
|
||||
<Folder className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted flex-shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleConfirmCreateFolder();
|
||||
if (e.key === 'Escape') setIsCreatingFolder(false);
|
||||
}}
|
||||
placeholder="Neuer Ordnername…"
|
||||
className="flex-1 h-8 sm:h-6 px-2 sm:px-1.5 input-field text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirmCreateFolder}
|
||||
className="p-1.5 sm:p-1 text-accent hover:text-main rounded-btn cursor-pointer"
|
||||
>
|
||||
<Check className="w-4 h-4 sm:w-3 sm:h-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreatingFolder(false)}
|
||||
className="p-1.5 sm:p-1 text-muted hover:text-main rounded-btn cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4 sm:w-3 sm:h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty workspace state */}
|
||||
{isEmpty ? (
|
||||
<div className="p-4 text-center text-xs text-muted space-y-2 flex-1">
|
||||
<Folder className="w-8 h-8 text-faint mx-auto" />
|
||||
<p className="font-semibold text-main">Noch keine Notizen</p>
|
||||
<p className="text-[11px] text-faint mb-3 max-w-xs mx-auto leading-relaxed">
|
||||
Erstelle eine neue Datei oder lege Ordner an, um deine Dokumente übersichtlich zu strukturieren.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
handleCreateDoc(null);
|
||||
}}
|
||||
className="btn-action min-h-[38px] sm:min-h-0 cursor-pointer"
|
||||
>
|
||||
<FilePlus className="w-3.5 h-3.5 text-muted" />
|
||||
<span>Neue Datei</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartCreateFolder(null)}
|
||||
className="btn-action min-h-[38px] sm:min-h-0 cursor-pointer"
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5 text-muted" />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1 flex-1 lg:max-h-[calc(100vh-240px)] overflow-y-auto">
|
||||
{/* Drag target for Root when dragging a note or a folder */}
|
||||
{(draggingDocId || draggingFolderPath) && (
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragOverFolder('__root__');
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
if (dragOverFolder === '__root__') setDragOverFolder(null);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const droppedFolder =
|
||||
e.dataTransfer.getData('application/mindsift-folder-path') ||
|
||||
draggingFolderPath;
|
||||
setDragOverFolder(null);
|
||||
setDraggingDocId(null);
|
||||
setDraggingFolderPath(null);
|
||||
|
||||
if (droppedFolder) {
|
||||
handleMoveFolder(droppedFolder, '');
|
||||
} else {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{draggingFolderPath
|
||||
? '📁 Ordner in Hauptverzeichnis verschieben'
|
||||
: '📁 In Hauptverzeichnis verschieben (ohne Ordner)'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Angepinnte Notizen (Favorites) - Collapsible */}
|
||||
{pinnedDocs.length > 0 && !searchQuery && (
|
||||
<div className="mb-2 pb-1.5 border-b border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPinnedCollapsed((prev) => !prev)}
|
||||
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||
title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Pin className="w-3 h-3 text-accent" />
|
||||
<span>Angepinnte Notizen ({pinnedDocs.length})</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 text-muted transition-transform duration-200 ${
|
||||
isPinnedCollapsed ? '-rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{!isPinnedCollapsed && (
|
||||
<div className="space-y-0.5 px-1 mt-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');
|
||||
setShowMobileFileDrawer(false);
|
||||
}}
|
||||
className={`group flex items-center justify-between px-2 py-2.5 sm:py-1.5 rounded-btn transition-colors cursor-pointer text-sm sm: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-base sm: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 }, true);
|
||||
}}
|
||||
className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 p-1 sm:p-0.5 text-muted hover:text-accent rounded transition-opacity"
|
||||
title="Von Angepinnt entfernen"
|
||||
>
|
||||
<Pin className="w-3.5 h-3.5 sm:w-3 sm:h-3 text-accent" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Themen & Hashtags (Collapsible) - directly under Pinned Notes */}
|
||||
{workspaceTags.length > 0 && !searchQuery && (
|
||||
<div className="mb-2 pb-1.5 border-b border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTagsCollapsed((prev) => !prev)}
|
||||
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-muted uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||
title={isTagsCollapsed ? 'Themen & Hashtags aufklappen' : 'Themen & Hashtags einklappen'}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-accent">
|
||||
<Hash className="w-3 h-3" />
|
||||
<span>Themen & Hashtags ({workspaceTags.length})</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 text-muted transition-transform duration-200 ${
|
||||
isTagsCollapsed ? '-rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{!isTagsCollapsed && (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-1 px-1.5 py-1 mt-1 max-h-36 overflow-y-auto">
|
||||
{workspaceTags.map((wt) => (
|
||||
<button
|
||||
key={wt.tag}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
openTagPage(wt.tag);
|
||||
}}
|
||||
className={`px-2.5 py-1 sm:px-2 sm:py-0.5 rounded-pill text-xs sm:text-[11px] font-medium border flex items-center gap-1 transition-colors ${
|
||||
selectedTag === wt.tag
|
||||
? 'bg-accent/20 border-accent text-accent font-semibold'
|
||||
: 'bg-canvas border-border text-muted hover:border-accent/40 hover:text-main'
|
||||
}`}
|
||||
title={`#${wt.tag} (${wt.count} Abschnitte)`}
|
||||
>
|
||||
<span>#{wt.tag}</span>
|
||||
<span className="text-[9px] opacity-60">({wt.count})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rootTree.map(renderFolder)}
|
||||
{rootDocs.map((doc) => renderDocItem(doc, 0))}
|
||||
|
||||
{filteredDocs.length === 0 && searchQuery && (
|
||||
<div className="p-4 text-center text-xs text-faint">
|
||||
Keine Dateien gefunden für "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
|
||||
@@ -3269,308 +3541,45 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* ===================================================================== */}
|
||||
{workspaceMode === 'editor' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
||||
{/* Mobile Backdrop for File Tree Slide-Over Drawer */}
|
||||
{/* Mobile Centered Floating Explorer Dialog ("in der Mitte schweben") */}
|
||||
{showMobileFileDrawer && (
|
||||
<div
|
||||
className="fixed inset-0 !m-0 bg-black/50 backdrop-blur-xs z-40 lg:hidden animate-in fade-in duration-150"
|
||||
className="fixed inset-0 !m-0 z-50 flex items-center justify-center p-3.5 sm:p-4 bg-black/60 backdrop-blur-xs lg:hidden animate-in fade-in duration-150"
|
||||
onClick={() => setShowMobileFileDrawer(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Left Document & Folder Explorer Tree (Desktop Sidebar + Mobile Slide-Over Drawer) */}
|
||||
<div
|
||||
className={`${
|
||||
showMobileFileDrawer
|
||||
? 'fixed inset-y-0 left-0 z-50 w-[86vw] max-w-sm bg-panel border-r border-border shadow-2xl flex'
|
||||
: 'hidden lg:flex'
|
||||
} lg:col-span-4 panel-surface flex-col self-start max-h-screen lg:max-h-none`}
|
||||
>
|
||||
{/* Mobile Drawer Top Header */}
|
||||
<div className="flex lg:hidden items-center justify-between px-3.5 py-3 border-b border-border bg-panel">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderTree className="w-4 h-4 text-accent" />
|
||||
<span className="text-sm font-bold text-main">Dateibaum & Ordner</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileFileDrawer(false)}
|
||||
className="p-1.5 text-muted hover:text-main rounded-btn"
|
||||
title="Schließen"
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md max-h-[85vh] bg-panel border border-border rounded-2xl shadow-panel flex flex-col overflow-hidden animate-in zoom-in-95 duration-150"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-2.5 sm:p-2 panel-header space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 sm:w-3.5 sm: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="Dateien durchsuchen…"
|
||||
className="w-full h-9 sm:h-7 pl-8 sm:pl-7 pr-2 input-field text-sm sm:text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
handleCreateDoc(null);
|
||||
}}
|
||||
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center"
|
||||
title="Neue Datei erstellen"
|
||||
>
|
||||
<FilePlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||
<span>Neue Datei</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartCreateFolder(null)}
|
||||
className="flex-1 btn-action min-h-[38px] sm:min-h-0 justify-center"
|
||||
title="Neuen Ordner erstellen"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted" />
|
||||
<span>Neuer Ordner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreatingFolder && newFolderParent === null && (
|
||||
<div className="p-2 panel-header bg-canvas flex items-center gap-1.5">
|
||||
<Folder className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted flex-shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleConfirmCreateFolder();
|
||||
if (e.key === 'Escape') setIsCreatingFolder(false);
|
||||
}}
|
||||
placeholder="Neuer Ordnername…"
|
||||
className="flex-1 h-8 sm:h-6 px-2 sm:px-1.5 input-field text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirmCreateFolder}
|
||||
className="p-1.5 sm:p-1 text-accent hover:text-main rounded-btn"
|
||||
title="Bestätigen"
|
||||
>
|
||||
<Check className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreatingFolder(false)}
|
||||
className="p-1.5 sm:p-1 text-muted hover:text-main rounded-btn"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<X className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEmpty ? (
|
||||
<div className="p-6 my-3 mx-2 border border-dashed border-border bg-canvas/50 text-center rounded-card">
|
||||
<Folder className="w-7 h-7 text-faint mx-auto mb-2" />
|
||||
<h5 className="text-xs font-semibold text-muted mb-1">Keine Dateien vorhanden</h5>
|
||||
<p className="text-[11px] text-faint mb-3 max-w-xs mx-auto leading-relaxed">
|
||||
Erstelle eine neue Datei oder lege Ordner an, um deine Dokumente übersichtlich zu strukturieren.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{/* Mobile Dialog Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
|
||||
<FolderTree className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-main">Dateibaum & Ordner</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
handleCreateDoc(null);
|
||||
}}
|
||||
className="btn-action min-h-[38px] sm:min-h-0"
|
||||
onClick={() => setShowMobileFileDrawer(false)}
|
||||
className="p-1.5 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||
title="Schließen"
|
||||
>
|
||||
<FilePlus className="w-3.5 h-3.5 text-muted" />
|
||||
<span>Neue Datei</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartCreateFolder(null)}
|
||||
className="btn-action min-h-[38px] sm:min-h-0"
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5 text-muted" />
|
||||
<span>Neuer Ordner</span>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||
{renderExplorerBody()}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1 flex-1 lg:max-h-[calc(100vh-240px)] overflow-y-auto">
|
||||
{/* Drag target for Root when dragging a note or a folder */}
|
||||
{(draggingDocId || draggingFolderPath) && (
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragOverFolder('__root__');
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
if (dragOverFolder === '__root__') setDragOverFolder(null);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const droppedFolder =
|
||||
e.dataTransfer.getData('application/mindsift-folder-path') ||
|
||||
draggingFolderPath;
|
||||
setDragOverFolder(null);
|
||||
setDraggingDocId(null);
|
||||
setDraggingFolderPath(null);
|
||||
</div>
|
||||
)}
|
||||
|
||||
if (droppedFolder) {
|
||||
handleMoveFolder(droppedFolder, '');
|
||||
} else {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{draggingFolderPath
|
||||
? '📁 Ordner in Hauptverzeichnis verschieben'
|
||||
: '📁 In Hauptverzeichnis verschieben (ohne Ordner)'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Angepinnte Notizen (Favorites) - Collapsible */}
|
||||
{pinnedDocs.length > 0 && !searchQuery && (
|
||||
<div className="mb-2 pb-1.5 border-b border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPinnedCollapsed((prev) => !prev)}
|
||||
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||
title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Pin className="w-3 h-3 text-accent" />
|
||||
<span>Angepinnte Notizen ({pinnedDocs.length})</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 text-muted transition-transform duration-200 ${
|
||||
isPinnedCollapsed ? '-rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{!isPinnedCollapsed && (
|
||||
<div className="space-y-0.5 px-1 mt-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');
|
||||
setShowMobileFileDrawer(false);
|
||||
}}
|
||||
className={`group flex items-center justify-between px-2 py-2.5 sm:py-1.5 rounded-btn transition-colors cursor-pointer text-sm sm: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-base sm: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 }, true);
|
||||
}}
|
||||
className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 p-1 sm:p-0.5 text-muted hover:text-accent rounded transition-opacity"
|
||||
title="Von Angepinnt entfernen"
|
||||
>
|
||||
<Pin className="w-3.5 h-3.5 sm:w-3 sm:h-3 text-accent" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Themen & Hashtags (Collapsible) - directly under Pinned Notes */}
|
||||
{workspaceTags.length > 0 && !searchQuery && (
|
||||
<div className="mb-2 pb-1.5 border-b border-border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTagsCollapsed((prev) => !prev)}
|
||||
className="w-full px-2 py-1.5 sm:py-1 text-[10px] font-bold text-muted uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
|
||||
title={isTagsCollapsed ? 'Themen & Hashtags aufklappen' : 'Themen & Hashtags einklappen'}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-accent">
|
||||
<Hash className="w-3 h-3" />
|
||||
<span>Themen & Hashtags ({workspaceTags.length})</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-3 h-3 text-muted transition-transform duration-200 ${
|
||||
isTagsCollapsed ? '-rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{!isTagsCollapsed && (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-1 px-1.5 py-1 mt-1 max-h-36 overflow-y-auto">
|
||||
{workspaceTags.map((wt) => (
|
||||
<button
|
||||
key={wt.tag}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowMobileFileDrawer(false);
|
||||
openTagPage(wt.tag);
|
||||
}}
|
||||
className={`px-2.5 py-1 sm:px-2 sm:py-0.5 rounded-pill text-xs sm:text-[11px] font-medium border flex items-center gap-1 transition-colors ${
|
||||
selectedTag === wt.tag
|
||||
? 'bg-accent/20 border-accent text-accent font-semibold'
|
||||
: 'bg-canvas border-border text-muted hover:border-accent/40 hover:text-main'
|
||||
}`}
|
||||
title={`#${wt.tag} (${wt.count} Abschnitte)`}
|
||||
>
|
||||
<span>#{wt.tag}</span>
|
||||
<span className="text-[9px] opacity-60">({wt.count})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rootTree.map(renderFolder)}
|
||||
{rootDocs.map((doc) => renderDocItem(doc, 0))}
|
||||
|
||||
{filteredDocs.length === 0 && searchQuery && (
|
||||
<div className="p-4 text-center text-xs text-faint">
|
||||
Keine Dateien gefunden für "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Desktop Left Document & Folder Explorer Tree (Sidebar) */}
|
||||
<div className="hidden lg:flex lg:col-span-4 panel-surface flex-col self-start max-h-screen lg:max-h-none">
|
||||
{renderExplorerBody()}
|
||||
</div>
|
||||
|
||||
{/* Right Logseq Outliner Editor */}
|
||||
<div className="lg:col-span-8">
|
||||
{selectedDoc ? (
|
||||
|
||||
Reference in new issue
Block a user