v4: the avatar menu shows at every width and holds Log out, so a profile keeps only Invite; the profile's dump cards lay out again — your own profile had four buttons in a row, Invite someone, Settings, Manage and Log out, Invite taller than the rest, and on wide screens it was the only way to reach /settings or /manage. The avatar menu now leads the header's icon row at every width, after the name and Playlists text links on wide screens, and ends with Log out under a separator, muted until hovered. The profile keeps Invite someone alone. The menu's button outweighs .app-header-nav button and the themes' button rules (Brutalist and GeoCities list it among their flat buttons), so it reads as one of the menu's links. Since b9ff9c4, a card under 472px is laid out by the narrow-card container query, but the profile columns' dense-card rules still placed the thumb, body and vote themselves, more specifically: the thumb lost its row span, so the title fell under it and every card was a tall, mostly empty box. Those rules now only make the card denser (a smaller thumb, a 2-line title, smaller type) and leave placement to the container query. 670 frontend tests pass, and tsc, eslint, deno fmt, doc:lint and i18n:compile --strict are clean; checked in screenshots in Smooth dark at 1400 and 390px, Brutalist light at 390px, GeoCities light and Kanagawa dark at 1400px, in French.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-08 22:22:15 +00:00
parent 10c9cf4269
commit 886216749e
11 changed files with 179 additions and 219 deletions

View File

@@ -3,7 +3,8 @@ import { screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { UserMenu } from "../../../components/layout/UserMenu.tsx";
import { makeSession } from "../../fixtures.ts";
import { currentPath, renderWithContexts } from "../../render.tsx";
import { currentPath, renderApp, renderWithContexts } from "../../render.tsx";
import { AUTH_STORAGE_KEY } from "../../../utils/auth/storage.ts";
describe("UserMenu", () => {
test("opens, links to profile and playlists, closes on Escape and outside", async () => {
@@ -41,4 +42,26 @@ describe("UserMenu", () => {
expect(currentPath()).toBe("/@ann");
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
test("links to /manage for those who hold category:manage", async () => {
const user = userEvent.setup();
renderWithContexts(
<UserMenu user={makeSession({ role: "admin" }).user} />,
);
await user.click(screen.getByRole("button", { name: "User menu" }));
expect(screen.getByRole("menuitem", { name: "Manage" })).toHaveAttribute(
"href",
"/manage",
);
});
test("logs out", async () => {
const session = makeSession({ username: "ann" });
renderApp(<UserMenu user={session.user} />, { session });
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "User menu" }));
await user.click(screen.getByRole("menuitem", { name: "Log out" }));
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
});

View File

@@ -75,8 +75,6 @@ describe("UserPublicProfile", () => {
expect(dumpsAll).toHaveAttribute("href", "/@ann/dumps");
expect(votesAll).toHaveAttribute("href", "/@ann/upvoted");
await waitFor(() => expect(document.title).toMatch(/^ann · /));
expect(screen.queryByRole("link", { name: "Settings" })).not
.toBeInTheDocument();
// The lists are read by the user's id, which a rename doesn't change.
expect(calls("GET /api/users/:u/dumps")[0].path).toBe(
"/api/users/ann/dumps",
@@ -275,11 +273,11 @@ describe("UserPublicProfile", () => {
expect(within(bob).getByRole("link", { name: /@cy/ })).toBeInTheDocument();
});
test("visitors can follow; a settings link is only on your own profile", async () => {
test("visitors can follow; inviting is only on your own profile", async () => {
open({ session: makeSession({ id: "bob" }) });
expect(await screen.findByRole("button", { name: "Follow ann" }))
.toBeInTheDocument();
expect(screen.queryByRole("link", { name: "Settings" })).not
expect(screen.queryByRole("button", { name: "+ Invite someone" })).not
.toBeInTheDocument();
});
@@ -414,29 +412,18 @@ describe("UserPublicProfile", () => {
}
});
test("your settings, and the site's tools for an admin, are a link away", async () => {
test("an admin's own profile has no manage tab: that's for others'", async () => {
open({
session: { ...annSession, user: { ...annSession.user, role: "admin" } },
user: { ...ann, role: "admin" },
});
expect(await screen.findByRole("link", { name: "Settings" }))
.toHaveAttribute("href", "/settings");
expect(screen.getByRole("link", { name: "Manage" })).toHaveAttribute(
"href",
"/manage",
);
// Their own profile has no manage tab: that's for others'.
expect(await screen.findByRole("button", { name: "+ Invite someone" }))
.toBeInTheDocument();
expect(
within(document.querySelector(".profile-tabs") as HTMLElement)
.queryByRole("button", { name: "Manage" }),
).not.toBeInTheDocument();
});
test("log out", async () => {
const { user } = open({ session: annSession });
await user.click(await screen.findByRole("button", { name: "Log out" }));
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
});
});
describe("manage", () => {