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,
onStatsChange,
}) => {
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tag'
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tag'>('editor');
// Top Workspace Tab Mode: 'editor' vs 'todos' vs 'tags'
const [workspaceMode, setWorkspaceMode] = useState<'editor' | 'todos' | 'tags'>('editor');
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [tagDetails, setTagDetails] = useState<TagDetails | null>(null);
const [workspaceTags, setWorkspaceTags] = useState<TagSummary[]>([]);
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 [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
@@ -394,7 +399,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const openTagPage = async (tag: string) => {
const clean = tag.replace(/^#/, '').toLowerCase();
setSelectedTag(clean);
setWorkspaceMode('tag');
setWorkspaceMode('tags');
setIsLoadingTag(true);
try {
const details = await api.getTagDetails(clean);
@@ -1817,6 +1822,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return docs.filter((d) => d.isPinned);
}, [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 isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
const isDragging = draggingDocId === doc.id;
@@ -2057,6 +2068,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
<span>Notizen</span>
</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
type="button"
onClick={() => setWorkspaceMode('todos')}
@@ -2072,26 +2101,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{allWorkspaceTodos.filter((t) => !t.block.checked).length}
</span>
</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 className="flex items-center gap-2 text-xs text-muted">
@@ -2259,14 +2268,27 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div>
)}
{/* Angepinnte Notizen (Favorites) */}
{/* Angepinnte Notizen (Favorites) - Collapsible */}
{pinnedDocs.length > 0 && !searchQuery && (
<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" />
<span>Angepinnte Notizen ({pinnedDocs.length})</span>
</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) => {
const isSelected = selectedDoc?.id === doc.id && workspaceMode === 'editor';
return (
@@ -2311,22 +2333,31 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
);
})}
</div>
)}
</div>
)}
{rootTree.map(renderFolder)}
{rootDocs.map((doc) => renderDocItem(doc, 0))}
{/* Workspace Hashtags Section */}
{/* Themen & Hashtags (Collapsible) - directly under Pinned Notes */}
{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="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>
</div>
<div className="flex flex-wrap gap-1 px-1.5 py-1">
<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}
@@ -2344,9 +2375,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</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 &quot;{searchQuery}&quot;
@@ -3233,20 +3268,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* ===================================================================== */}
{/* VIEW MODE 3: HASHTAG / THEMEN-SEITE (LOGSEQ STYLE) */}
{/* ===================================================================== */}
{workspaceMode === 'tag' && (
{workspaceMode === 'tags' && (
<div className="space-y-4">
{/* Tag Page Header */}
<div className="p-4 panel-surface space-y-2 border border-border rounded-card">
{/* Header Toolbar */}
<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 items-center gap-2">
{selectedTag ? (
<>
<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"
title="Zurück zum Editor"
title="Zurück zur Themen-Übersicht"
>
<ArrowLeft className="w-3.5 h-3.5 text-accent" />
<span>Notizen</span>
<span>Alle Themen</span>
</button>
<div className="h-4 w-px bg-border" />
<div className="flex items-center gap-2">
@@ -3268,29 +3305,124 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</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>
{/* Other tags pills for fast hopping */}
<div className="flex items-center gap-1 flex-wrap">
{workspaceTags
.filter((t) => t.tag !== selectedTag)
.slice(0, 6)
.map((t) => (
{/* Search Bar for tags */}
<div className="relative flex-1 min-w-[200px] max-w-xs">
<Search className="w-3.5 h-3.5 text-muted absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="text"
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
key={t.tag}
type="button"
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})
</button>
))}
</div>
</div>
)}
</div>
{/* Matched Documents List */}
{isLoadingTag ? (
{/* If NO tag is selected, show the big overview grid! */}
{!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">
<Sparkles className="w-6 h-6 animate-spin mx-auto mb-2 text-accent" />
Hashtag-Verknüpfungen werden geladen…