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:
parent
3afb284d46
commit
3e1823b08b
2 changed files with 211 additions and 72 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue