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