Wartung: UI/UX-Politur fuer MC2.0 (Sidebar-Gruppierung, Kachel-Livedaten, Memory-Splitscreen und Mobile-Touch)
This commit is contained in:
@@ -174,7 +174,7 @@ export function MemoryView() {
|
||||
className="h-9 px-3 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer shadow-md shadow-primary/10">
|
||||
<Plus className="h-4 w-4" /> Eintrag
|
||||
</button>
|
||||
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg">
|
||||
<div className="flex items-center gap-1 p-1 bg-card/30 border border-border/40 rounded-lg lg:hidden">
|
||||
{([["liste", List, "Liste"], ["graph", Share2, "Graph"]] as const).map(([v, Icon, lbl]) => (
|
||||
<button key={v} onClick={() => setView(v)}
|
||||
className={cn("flex h-7 px-2.5 items-center gap-1.5 text-[11px] font-semibold rounded-md transition-all cursor-pointer",
|
||||
@@ -270,83 +270,89 @@ export function MemoryView() {
|
||||
<button onClick={() => setShowAdd(true)} className="underline hover:text-foreground">oder lieber manuell anlegen</button>.
|
||||
</p>
|
||||
</div>
|
||||
) : view === "graph" ? (
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="h-[480px] rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} onDelete={del} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
) : (
|
||||
<>
|
||||
{/* Kategorie-Filter */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-card/30 border border-border/40 rounded-xl w-fit">
|
||||
<button onClick={() => setFilter("")}
|
||||
className={cn("h-7 px-3 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer",
|
||||
!filter ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>Alle</button>
|
||||
{CATEGORIES.map((c) => {
|
||||
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||
const Icon = conf.icon
|
||||
return (
|
||||
<button key={c} onClick={() => setFilter(c)}
|
||||
className={cn("h-7 px-2.5 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1",
|
||||
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
|
||||
<Icon className="h-3 w-3" /> {conf.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center">
|
||||
Keine Einträge für die aktuellen Filterkriterien gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{[...CATEGORIES, "__other"].map((c) => {
|
||||
const list = c === "__other" ? otherItems : (grouped[c] || [])
|
||||
if (!list.length) return null
|
||||
<div className="flex flex-col lg:flex-row gap-6 items-stretch">
|
||||
{/* Liste-Sicht */}
|
||||
<div className={cn("flex-1 space-y-5 lg:max-w-md xl:max-w-xl lg:shrink-0", view === "graph" && "hidden lg:block")}>
|
||||
{/* Kategorie-Filter */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-card/30 border border-border/40 rounded-xl w-fit">
|
||||
<button onClick={() => setFilter("")}
|
||||
className={cn("h-7 px-3 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer",
|
||||
!filter ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>Alle</button>
|
||||
{CATEGORIES.map((c) => {
|
||||
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||
const Icon = conf.icon
|
||||
return (
|
||||
<div key={c}>
|
||||
{/* Sektions-Kopf je Kategorie */}
|
||||
<div className="flex items-center gap-2 mb-2 px-1">
|
||||
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
|
||||
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1.5 py-0.5">{list.length}</span>
|
||||
<div className="ml-1 h-px flex-1 bg-border/30" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{list.map((m) => {
|
||||
const isAuto = AUTO_SOURCES.has(m.source)
|
||||
return (
|
||||
<div key={m.id} className={cn("flex items-start justify-between gap-4 p-3.5 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 hover:border-primary/20 transition-all group",
|
||||
BORDER_CLASSES[m.category] || "border-l-muted")}>
|
||||
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{typeof m.score === "number" && (
|
||||
<span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span>
|
||||
)}
|
||||
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded",
|
||||
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
|
||||
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
|
||||
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
|
||||
</span>
|
||||
<button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<button key={c} onClick={() => setFilter(c)}
|
||||
className={cn("h-7 px-2.5 rounded-lg text-[10px] font-semibold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1",
|
||||
filter === c ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground")}>
|
||||
<Icon className="h-3 w-3" /> {conf.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-xs text-muted-foreground border border-dashed border-border/60 rounded-2xl p-12 text-center bg-card/10">
|
||||
Keine Einträge für die aktuellen Filterkriterien gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5 max-h-[calc(100vh-14rem)] overflow-y-auto pr-1 scrollbar-thin">
|
||||
{[...CATEGORIES, "__other"].map((c) => {
|
||||
const list = c === "__other" ? otherItems : (grouped[c] || [])
|
||||
if (!list.length) return null
|
||||
const conf = CAT_CONFIG[c] || DEFAULT_CAT
|
||||
const Icon = conf.icon
|
||||
return (
|
||||
<div key={c} className="space-y-2">
|
||||
{/* Sektions-Kopf je Kategorie */}
|
||||
<div className="flex items-center gap-2 mb-2 px-1">
|
||||
<Icon className={cn("h-3.5 w-3.5", conf.text)} />
|
||||
<span className={cn("text-[11px] font-bold uppercase tracking-wider", conf.text)}>{conf.label}</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground/60 bg-card/50 rounded px-1.5 py-0.5">{list.length}</span>
|
||||
<div className="ml-1 h-px flex-1 bg-border/30" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{list.map((m) => {
|
||||
const isAuto = AUTO_SOURCES.has(m.source)
|
||||
return (
|
||||
<div key={m.id} className={cn("flex items-start justify-between gap-4 p-3.5 rounded-xl border border-l-4 bg-card/45 backdrop-blur-md border-border/60 hover:border-primary/20 transition-all group",
|
||||
BORDER_CLASSES[m.category] || "border-l-muted")}>
|
||||
<span className="text-xs text-foreground leading-relaxed break-words whitespace-pre-wrap flex-1 min-w-0">{m.content}</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{typeof m.score === "number" && (
|
||||
<span className="text-[9px] font-mono text-primary bg-primary/10 px-1.5 py-0.5 rounded" title="Relevanz der semantischen Suche">{Math.round(m.score * 100)}%</span>
|
||||
)}
|
||||
<span className={cn("flex items-center gap-1 text-[9px] font-mono px-1.5 py-0.5 rounded",
|
||||
isAuto ? "text-emerald-400 bg-emerald-500/10" : "text-muted-foreground/60 bg-background/20")}
|
||||
title={isAuto ? "Automatisch gelernt" : "Manuell angelegt"}>
|
||||
{isAuto && <Sparkles className="h-2.5 w-2.5" />}{m.source}
|
||||
</span>
|
||||
<button onClick={() => del(m.id)} aria-label="Eintrag löschen" title="Eintrag löschen"
|
||||
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all opacity-0 group-hover:opacity-100">
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Graph-Sicht */}
|
||||
<div className={cn("flex-1", view === "liste" && "hidden lg:block")}>
|
||||
<GraphErrorBoundary>
|
||||
<Suspense fallback={<div className="h-[480px] rounded-2xl border border-border/60 bg-card/30 flex items-center justify-center text-xs text-muted-foreground">Graph wird geladen…</div>}>
|
||||
<GraphView data={graphData} onDelete={del} />
|
||||
</Suspense>
|
||||
</GraphErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialogElement}
|
||||
|
||||
Reference in New Issue
Block a user