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
+182 -50
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,14 +2268,27 @@ 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
type="button"
onClick={() => setIsPinnedCollapsed((prev) => !prev)}
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"
title={isPinnedCollapsed ? 'Angepinnte Notizen aufklappen' : 'Angepinnte Notizen einklappen'}
>
<div className="flex items-center gap-1.5">
<Pin className="w-3 h-3 text-accent" /> <Pin className="w-3 h-3 text-accent" />
<span>Angepinnte Notizen ({pinnedDocs.length})</span> <span>Angepinnte Notizen ({pinnedDocs.length})</span>
</div> </div>
<div className="space-y-0.5 px-1"> <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) => { {pinnedDocs.map((doc) => {
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor'; const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
return ( return (
@@ -2311,22 +2333,31 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
); );
})} })}
</div> </div>
)}
</div> </div>
)} )}
{rootTree.map(renderFolder)} {/* Themen & Hashtags (Collapsible) - directly under Pinned Notes */}
{rootDocs.map((doc) => renderDocItem(doc, 0))}
{/* Workspace Hashtags Section */}
{workspaceTags.length > 0 && !searchQuery && ( {workspaceTags.length > 0 && !searchQuery && (
<div className="mt-3 pt-2.5 border-t 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-muted uppercase tracking-wider flex items-center justify-between"> <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"> <div className="flex items-center gap-1.5 text-accent">
<Hash className="w-3 h-3" /> <Hash className="w-3 h-3" />
<span>Themen & Hashtags ({workspaceTags.length})</span> <span>Themen & Hashtags ({workspaceTags.length})</span>
</div> </div>
</div> <ChevronDown
<div className="flex flex-wrap gap-1 px-1.5 py-1"> 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) => ( {workspaceTags.map((wt) => (
<button <button
key={wt.tag} key={wt.tag}
@@ -2344,9 +2375,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</button> </button>
))} ))}
</div> </div>
)}
</div> </div>
)} )}
{rootTree.map(renderFolder)}
{rootDocs.map((doc) => renderDocItem(doc, 0))}
{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,20 +3268,22 @@ 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">
{selectedTag ? (
<>
<button <button
type="button" type="button"
onClick={() => setWorkspaceMode('editor')} onClick={() => setSelectedTag(null)}
className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5" className="btn-action h-8 px-2.5 text-xs flex items-center gap-1.5"
title="Zurück zum Editor" title="Zurück zur Themen-Übersicht"
> >
<ArrowLeft className="w-3.5 h-3.5 text-accent" /> <ArrowLeft className="w-3.5 h-3.5 text-accent" />
<span>Notizen</span> <span>Alle Themen</span>
</button> </button>
<div className="h-4 w-px bg-border" /> <div className="h-4 w-px bg-border" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -3268,29 +3305,124 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</p> </p>
</div> </div>
</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>
{/* 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}
onChange={(e) => setTagSearchQuery(e.target.value)}
placeholder="Hashtags durchsuchen…"
className="w-full h-8 pl-8 pr-7 input-field text-xs rounded-btn"
/>
{tagSearchQuery && (
<button
type="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>
{/* 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 <button
key={t.tag} key={t.tag}
type="button" type="button"
onClick={() => openTagPage(t.tag)} 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" 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}) #{t.tag} ({t.count})
</button> </button>
))} ))}
</div> </div>
</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…