feat: complete UI overhaul (liquid glass theme, i18n, FontAwesome)

This commit is contained in:
2026-06-03 22:00:37 +02:00
parent e5a9936066
commit d34d34565f
23 changed files with 732 additions and 580 deletions
+56 -75
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { Clock, History, Trash2, X } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClock, faHistory, faTrashAlt, faTimes } from "@fortawesome/free-solid-svg-icons";
import {
clearHistory,
formatRelative,
@@ -9,88 +10,72 @@ import {
type HistoryEntry,
} from "../lib/history";
import type { InputValues } from "../lib/types";
import { useI18n } from "../lib/i18n";
interface HistoryPanelProps {
onLoad: (entry: HistoryEntry) => void;
}
export function HistoryPanel({ onLoad }: HistoryPanelProps) {
const [open, setOpen] = useState(false);
const [entries, setEntries] = useState<HistoryEntry[]>([]);
const { t } = useI18n();
// Refresh the list whenever the panel is opened.
useEffect(() => {
if (!open) return;
setEntries(loadHistory());
}, [open]);
}, []);
// And whenever the history is updated from elsewhere in the app (after a
// successful generation, or a delete/clear from a different panel).
useEffect(() => {
const handler = () => {
if (!open) return;
setEntries(loadHistory());
};
window.addEventListener(HISTORY_UPDATED_EVENT, handler);
return () => window.removeEventListener(HISTORY_UPDATED_EVENT, handler);
}, [open]);
if (entries.length === 0 && !open) {
return null;
}
}, []);
return (
<div className="mt-4">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between gap-2 px-3 py-2 rounded-lg border border-border bg-bg-card/50 hover:bg-bg-hover transition-colors text-sm"
aria-expanded={open}
>
<span className="flex items-center gap-2 text-fg">
<History className="w-4 h-4" />
Recent generations
<div className="flex flex-col h-full bg-bg-card/40 backdrop-blur-md border border-border rounded-xl shadow-lg overflow-hidden">
<div className="px-4 py-3 border-b border-border bg-bg/20">
<span className="flex items-center gap-2 text-sm font-bold text-fg">
<FontAwesomeIcon icon={faHistory} className="text-accent-secondary" />
{t.history}
{entries.length > 0 && (
<span className="text-xs text-fg-muted">({entries.length})</span>
<span className="text-[10px] bg-accent-primary/20 text-accent-primary px-1.5 py-0.5 rounded-full">
{entries.length}
</span>
)}
</span>
<span className="text-xs text-fg-muted">{open ? "Hide" : "Show"}</span>
</button>
</div>
{open && (
<div className="mt-2 rounded-lg border border-border bg-bg-card/30 divide-y divide-border">
{entries.length === 0 ? (
<p className="px-3 py-4 text-xs text-fg-muted text-center">
No recent generations yet.
</p>
) : (
<>
{entries.map((e) => (
<HistoryRow
key={e.id}
entry={e}
onLoad={() => {
onLoad(e);
setOpen(false);
}}
onRemove={() => setEntries(removeFromHistory(e.id))}
/>
))}
<div className="px-3 py-2 flex justify-end">
<button
type="button"
onClick={() => {
clearHistory();
setEntries([]);
}}
className="inline-flex items-center gap-1 text-xs text-fg-muted hover:text-rose-300 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
Clear all
</button>
</div>
</>
)}
<div className="flex-1 overflow-y-auto scrollbar-thin p-2 space-y-1">
{entries.length === 0 ? (
<p className="px-3 py-6 text-xs text-fg-muted text-center italic">
{t.noHistory}
</p>
) : (
entries.map((e) => (
<HistoryRow
key={e.id}
entry={e}
onLoad={() => onLoad(e)}
onRemove={() => setEntries(removeFromHistory(e.id))}
/>
))
)}
</div>
{entries.length > 0 && (
<div className="p-2 border-t border-border bg-bg/20">
<button
type="button"
onClick={() => {
clearHistory();
setEntries([]);
}}
className="w-full inline-flex items-center justify-center gap-2 text-xs font-semibold text-fg-muted hover:text-rose-400 hover:bg-rose-400/10 transition-colors py-2 rounded-lg"
>
<FontAwesomeIcon icon={faTrashAlt} />
{t.history === 'History' ? 'Clear history' : 'Verlauf löschen'}
</button>
</div>
)}
</div>
@@ -108,31 +93,27 @@ function HistoryRow({
}) {
const summary = summarizeInput(entry.input);
return (
<div className="px-3 py-2 flex items-center justify-between gap-2 text-sm hover:bg-bg-hover/40 transition-colors">
<button
type="button"
onClick={onLoad}
className="flex-1 text-left min-w-0"
>
<div className="truncate text-fg">{summary}</div>
<div className="flex items-center gap-2 text-[11px] text-fg-muted mt-0.5">
<Clock className="w-3 h-3" />
<div className="group px-3 py-2.5 flex items-center justify-between gap-2 text-sm hover:bg-bg-hover/80 rounded-lg transition-colors cursor-pointer" onClick={onLoad}>
<div className="flex-1 min-w-0">
<div className="truncate text-fg font-medium text-[13px]">{summary}</div>
<div className="flex items-center gap-1.5 text-[10px] text-fg-muted mt-1 uppercase tracking-wider font-semibold">
<FontAwesomeIcon icon={faClock} />
<span>{formatRelative(entry.timestamp)}</span>
{entry.input.mood && (
<>
<span>·</span>
<span className="opacity-50">·</span>
<span className="truncate">{entry.input.mood}</span>
</>
)}
</div>
</button>
</div>
<button
type="button"
onClick={onRemove}
className="text-fg-muted hover:text-rose-300 transition-colors p-1"
onClick={(e) => { e.stopPropagation(); onRemove(); }}
className="opacity-0 group-hover:opacity-100 text-fg-muted hover:text-rose-400 transition-all p-1.5 rounded-md hover:bg-rose-400/10"
aria-label="Remove from history"
>
<X className="w-3.5 h-3.5" />
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
);
@@ -140,7 +121,7 @@ function HistoryRow({
function summarizeInput(v: InputValues): string {
const idea = v.idea.trim();
const max = 90;
const max = 60;
if (idea.length <= max) return idea || "(no idea text)";
return idea.slice(0, max - 1) + "…";
}