Dev #3

Merged
sandrodach merged 30 commits from dev into main 2026-10-25 18:00:10 +01:00
Showing only changes of commit 20c50aa8d6 - Show all commits
+280 -148
View File
@@ -98,12 +98,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
initialDocId, initialDocId,
onStatsChange, onStatsChange,
}) => { }) => {
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tag' // Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags'
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tag'>('editor'); const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tags'>('editor');
const [selectedTag, setSelectedTag] = useState<string | null>(null); const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [tagDetails, setTagDetails] = useState<TagDetails | null>(null); const [tagDetails, setTagDetails] = useState<TagDetails | null>(null);
const [workspaceTags, setWorkspaceTags] = useState<TagSummary[]>([]); const [workspaceTags, setWorkspaceTags] = useState<TagSummary[]>([]);
const [isLoadingTag, setIsLoadingTag] = useState(false); const [isLoadingTag, setIsLoadingTag] = useState(false);
const [tagSearchQuery, setTagSearchQuery] = useState('');
// Collapsible sidebar sections
const [isPinnedCollapsed, setIsPinnedCollapsed] = useState(false);
const [isTagsCollapsed, setIsTagsCollapsed] = useState(false);
const [docs, setDocs] = useState<Document[]>([]); const [docs, setDocs] = useState<Document[]>([]);
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null); const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
@@ -394,7 +399,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const openTagPage = async (tag: string) => { const openTagPage = async (tag: string) => {
const clean = tag.replace(/^#/, '').toLowerCase(); const clean = tag.replace(/^#/, '').toLowerCase();
setSelectedTag(clean); setSelectedTag(clean);
setWorkspaceMode('tag'); setWorkspaceMode('tags');
setIsLoadingTag(true); setIsLoadingTag(true);
try { try {
const details = await api.getTagDetails(clean); const details = await api.getTagDetails(clean);
@@ -1817,6 +1822,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return docs.filter((d) => d.isPinned); return docs.filter((d) => d.isPinned);
}, [docs]); }, [docs]);
const filteredWorkspaceTags = useMemo(() => {
const q = tagSearchQuery.trim().replace(/^#/, '').toLowerCase();
if (!q) return workspaceTags;
return workspaceTags.filter((t) => t.tag.toLowerCase().includes(q));
}, [workspaceTags, tagSearchQuery]);
const renderDocItem = (doc: Document, depth: number) => { const renderDocItem = (doc: Document, depth: number) => {
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
const isDragging = draggingDocId === doc.id; const isDragging = draggingDocId === doc.id;
@@ -2057,6 +2068,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<span>Notizen</span> <span>Notizen</span>
</button> </button>
<button
type="button"
onClick={() => setWorkspaceMode('tags')}
className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
workspaceMode === 'tags'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<Hash className="w-3.5 h-3.5 text-accent" />
<span>Themen & Hashtags</span>
{workspaceTags.length > 0 && (
<span className="badge-neutral font-mono text-[10px]">
{workspaceTags.length}
</span>
)}
</button>
<button <button
type="button" type="button"
onClick={() => setWorkspaceMode('todos')} onClick={() => setWorkspaceMode('todos')}
@@ -2072,26 +2101,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{allWorkspaceTodos.filter((t) => !t.block.checked).length} {allWorkspaceTodos.filter((t) => !t.block.checked).length}
</span> </span>
</button> </button>
{selectedTag && (
<button
type="button"
onClick={() => setWorkspaceMode('tag')}
className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
workspaceMode === 'tag'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<Hash className="w-3.5 h-3.5 text-accent" />
<span>#{selectedTag}</span>
{tagDetails && (
<span className="badge-neutral font-mono text-[10px]">
{tagDetails.totalMatches}
</span>
)}
</button>
)}
</div> </div>
<div className="flex items-center gap-2 text-xs text-muted"> <div className="flex items-center gap-2 text-xs text-muted">
@@ -2259,94 +2268,120 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div> </div>
)} )}
{/* Angepinnte Notizen (Favorites) */} {/* Angepinnte Notizen (Favorites) - Collapsible */}
{pinnedDocs.length > 0 && !searchQuery && ( {pinnedDocs.length > 0 && !searchQuery && (
<div className="mb-2 pb-1.5 border-b border-border"> <div className="mb-2 pb-1.5 border-b border-border">
<div className="px-2.5 py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center gap-1.5"> <button
<Pin className="w-3 h-3 text-accent" /> type="button"
<span>Angepinnte Notizen ({pinnedDocs.length})</span> onClick={() => setIsPinnedCollapsed((prev) => !prev)}
</div> className="w-full px-2 py-1 text-[10px] font-bold text-accent uppercase tracking-wider flex items-center justify-between hover:bg-canvas/40 rounded transition-colors"
<div className="space-y-0.5 px-1"> title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
{pinnedDocs.map((doc) => { >
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; <div className="flex items-center gap-1.5">
return ( <Pin className="w-3 h-3 text-accent" />
<div <span>Angepinnte Notizen ({pinnedDocs.length})</span>
key={`pinned-${doc.id}`} </div>
draggable <ChevronDown
onDragStart={(e) => { className={`w-3 h-3 text-muted transition-transform duration-200 ${
e.stopPropagation(); isPinnedCollapsed ? '-rotate-90' : ''
e.dataTransfer.setData('application/mindsift-doc-id', doc.id); }`}
e.dataTransfer.setData('text/plain', doc.id); />
setDraggingDocId(doc.id); </button>
}} {!isPinnedCollapsed && (
onDragEnd={() => setDraggingDocId(null)} <div className="space-y-0.5 px-1 mt-1">
onClick={() => { {pinnedDocs.map((doc) => {
setSelectedDoc(doc); const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
setZoomedBlockId(null); return (
setWorkspaceMode('editor'); <div
}} key={`pinned-${doc.id}`}
className={`group flex items-center justify-between px-2 py-1.5 rounded-btn transition-colors cursor-pointer text-xs ${ draggable
isSelected onDragStart={(e) => {
? '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-sm flex-shrink-0">{doc.icon || '📄'}</span>
<span className="truncate">{doc.title || 'Unbenannt'}</span>
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
persistDocument({ ...doc, isPinned: false }, true); e.dataTransfer.setData('application/mindsift-doc-id', doc.id);
e.dataTransfer.setData('text/plain', doc.id);
setDraggingDocId(doc.id);
}} }}
className="opacity-0 group-hover:opacity-100 p-0.5 text-muted hover:text-accent rounded transition-opacity" onDragEnd={() => setDraggingDocId(null)}
title="Von Angepinnt entfernen" onClick={() => {
setSelectedDoc(doc);
setZoomedBlockId(null);
setWorkspaceMode('editor');
}}
className={`group flex items-center justify-between px-2 py-1.5 rounded-btn transition-colors cursor-pointer 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"
> >
<Pin className="w-3 h-3 text-accent" /> <div className="flex items-center gap-2 min-w-0 flex-1">
</button> <span className="text-sm flex-shrink-0">{doc.icon || '📄'}</span>
</div> <span className="truncate">{doc.title || 'Unbenannt'}</span>
); </div>
})} <button
</div> type="button"
onClick={(e) => {
e.stopPropagation();
persistDocument({ ...doc, isPinned: false }, true);
}}
className="opacity-0 group-hover:opacity-100 p-0.5 text-muted hover:text-accent rounded transition-opacity"
title="Von Angepinnt entfernen"
>
<Pin className="w-3 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 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 px-1.5 py-1 mt-1 max-h-36 overflow-y-auto">
{workspaceTags.map((wt) => (
<button
key={wt.tag}
type="button"
onClick={() => openTagPage(wt.tag)}
className={`px-2 py-0.5 rounded-pill 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> </div>
)} )}
{rootTree.map(renderFolder)} {rootTree.map(renderFolder)}
{rootDocs.map((doc) => renderDocItem(doc, 0))} {rootDocs.map((doc) => renderDocItem(doc, 0))}
{/* Workspace Hashtags Section */}
{workspaceTags.length > 0 && !searchQuery && (
<div className="mt-3 pt-2.5 border-t border-border">
<div className="px-2.5 py-1 text-[10px] font-bold text-muted uppercase tracking-wider flex items-center justify-between">
<div className="flex items-center gap-1.5 text-accent">
<Hash className="w-3 h-3" />
<span>Themen & Hashtags ({workspaceTags.length})</span>
</div>
</div>
<div className="flex flex-wrap gap-1 px-1.5 py-1">
{workspaceTags.map((wt) => (
<button
key={wt.tag}
type="button"
onClick={() => openTagPage(wt.tag)}
className={`px-2 py-0.5 rounded-pill 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>
)}
{filteredDocs.length === 0 && searchQuery && ( {filteredDocs.length === 0 && searchQuery && (
<div className="p-4 text-center text-xs text-faint"> <div className="p-4 text-center text-xs text-faint">
Keine Dateien gefunden für &quot;{searchQuery}&quot; Keine Dateien gefunden für &quot;{searchQuery}&quot;
@@ -3233,64 +3268,161 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* ===================================================================== */} {/* ===================================================================== */}
{/* VIEW MODE 3: HASHTAG / THEMEN-SEITE (LOGSEQ STYLE) */} {/* VIEW MODE 3: HASHTAG / THEMEN-SEITE (LOGSEQ STYLE) */}
{/* ===================================================================== */} {/* ===================================================================== */}
{workspaceMode === 'tag' && ( {workspaceMode === 'tags' && (
<div className="space-y-4"> <div className="space-y-4">
{/* Tag Page Header */} {/* Header Toolbar */}
<div className="p-4 panel-surface space-y-2 border border-border rounded-card"> <div className="p-4 panel-surface space-y-3 border border-border rounded-card">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button {selectedTag ? (
type="button" <>
onClick={() => setWorkspaceMode('editor')} <button
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5" type="button"
title="Zurück zum Editor" onClick={() => setSelectedTag(null)}
> className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
<ArrowLeft className="w-3.5 h-3.5 text-accent" /> title="Zurück zur Themen-Übersicht"
<span>Notizen</span> >
</button> <ArrowLeft className="w-3.5 h-3.5 text-accent" />
<div className="h-4 w-px bg-border" /> <span>Alle Themen</span>
<div className="flex items-center gap-2"> </button>
<div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/30 flex items-center justify-center text-blue-500 dark:text-blue-400 font-bold"> <div className="h-4 w-px bg-border" />
<Hash className="w-4 h-4" /> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/30 flex items-center justify-center text-blue-500 dark:text-blue-400 font-bold">
<Hash className="w-4 h-4" />
</div>
<div>
<h2 className="text-base font-bold text-main leading-tight flex items-center gap-1.5">
<span>#{selectedTag}</span>
</h2>
<p className="text-[11px] text-muted">
{isLoadingTag
? 'Wird geladen…'
: `${tagDetails?.totalMatches || 0} ${
tagDetails?.totalMatches === 1 ? 'Abschnitt' : 'Abschnitte'
} in ${tagDetails?.documents.length || 0} ${
tagDetails?.documents.length === 1 ? 'Notiz' : 'Notizen'
}`}
</p>
</div>
</div>
</>
) : (
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-lg bg-accent/10 border border-accent/30 flex items-center justify-center text-accent">
<Hash className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-bold text-main leading-tight">Themen & Hashtags</h2>
<p className="text-[11px] text-muted">
{workspaceTags.length} {workspaceTags.length === 1 ? 'Thema' : 'Themen'} im gesamten Workspace
</p>
</div>
</div> </div>
<div> )}
<h2 className="text-base font-bold text-main leading-tight flex items-center gap-1.5">
<span>#{selectedTag}</span>
</h2>
<p className="text-[11px] text-muted">
{isLoadingTag
? 'Wird geladen…'
: `${tagDetails?.totalMatches || 0} ${
tagDetails?.totalMatches === 1 ? 'Abschnitt' : 'Abschnitte'
} in ${tagDetails?.documents.length || 0} ${
tagDetails?.documents.length === 1 ? 'Notiz' : 'Notizen'
}`}
</p>
</div>
</div>
</div> </div>
{/* Other tags pills for fast hopping */} {/* Search Bar for tags */}
<div className="flex items-center gap-1 flex-wrap"> <div className="relative flex-1 min-w-[200px] max-w-xs">
{workspaceTags <Search className="w-3.5 h-3.5 text-muted absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
.filter((t) => t.tag !== selectedTag) <input
.slice(0, 6) type="text"
.map((t) => ( value={tagSearchQuery}
<button onChange={(e) => setTagSearchQuery(e.target.value)}
key={t.tag} placeholder="Hashtags durchsuchen…"
type="button" className="w-full h-8 pl-8 pr-7 input-field text-xs rounded-btn"
onClick={() => openTagPage(t.tag)} />
className="px-2 py-0.5 rounded-pill text-[11px] badge-neutral hover:border-accent/40 hover:text-main transition-colors" {tagSearchQuery && (
> <button
#{t.tag} ({t.count}) type="button"
</button> onClick={() => setTagSearchQuery('')}
))} className="absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-main"
title="Suche leeren"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div> </div>
</div> </div>
{/* Quick tag hopping pills */}
{workspaceTags.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap pt-1 border-t border-border/50">
<span className="text-[11px] text-muted font-medium mr-1">Filter:</span>
<button
type="button"
onClick={() => setSelectedTag(null)}
className={`px-2 py-0.5 rounded-pill text-[11px] transition-colors ${
selectedTag === null
? 'bg-accent text-white font-semibold'
: 'badge-neutral hover:border-accent/40 hover:text-main'
}`}
>
Alle ({workspaceTags.length})
</button>
{(tagSearchQuery ? filteredWorkspaceTags : workspaceTags).slice(0, 10).map((t) => (
<button
key={t.tag}
type="button"
onClick={() => openTagPage(t.tag)}
className={`px-2 py-0.5 rounded-pill text-[11px] transition-colors ${
selectedTag === t.tag
? 'bg-accent/20 border border-accent text-accent font-semibold'
: 'badge-neutral hover:border-accent/40 hover:text-main'
}`}
>
#{t.tag} ({t.count})
</button>
))}
</div>
)}
</div> </div>
{/* Matched Documents List */} {/* If NO tag is selected, show the big overview grid! */}
{isLoadingTag ? ( {!selectedTag ? (
<div>
{filteredWorkspaceTags.length === 0 ? (
<div className="p-12 text-center text-muted text-xs panel-surface space-y-2 border border-border rounded-card">
<Hash className="w-8 h-8 mx-auto text-faint" />
<p className="font-medium text-main">
{tagSearchQuery
? `Keine Hashtags gefunden für "${tagSearchQuery}"`
: 'Noch keine Themen oder Hashtags vorhanden'}
</p>
<p className="text-xs text-faint">
Füge einfach in irgendeiner Notiz ein Tag wie <code className="text-accent">#projekt</code> oder <code className="text-accent">#idee</code> ein.
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{filteredWorkspaceTags.map((wt) => (
<div
key={wt.tag}
onClick={() => openTagPage(wt.tag)}
className="group panel-surface p-4 rounded-card border border-border hover:border-accent hover:shadow-md cursor-pointer transition-all flex flex-col justify-between"
>
<div className="flex items-start justify-between gap-2 mb-3">
<div className="w-8 h-8 rounded-lg bg-accent/10 border border-accent/20 flex items-center justify-center text-accent group-hover:scale-105 transition-transform">
<Hash className="w-4 h-4" />
</div>
<span className="badge-neutral text-[10px] font-mono">
{wt.count} {wt.count === 1 ? 'Abschnitt' : 'Abschnitte'}
</span>
</div>
<div>
<h3 className="font-bold text-sm text-main group-hover:text-accent transition-colors truncate">
#{wt.tag}
</h3>
<p className="text-[11px] text-muted mt-2 flex items-center justify-between group-hover:text-main">
<span>Abschnitte ansehen</span>
<ExternalLink className="w-3 h-3 group-hover:translate-x-0.5 transition-transform text-accent" />
</p>
</div>
</div>
))}
</div>
)}
</div>
) : isLoadingTag ? (
<div className="p-12 text-center text-muted text-xs panel-surface"> <div className="p-12 text-center text-muted text-xs panel-surface">
<Sparkles className="w-6 h-6 animate-spin mx-auto mb-2 text-accent" /> <Sparkles className="w-6 h-6 animate-spin mx-auto mb-2 text-accent" />
Hashtag-Verknüpfungen werden geladen… Hashtag-Verknüpfungen werden geladen…