From 0a40d7627f7e9a9d6e0806098f2f437ce40284db Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Apr 2026 20:19:53 +0200 Subject: [PATCH] feat: add user menu dropdown with settings link and sign out Replace the plain "Sign out" button in the header with a user icon that opens a dropdown menu containing Settings and Sign out options. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-04-03-user-menu-design.md | 35 ++++++++++++ src/client/components/TotalsBar.tsx | 32 ++++------- src/client/components/UserMenu.tsx | 57 +++++++++++++++++++ 3 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 docs/superpowers/specs/2026-04-03-user-menu-design.md create mode 100644 src/client/components/UserMenu.tsx diff --git a/docs/superpowers/specs/2026-04-03-user-menu-design.md b/docs/superpowers/specs/2026-04-03-user-menu-design.md new file mode 100644 index 0000000..33f5256 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-user-menu-design.md @@ -0,0 +1,35 @@ +# User Menu Design + +## Summary + +Replace the plain "Sign out" button in the header with a user icon that opens a dropdown menu containing Settings and Sign out options. This provides a way to navigate to the Settings page from the header. + +## Components + +### UserMenu (`src/client/components/UserMenu.tsx`) + +New component rendered by `TotalsBar` when authenticated. + +**Trigger:** Circular `CircleUser` icon button (Lucide). Styled consistently with surrounding header elements. + +**Dropdown:** Absolutely-positioned popover anchored to the right edge, appearing below the icon: + +1. **Settings** — `Settings` (gear) icon + "Settings" label, `` +2. **Divider** — thin horizontal line +3. **Sign out** — `LogOut` icon + "Sign out" label, calls `logout.mutate()` from `useLogout()` + +**Behavior:** +- Click icon toggles open/close +- Click outside closes (via `useEffect` with document click listener) +- Clicking a menu item closes the dropdown +- Dropdown anchored right so it doesn't overflow viewport + +### TotalsBar Changes (`src/client/components/TotalsBar.tsx`) + +- When `isAuthenticated`: render `` in place of the current "Sign out" button +- When not authenticated: keep the existing "Sign in" link unchanged +- Remove the `useLogout` hook usage from TotalsBar (moved into UserMenu) + +## No Backend Changes + +The existing `/api/auth/me` endpoint and `useAuth` hook are sufficient. No username display needed — using a generic user icon. diff --git a/src/client/components/TotalsBar.tsx b/src/client/components/TotalsBar.tsx index 1ad86d3..40e6aae 100644 --- a/src/client/components/TotalsBar.tsx +++ b/src/client/components/TotalsBar.tsx @@ -1,10 +1,11 @@ import { Link } from "@tanstack/react-router"; -import { useAuth, useLogout } from "../hooks/useAuth"; +import { useAuth } from "../hooks/useAuth"; import { useFormatters } from "../hooks/useFormatters"; import { useUpdateSetting } from "../hooks/useSettings"; import { useTotals } from "../hooks/useTotals"; import type { WeightUnit } from "../lib/formatters"; import { LucideIcon } from "../lib/iconData"; +import { UserMenu } from "./UserMenu"; const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"]; @@ -21,7 +22,6 @@ export function TotalsBar({ }: TotalsBarProps) { const { data } = useTotals(); const { data: auth } = useAuth(); - const logout = useLogout(); const isAuthenticated = !!auth?.user; const { weight, price, unit } = useFormatters(); const updateSetting = useUpdateSetting(); @@ -104,24 +104,16 @@ export function TotalsBar({ ))} )} -
- {isAuthenticated ? ( - - ) : ( - - Sign in - - )} -
+ {isAuthenticated ? ( + + ) : ( + + Sign in + + )} diff --git a/src/client/components/UserMenu.tsx b/src/client/components/UserMenu.tsx new file mode 100644 index 0000000..4704650 --- /dev/null +++ b/src/client/components/UserMenu.tsx @@ -0,0 +1,57 @@ +import { Link } from "@tanstack/react-router"; +import { useEffect, useRef, useState } from "react"; +import { useLogout } from "../hooks/useAuth"; +import { LucideIcon } from "../lib/iconData"; + +export function UserMenu() { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + const logout = useLogout(); + + useEffect(() => { + if (!open) return; + function handleClick(e: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [open]); + + return ( +
+ + {open && ( +
+ setOpen(false)} + className="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors" + > + + Settings + +
+ +
+ )} +
+ ); +}