From 86f2ae41af481f8621f33c7a1a69705dbbba0dea Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 28 Jun 2026 00:12:29 +0800 Subject: [PATCH] feat(panel): sidebar foot shows current account + sign-out; reorder Account cards --- panel/src/components/AppShell.tsx | 88 ++++++++++++++++++++++++++----- panel/src/pages/Account.tsx | 40 +++++++------- 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/panel/src/components/AppShell.tsx b/panel/src/components/AppShell.tsx index 85858d9..a7176ae 100644 --- a/panel/src/components/AppShell.tsx +++ b/panel/src/components/AppShell.tsx @@ -1,6 +1,6 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { NavLink, Outlet } from "react-router-dom"; -import { Cat, Globe, Sun, Moon } from "lucide-react"; +import { Cat, Globe, Sun, Moon, LogOut, UserRound } from "lucide-react"; import { useTranslation } from "react-i18next"; import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; @@ -8,6 +8,7 @@ import { useTier } from "@/lib/tier"; import { visibleSections, type NavSection } from "@/lib/nav"; import { Select, SelectContent, SelectItem } from "@/components/ui/select"; import { useTheme } from "@/lib/theme"; +import { api } from "@/lib/api"; function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: boolean }) { const { t } = useTranslation("navigation"); @@ -48,12 +49,66 @@ function SectionGroup({ section, isFirst }: { section: NavSection; isFirst: bool ); } +// Shared visual baseline for every clickable icon button in the sidebar foot. +// Styled with a subtle border and background to make them feel like tangible widgets, +// resolving the flat "floating icons in empty space" visual issue. +const FOOT_ICON_BTN = + "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/40 bg-muted/20 text-muted-foreground hover:bg-accent hover:text-foreground focus:outline-none transition-all duration-150 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"; + +function UserStrip() { + // The sidebar foot identifies the principal and exposes one action — sign out. + // identity?.email is the only display-safe field (user_id is a UUID, role is + // server-truth not display). While /me is loading or has failed we render a + // muted placeholder rather than a broken row, so the strip never flashes empty. + const { identity, refresh } = useTier(); + const { t } = useTranslation("account"); + const [signingOut, setSigningOut] = useState(false); + + // Mirrors Account.tsx#signOut: idempotent on the server; refresh() flips + // unauthenticated → RequireAuth bounces to /login. No navigate() needed. + async function signOut() { + if (signingOut) return; + setSigningOut(true); + try { + await api.logout(); + } finally { + await refresh(); + } + } + + return ( +
+
+
+ {identity?.email ?? —} +
+
+ {identity?.role ?? "user"} +
+
+ +
+ ); +} + function LangToggle() { const { i18n } = useTranslation(); return (