Unverified Commit 86f2ae41 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(panel): sidebar foot shows current account + sign-out; reorder Account cards

parent a49443cd
Loading
Loading
Loading
Loading
+74 −14
Changes for panel/src/components/AppShell.tsx: 74 added lines, 14 removed lines.
Original line number Diff line number Diff line
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 (
    <div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2 min-w-0 shadow-sm">
      <div className="min-w-0 flex-1 leading-tight">
        <div
          className="truncate text-[13px] font-semibold text-foreground"
          title={identity?.email ?? ""}
        >
          {identity?.email ?? <span className="text-muted-foreground/40">—</span>}
        </div>
        <div className="text-[10px] text-muted-foreground/70 capitalize font-medium">
          {identity?.role ?? "user"}
        </div>
      </div>
      <button
        type="button"
        onClick={signOut}
        disabled={signingOut || !identity}
        className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-background hover:text-destructive border border-transparent hover:border-border/50 focus:outline-none transition-all active:scale-95 disabled:opacity-50"
        aria-label={t("sign_out")}
        title={t("sign_out")}
      >
        <LogOut className="h-3.5 w-3.5" />
      </button>
    </div>
  );
}

function LangToggle() {
  const { i18n } = useTranslation();

  return (
    <Select value={i18n.language} onValueChange={(v) => i18n.changeLanguage(v)}>
      <SelectPrimitive.Trigger className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-colors shrink-0">
      <SelectPrimitive.Trigger className={FOOT_ICON_BTN}>
        <Globe className="h-4 w-4" />
      </SelectPrimitive.Trigger>
      <SelectContent align="start" className="min-w-[6rem]">
@@ -71,15 +126,11 @@ function ThemeToggle() {
  return (
    <button
      onClick={toggleTheme}
      className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-all duration-200 active:scale-95 shrink-0"
      className={FOOT_ICON_BTN}
      aria-label={t("toggle_theme")}
      title={t("toggle_theme")}
    >
      {theme === "light" ? (
        <Sun className="h-4 w-4" />
      ) : (
        <Moon className="h-4 w-4" />
      )}
      {theme === "light" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
    </button>
  );
}
@@ -112,16 +163,25 @@ export function AppShell() {
          ))}
        </nav>

        <div className="mt-auto px-3 pt-4 border-t border-border/60">
          <div className="flex items-start gap-2">
            <div className="flex items-center gap-1.5 shrink-0">
        <div className="mt-auto flex flex-col gap-2 border-t border-border/50 pt-2.5">
          {/* 1. Toggles & Meta */}
          <div className="flex items-center justify-between px-1">
            <div className="flex items-center gap-1.5">
              <LangToggle />
              <ThemeToggle />
            </div>
            <span className="text-[10px] text-muted-foreground/60 leading-tight pt-1">
              {t("common:brand_tagline")}
            <span className="text-[10px] font-mono text-muted-foreground/40 select-none">
              Felis v0.1.0
            </span>
          </div>

          {/* 2. Profile Card */}
          <UserStrip />

          {/* 3. Branding Sign-off (sits tight at the absolute bottom with leading-tight and centered) */}
          <div className="px-1 text-[10px] text-muted-foreground/50 leading-tight text-center">
            {t("common:brand_tagline")}
          </div>
        </div>
      </aside>

+20 −20
Changes for panel/src/pages/Account.tsx: 20 added lines, 20 removed lines.
Original line number Diff line number Diff line
@@ -78,26 +78,6 @@ export function Account() {
        </div>
      </div>

      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2 text-base">
            <ShieldCheck className="h-4 w-4 text-primary" /> {t("session")}
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4 text-sm text-muted-foreground">
          <p>{t("session_desc")}</p>
          <Button
            variant="outline"
            size="sm"
            onClick={signOut}
            disabled={signingOut}
          >
            <LogOut className="mr-2 h-4 w-4" />
            {signingOut ? t("signing_out") : t("sign_out")}
          </Button>
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2 text-base">
@@ -122,6 +102,26 @@ export function Account() {
          )}
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2 text-base">
            <ShieldCheck className="h-4 w-4 text-primary" /> {t("session")}
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4 text-sm text-muted-foreground">
          <p>{t("session_desc")}</p>
          <Button
            variant="outline"
            size="sm"
            onClick={signOut}
            disabled={signingOut}
          >
            <LogOut className="mr-2 h-4 w-4" />
            {signingOut ? t("signing_out") : t("sign_out")}
          </Button>
        </CardContent>
      </Card>
    </>
  );
}