feat(ui): account hover-popup, upload-date chips, top clear-filters bar

- Header: avatar opens an account popover (hover + click) with identity, admin
  badge and Sign out; removes the standalone logout icon.
- Sidebar: relative upload-date chips (24h/1w/1m/6m/1y) backed by max_age_days,
  with a Custom toggle revealing the from/to range; mutually exclusive.
- Sidebar: 'Filters · N active · Clear all' moved to the top; drop bottom button.
This commit is contained in:
npeter83 2026-06-11 17:39:24 +02:00
parent 1b9e4c56a2
commit 8b944504a9
2 changed files with 211 additions and 72 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import {
LayoutGrid,
List,
@ -6,6 +6,7 @@ import {
Moon,
Palette,
Search,
Shield,
Sun,
} from "lucide-react";
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
@ -129,19 +130,78 @@ export default function Header({
)}
</div>
<div className="flex items-center gap-2 pl-2">
{me.avatar_url ? (
<img src={me.avatar_url} alt="" className="w-8 h-8 rounded-full" />
) : (
<div className="w-8 h-8 rounded-full bg-card grid place-items-center text-xs">
{me.email[0]?.toUpperCase()}
</div>
)}
<IconBtn onClick={logout} title="Sign out">
<LogOut className="w-5 h-5" />
</IconBtn>
<div className="pl-1">
<AccountMenu me={me} logout={logout} />
</div>
</div>
</header>
);
}
function Avatar({ me, className = "" }: { me: Me; className?: string }) {
return me.avatar_url ? (
<img src={me.avatar_url} alt="" className={`rounded-full ${className}`} />
) : (
<div className={`rounded-full bg-card grid place-items-center font-semibold ${className}`}>
{(me.display_name?.[0] ?? me.email[0] ?? "?").toUpperCase()}
</div>
);
}
function AccountMenu({ me, logout }: { me: Me; logout: () => void }) {
const [open, setOpen] = useState(false);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
function openNow() {
if (closeTimer.current) clearTimeout(closeTimer.current);
setOpen(true);
}
function closeSoon() {
closeTimer.current = setTimeout(() => setOpen(false), 220);
}
return (
<div
className="relative"
onMouseEnter={openNow}
onMouseLeave={closeSoon}
>
<button
onClick={() => setOpen((o) => !o)}
title={me.email}
className="block rounded-full ring-2 ring-transparent hover:ring-border focus:outline-none focus:ring-accent transition"
>
<Avatar me={me} className="w-8 h-8 text-xs" />
</button>
{open && (
<div className="absolute right-0 mt-2 w-64 rounded-xl border border-border bg-surface shadow-2xl p-3 z-30">
<div className="flex items-center gap-3 pb-3 border-b border-border">
<Avatar me={me} className="w-10 h-10 text-sm shrink-0" />
<div className="min-w-0">
<div className="text-sm font-semibold truncate">
{me.display_name ?? me.email.split("@")[0]}
</div>
<div className="text-xs text-muted truncate">{me.email}</div>
</div>
</div>
{me.role === "admin" && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-medium text-accent">
<Shield className="w-3.5 h-3.5" />
Admin
</div>
)}
<button
onClick={logout}
className="mt-2 w-full flex items-center gap-2 text-sm px-2 py-2 rounded-lg text-muted hover:text-fg hover:bg-card transition"
>
<LogOut className="w-4 h-4" />
Sign out
</button>
</div>
)}
</div>
);
}