Dev #3
@@ -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 "{searchQuery}"
|
||||
@@ -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…
|
||||
|
||||
Reference in new issue
Block a user