Dev #7

Merged
sandrodach merged 33 commits from dev into main 2026-10-27 11:42:12 +01:00
Showing only changes of commit 60fd1c9ae7 - Show all commits
+421 -14
View File
@@ -17,6 +17,8 @@ import {
X,
ExternalLink,
Sparkles,
ArrowLeft,
CheckSquare,
} from 'lucide-react';
import {
Document,
@@ -40,7 +42,7 @@ interface WorkspaceViewProps {
onStatsChange: () => void;
}
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos';
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags';
interface FolderTreeNode {
name: string;
@@ -248,6 +250,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [tagDetails, setTagDetails] = useState<TagDetails | null>(null);
const [tagNoteDraft, setTagNoteDraft] = useState<string>('');
const [tagSearchQuery, setTagSearchQuery] = useState<string>('');
const [tagSortBy, setTagSortBy] = useState<'count' | 'name'>('count');
const [searchQuery, setSearchQuery] = useState<string>('');
const [expandedFolders, setExpandedFolders] = useState<Record<string, boolean>>({});
@@ -925,6 +929,70 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return [];
}, [activeDocShares, shareModalDoc, shareModalFolder]);
// Aggregated metadata map for every tag (documents where it occurs, open todos count)
const tagsMetadataMap = useMemo(() => {
const map = new Map<
string,
{
tag: string;
count: number;
docs: { id: string; title: string; icon?: string; folderPath?: string }[];
openTodosCount: number;
}
>();
for (const t of tagsSummary) {
const cleanTag = t.tag.toLowerCase();
const matchingDocs: { id: string; title: string; icon?: string; folderPath?: string }[] = [];
let openTodos = 0;
for (const d of docs) {
let hasInDoc = false;
const blocks = d.blocks || [];
for (const b of blocks) {
const content = b.content || '';
const regex = new RegExp(`(?:^|[\\s(])#(${cleanTag})(?=[\\s.,!?:;)#\\]}]|$)`, 'i');
if (regex.test(content)) {
hasInDoc = true;
if (b.type === 'todo' && !b.checked) openTodos++;
}
}
if (hasInDoc) {
matchingDocs.push({
id: d.id,
title: d.title || 'Unbenannte Notiz',
icon: d.icon,
folderPath: d.folderPath,
});
}
}
map.set(cleanTag, {
tag: t.tag,
count: t.count,
docs: matchingDocs,
openTodosCount: openTodos,
});
}
return map;
}, [tagsSummary, docs]);
// Filtered and sorted tags for the Kachel Grid view
const filteredTags = useMemo(() => {
const q = tagSearchQuery.trim().toLowerCase().replace(/^#/, '');
let list = [...tagsSummary];
if (q) {
list = list.filter((t) => t.tag.toLowerCase().includes(q));
}
if (tagSortBy === 'count') {
list.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
} else {
list.sort((a, b) => a.tag.localeCompare(b.tag));
}
return list;
}, [tagsSummary, tagSearchQuery, tagSortBy]);
return (
<div className="flex h-[calc(100vh-4rem)] bg-canvas rounded-card border border-border overflow-hidden shadow-panel">
{/* LEFT MASTER SIDEBAR */}
@@ -981,18 +1049,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
)}
</div>
{/* Sub-view Switcher: Notizen vs. To-Dos */}
<div className="grid grid-cols-2 gap-1 p-0.5 rounded-btn bg-canvas border border-border text-xs">
{/* Sub-view Switcher: Notizen vs. To-Dos vs. Hashtags */}
<div className="grid grid-cols-3 gap-1 p-0.5 rounded-btn bg-canvas border border-border text-xs">
<button
type="button"
onClick={() => setSubView('editor')}
className={`py-1 rounded-btn font-medium transition-colors ${
className={`py-1.5 px-1 rounded-btn font-medium transition-colors flex items-center justify-center gap-1 cursor-pointer truncate ${
subView === 'editor'
? 'bg-panel-elevated text-main font-semibold shadow-sm'
? 'bg-panel-elevated text-main font-semibold shadow-xs'
: 'text-muted hover:text-main'
}`}
title="Notizen & Dokumente"
>
Dokumente ({docs.length})
<FileText className="w-3.5 h-3.5 text-accent shrink-0" />
<span className="truncate">Notizen</span>
</button>
<button
type="button"
@@ -1000,13 +1070,37 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setSubView('todos');
setMobileShowDetail(true);
}}
className={`py-1 rounded-btn font-medium transition-colors ${
className={`py-1.5 px-1 rounded-btn font-medium transition-colors flex items-center justify-center gap-1 cursor-pointer truncate ${
subView === 'todos'
? 'bg-panel-elevated text-main font-semibold shadow-sm'
? 'bg-panel-elevated text-main font-semibold shadow-xs'
: 'text-muted hover:text-main'
}`}
title="To-Dos Rollup"
>
To-Dos ({totalOpenTodos})
<Check className="w-3.5 h-3.5 text-status-ok shrink-0" />
<span className="truncate">To-Dos</span>
{totalOpenTodos > 0 && (
<span className="badge-neutral text-[9px] px-1 py-0">{totalOpenTodos}</span>
)}
</button>
<button
type="button"
onClick={() => {
setSubView('tags');
setMobileShowDetail(true);
}}
className={`py-1.5 px-1 rounded-btn font-medium transition-colors flex items-center justify-center gap-1 cursor-pointer truncate ${
subView === 'tags' || subView === 'tag_detail'
? 'bg-panel-elevated text-main font-semibold shadow-xs'
: 'text-muted hover:text-main'
}`}
title="Hashtag Kacheln & Übersicht"
>
<Hash className="w-3.5 h-3.5 text-accent shrink-0" />
<span className="truncate">Hashtags</span>
{tagsSummary.length > 0 && (
<span className="badge-neutral text-[9px] px-1 py-0">{tagsSummary.length}</span>
)}
</button>
</div>
</div>
@@ -1189,22 +1283,292 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onWikiLinkClick={handleWikiLinkClick}
onBackMobile={() => setMobileShowDetail(false)}
/>
) : subView === 'tags' ? (
/* =========================================================================
HASHTAG OVERVIEW & TILE DIRECTORY (KACHEL-ANSICHT MIT SUCHE)
========================================================================= */
<div className="flex-1 overflow-y-auto p-4 sm:p-8 bg-canvas">
<div className="max-w-4xl mx-auto space-y-5">
{/* Top Navigation Tabs */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-3">
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
onClick={() => setSubView('editor')}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<FileText className="w-3.5 h-3.5 text-accent" />
<span>Notizen</span>
<span className="badge-neutral text-[10px]">{docs.length}</span>
</button>
<button
type="button"
onClick={() => {
setSubView('todos');
setMobileShowDetail(true);
}}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<Check className="w-3.5 h-3.5 text-status-ok" />
<span>To-Dos</span>
<span className="badge-neutral text-[10px]">{totalOpenTodos}</span>
</button>
<button
type="button"
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-panel border-accent text-main font-semibold shadow-xs"
>
<Hash className="w-3.5 h-3.5 text-accent" />
<span>Hashtags</span>
<span className="badge-neutral text-[10px]">{tagsSummary.length}</span>
</button>
</div>
<button
type="button"
onClick={() => setSubView('editor')}
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
>
Zum Editor
</button>
</div>
{/* Header with Title and Search Controls */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<Hash className="w-5 h-5 text-accent" />
<h1 className="text-xl font-bold text-main">Hashtags Übersicht</h1>
<span className="badge-neutral text-xs font-mono">{tagsSummary.length} Tags</span>
</div>
<p className="text-xs text-muted mt-0.5">
Entdecke alle Themen, Schlagworte und verknüpften Blöcke aus deinen Notizen
</p>
</div>
{/* Search & Sort Toolbar */}
<div className="flex items-center gap-2">
<div className="relative min-w-[200px] sm:min-w-[240px]">
<Search className="w-3.5 h-3.5 text-faint absolute left-2.5 top-1/2 -translate-y-1/2" />
<input
type="text"
value={tagSearchQuery}
onChange={(e) => setTagSearchQuery(e.target.value)}
placeholder="Hashtags durchsuchen…"
className="input-field w-full pl-8 pr-7 py-1.5 text-xs"
/>
{tagSearchQuery && (
<button
type="button"
onClick={() => setTagSearchQuery('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-faint hover:text-main cursor-pointer"
>
<X className="w-3 h-3" />
</button>
)}
</div>
{/* Sort Mode Switcher */}
<div className="flex items-center bg-canvas border border-border rounded-btn p-0.5 text-[11px]">
<button
type="button"
onClick={() => setTagSortBy('count')}
className={`px-2 py-1 rounded-btn font-semibold transition-colors cursor-pointer ${
tagSortBy === 'count'
? 'bg-panel-elevated text-main shadow-xs'
: 'text-muted hover:text-main'
}`}
title="Nach Häufigkeit sortieren"
>
Häufigkeit
</button>
<button
type="button"
onClick={() => setTagSortBy('name')}
className={`px-2 py-1 rounded-btn font-semibold transition-colors cursor-pointer ${
tagSortBy === 'name'
? 'bg-panel-elevated text-main shadow-xs'
: 'text-muted hover:text-main'
}`}
title="Alphabetisch sortieren"
>
A-Z
</button>
</div>
</div>
</div>
{/* Grid of Hashtag Tiles (Kacheln) */}
{filteredTags.length === 0 ? (
<div className="p-12 text-center text-muted text-xs space-y-2 border border-border rounded-card bg-canvas">
<Hash className="w-8 h-8 text-faint mx-auto" />
<p className="font-semibold text-main text-sm">
{tagSearchQuery
? `Keine Hashtags für "${tagSearchQuery}" gefunden`
: 'Noch keine Hashtags vorhanden'}
</p>
<p className="text-faint max-w-sm mx-auto">
{tagSearchQuery
? 'Versuche einen anderen Suchbegriff oder setze die Suche zurück.'
: 'Schreibe #tag in einen beliebigen Notiz-Bullet, um Themen automatisch als Kachel zu gruppieren.'}
</p>
{tagSearchQuery && (
<button
type="button"
onClick={() => setTagSearchQuery('')}
className="btn-action inline-flex items-center gap-1 px-3 py-1 text-xs text-accent mt-2 cursor-pointer"
>
Suche zurücksetzen
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
{filteredTags.map((t) => {
const meta = tagsMetadataMap.get(t.tag.toLowerCase());
return (
<div
key={t.tag}
onClick={() => handleSelectTag(t.tag)}
className="p-4 rounded-card border border-border bg-canvas hover:border-accent hover:shadow-panel transition-all cursor-pointer group flex flex-col justify-between space-y-3"
>
<div className="space-y-2.5">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className="w-7 h-7 rounded-btn bg-accent/10 border border-accent/20 text-accent font-bold flex items-center justify-center shrink-0 text-sm group-hover:bg-accent group-hover:text-on-accent transition-colors">
#
</span>
<h3 className="font-bold text-sm text-main group-hover:text-accent transition-colors truncate">
#{t.tag}
</h3>
</div>
<span className="badge-neutral font-mono text-[10px] shrink-0">
{t.count} {t.count === 1 ? 'Treffer' : 'Treffer'}
</span>
</div>
{meta && meta.docs.length > 0 && (
<div className="space-y-1">
<span className="text-[10px] font-semibold text-faint uppercase tracking-wider block">
In {meta.docs.length} {meta.docs.length === 1 ? 'Notiz' : 'Notizen'}:
</span>
<div className="flex flex-wrap gap-1">
{meta.docs.slice(0, 3).map((d) => (
<span
key={d.id}
className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-btn bg-panel-elevated text-muted truncate max-w-[140px]"
title={d.title}
>
<span>{d.icon || '📝'}</span>
<span className="truncate">{d.title}</span>
</span>
))}
{meta.docs.length > 3 && (
<span className="text-[10px] text-faint self-center">
+{meta.docs.length - 3}
</span>
)}
</div>
</div>
)}
</div>
<div className="flex items-center justify-between pt-2.5 border-t border-border/50 text-[11px]">
<div>
{meta && meta.openTodosCount > 0 ? (
<span className="text-status-warn text-[10px] flex items-center gap-1 font-medium">
<Check className="w-2.5 h-2.5" />
{meta.openTodosCount} offene To-Dos
</span>
) : (
<span className="text-faint text-[10px]">Blöcke & Childs anzeigen</span>
)}
</div>
<span className="text-accent text-xs font-semibold group-hover:translate-x-0.5 transition-transform flex items-center gap-0.5">
<span>Details</span>
<span>→</span>
</span>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
) : subView === 'tag_detail' && selectedTag ? (
<div className="flex-1 overflow-y-auto p-4 sm:p-8 bg-canvas">
<div className="max-w-3xl mx-auto space-y-4">
<div className="flex items-center justify-between border-b border-border pb-3">
{/* Top Navigation Tabs */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-3">
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
onClick={() => setSubView('editor')}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<FileText className="w-3.5 h-3.5 text-accent" />
<span>Notizen</span>
<span className="badge-neutral text-[10px]">{docs.length}</span>
</button>
<button
type="button"
onClick={() => {
setSubView('todos');
setMobileShowDetail(true);
}}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<Check className="w-3.5 h-3.5 text-status-ok" />
<span>To-Dos</span>
<span className="badge-neutral text-[10px]">{totalOpenTodos}</span>
</button>
<button
type="button"
onClick={() => setSubView('tags')}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-panel border-accent text-main font-semibold shadow-xs"
>
<Hash className="w-3.5 h-3.5 text-accent" />
<span>Hashtags</span>
<span className="badge-neutral text-[10px]">{tagsSummary.length}</span>
</button>
</div>
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => setSubView('tags')}
className="btn-action inline-flex items-center gap-1 px-2.5 py-1 text-xs text-muted hover:text-main cursor-pointer"
>
<ArrowLeft className="w-3.5 h-3.5" />
<span>Alle Kacheln</span>
</button>
<button
type="button"
onClick={() => setSubView('editor')}
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
>
Zum Editor
</button>
</div>
</div>
{/* Tag Context Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Hash className="w-5 h-5 text-accent" />
<span className="w-7 h-7 rounded-btn bg-accent/10 border border-accent/20 text-accent font-bold flex items-center justify-center shrink-0 text-sm">
#
</span>
<h1 className="text-xl font-bold text-main">
#{selectedTag}
</h1>
</div>
<button
type="button"
onClick={() => setSubView('editor')}
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
onClick={() => setSubView('tags')}
className="text-xs text-accent hover:underline flex items-center gap-1 cursor-pointer"
>
Zurück zum Editor
<ArrowLeft className="w-3 h-3" />
<span>Zur Kachel-Übersicht</span>
</button>
</div>
@@ -1382,6 +1746,49 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
) : subView === 'todos' ? (
<div className="flex-1 overflow-y-auto p-4 sm:p-8 bg-canvas">
<div className="max-w-3xl mx-auto space-y-4">
{/* Top Navigation Tabs */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-3">
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
onClick={() => setSubView('editor')}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<FileText className="w-3.5 h-3.5 text-accent" />
<span>Notizen</span>
<span className="badge-neutral text-[10px]">{docs.length}</span>
</button>
<button
type="button"
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-panel border-accent text-main font-semibold shadow-xs"
>
<Check className="w-3.5 h-3.5 text-status-ok" />
<span>To-Dos</span>
<span className="badge-neutral text-[10px]">{totalOpenTodos}</span>
</button>
<button
type="button"
onClick={() => {
setSubView('tags');
setMobileShowDetail(true);
}}
className="h-8 px-3 text-xs font-medium border rounded-btn transition-colors flex items-center gap-1.5 cursor-pointer bg-canvas border-border text-muted hover:text-main"
>
<Hash className="w-3.5 h-3.5 text-accent" />
<span>Hashtags</span>
<span className="badge-neutral text-[10px]">{tagsSummary.length}</span>
</button>
</div>
<button
type="button"
onClick={() => setSubView('editor')}
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
>
Zum Editor
</button>
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between border-b border-border pb-3 gap-2">
<div>
<h2 className="text-lg font-bold text-main">