fix(design): the nav rail's footer rows (language, About, Sign out) share the rail row and glyph (E28)
This commit is contained in:
@@ -4,6 +4,7 @@ import { LANGUAGES, type LangCode } from "../i18n";
|
|||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "./ui/Pressable";
|
||||||
|
import { RAIL_GLYPH, RAIL_ROW } from "./ui/NavRail";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "./ui/surfaces";
|
||||||
|
|
||||||
// Compact language picker (globe + current code). Presentational: the parent decides what
|
// Compact language picker (globe + current code). Presentational: the parent decides what
|
||||||
@@ -52,20 +53,23 @@ export default function LanguageSwitcher({
|
|||||||
layer
|
layer
|
||||||
className={
|
className={
|
||||||
rail
|
rail
|
||||||
? "relative p-2 rounded-lg text-muted hover:text-fg transition touch-target"
|
? `${RAIL_ROW} h-9 text-muted hover:text-fg`
|
||||||
: "flex h-7 items-center gap-1.5 px-2.5 rounded-lg text-sm text-muted hover:text-fg transition touch-target"
|
: "flex h-7 items-center gap-1.5 px-2.5 rounded-lg text-sm text-muted hover:text-fg transition touch-target"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Globe className={rail ? "w-5 h-5" : "w-4 h-4"} />
|
|
||||||
{rail ? (
|
{rail ? (
|
||||||
/* Current-language badge (HU/EN) so the active language reads at a glance without
|
/* A rail row (F32), with the current-language badge (HU/EN) on the glyph's corner so the
|
||||||
opening the menu. The ring matches the page background to detach it from the icon. */
|
active language reads at a glance. The ring matches the page to detach it from the icon. */
|
||||||
<span className="absolute -bottom-1 -right-1 text-[0.5rem] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
|
<span className="relative">
|
||||||
{current.code.toUpperCase()}
|
<Globe className={RAIL_GLYPH} />
|
||||||
|
<span className="absolute -bottom-1.5 -right-2.5 text-[0.5rem] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
|
||||||
|
{current.code.toUpperCase()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs font-semibold uppercase">{current.code}</span>
|
<Globe className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
|
{rail ? null : <span className="text-xs font-semibold uppercase">{current.code}</span>}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<PopoverSurface popover={pop} className={`${SURFACE.menu} w-40 animate-[popIn_0.16s_ease]`}>
|
<PopoverSurface popover={pop} className={`${SURFACE.menu} w-40 animate-[popIn_0.16s_ease]`}>
|
||||||
|
|||||||
@@ -20,14 +20,20 @@ import {
|
|||||||
} from "../lib/modules";
|
} from "../lib/modules";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import { type LangCode } from "../i18n";
|
import { type LangCode } from "../i18n";
|
||||||
import { IconButton } from "./ui/IconButton";
|
|
||||||
import AvatarImg from "./ui/Avatar";
|
import AvatarImg from "./ui/Avatar";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
import LanguageSwitcher from "./LanguageSwitcher";
|
||||||
import { PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverSurface } from "./ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||||
import { Pressable } from "./ui/Pressable";
|
import { Pressable } from "./ui/Pressable";
|
||||||
import { SURFACE } from "./ui/surfaces";
|
import { SURFACE } from "./ui/surfaces";
|
||||||
import { NavRail, NavRailDivider, NavRailItem, NavRailList, RAIL_ROW } from "./ui/NavRail";
|
import {
|
||||||
|
NavRail,
|
||||||
|
NavRailAction,
|
||||||
|
NavRailDivider,
|
||||||
|
NavRailItem,
|
||||||
|
NavRailList,
|
||||||
|
RAIL_ROW,
|
||||||
|
} from "./ui/NavRail";
|
||||||
import { usePrefs } from "./prefsContext";
|
import { usePrefs } from "./prefsContext";
|
||||||
|
|
||||||
// Primary app navigation: a thin icon-only left rail. The modules used to hide under the avatar
|
// Primary app navigation: a thin icon-only left rail. The modules used to hide under the avatar
|
||||||
@@ -196,24 +202,10 @@ export default function NavSidebar({
|
|||||||
</NavRailList>
|
</NavRailList>
|
||||||
|
|
||||||
<div className="mt-2 pt-2 border-t border-border flex flex-col gap-1">
|
<div className="mt-2 pt-2 border-t border-border flex flex-col gap-1">
|
||||||
<div className="flex flex-col items-center justify-center gap-2 pb-2 mb-1 border-b border-border">
|
<div className="flex flex-col gap-1 pb-2 mb-1 border-b border-border">
|
||||||
<LanguageSwitcher variant="rail" value={language} onChange={onChangeLanguage} />
|
<LanguageSwitcher variant="rail" value={language} onChange={onChangeLanguage} />
|
||||||
<IconButton
|
<NavRailAction icon={Info} label={t("header.account.about")} onClick={onOpenAbout} />
|
||||||
label={t("header.account.about")}
|
<NavRailAction icon={LogOut} label={t("header.account.signOut")} onClick={logout} />
|
||||||
size="md"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onOpenAbout}
|
|
||||||
>
|
|
||||||
<Info className="w-5 h-5" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
label={t("header.account.signOut")}
|
|
||||||
size="md"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={logout}
|
|
||||||
>
|
|
||||||
<LogOut className="w-5 h-5" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
<NavRailItem
|
<NavRailItem
|
||||||
testId="nav-settings"
|
testId="nav-settings"
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
// E28 S3d (B7): the nav rail's row. Icon-only, its name is the module's full name (with an unread
|
// E28 S3d (B7): the nav rail's row. Icon-only, its name is the module's full name (with an unread
|
||||||
// count, which was only ever drawn); with the labels on, the name is the short label the user SEES
|
// count, which was only ever drawn); with the labels on, the name is the short label the user SEES
|
||||||
// (WCAG 2.5.3), the full one staying in the tooltip. "You are here" is aria-current plus the tint.
|
// (WCAG 2.5.3), the full one staying in the tooltip. "You are here" is aria-current plus the tint.
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import { Bell } from "lucide-react";
|
import { Bell } from "lucide-react";
|
||||||
import { NavRailItem } from "./NavRail";
|
import { NavRailAction, NavRailItem } from "./NavRail";
|
||||||
|
|
||||||
vi.mock("../../lib/hints", () => ({ hintsEnabled: () => true, subscribeHints: () => () => {} }));
|
vi.mock("../../lib/hints", () => ({ hintsEnabled: () => true, subscribeHints: () => () => {} }));
|
||||||
|
|
||||||
@@ -45,3 +47,20 @@ describe("NavRailItem", () => {
|
|||||||
expect(row.getAttribute("aria-current")).toBeNull();
|
expect(row.getAttribute("aria-current")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// F32: the rail's footer (language, About, Sign out) drew 32 and 36 squares with 16 and 20px glyphs
|
||||||
|
// beside the 36-tall full-width rows, so its spacing and glyphs broke the column. An action is a row too.
|
||||||
|
describe("NavRailAction", () => {
|
||||||
|
it("is a named rail row, not a toolbar icon button", () => {
|
||||||
|
render(<NavRailAction icon={Bell} label="About" onClick={() => {}} />);
|
||||||
|
const row = screen.getByRole("button", { name: "About" });
|
||||||
|
expect(row.className).toContain("h-9");
|
||||||
|
expect(row.className).toContain("w-full");
|
||||||
|
expect(row.querySelector("svg")?.getAttribute("class")).toContain("size-4.5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the nav rail holds no toolbar icon button", () => {
|
||||||
|
const src = readFileSync(resolve(__dirname, "../NavSidebar.tsx"), "utf8");
|
||||||
|
expect(src).not.toMatch(/<IconButton[\s>]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export function NavRailDivider() {
|
|||||||
return <div className="mx-auto my-1.5 w-7 shrink-0 border-t-2 border-border" />;
|
return <div className="mx-auto my-1.5 w-7 shrink-0 border-t-2 border-border" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The glyph of every rail row: a page, an action, the language switcher. */
|
||||||
|
export const RAIL_GLYPH = "size-4.5";
|
||||||
|
|
||||||
/** The shape of one rail row, for a row that is not a page (the account button). */
|
/** The shape of one rail row, for a row that is not a page (the account button). */
|
||||||
export const RAIL_ROW =
|
export const RAIL_ROW =
|
||||||
"relative flex w-full flex-col items-center justify-center gap-0.5 rounded-lg transition touch-target";
|
"relative flex w-full flex-col items-center justify-center gap-0.5 rounded-lg transition touch-target";
|
||||||
@@ -112,7 +115,7 @@ export function NavRailItem({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="relative shrink-0">
|
<span className="relative shrink-0">
|
||||||
<Icon className="size-4.5" aria-hidden="true" />
|
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||||||
{shown && (
|
{shown && (
|
||||||
<span className="absolute -top-2 -right-2 grid h-4 min-w-4 place-items-center rounded-full bg-accent px-1 text-3xs leading-none font-bold text-accent-fg ring-2 ring-bg">
|
<span className="absolute -top-2 -right-2 grid h-4 min-w-4 place-items-center rounded-full bg-accent px-1 text-3xs leading-none font-bold text-accent-fg ring-2 ring-bg">
|
||||||
{badge > 9 ? "9+" : badge}
|
{badge > 9 ? "9+" : badge}
|
||||||
@@ -128,3 +131,28 @@ export function NavRailItem({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A rail row that acts instead of navigating (About, Sign out): the page rows' geometry and glyph, so
|
||||||
|
* the column keeps one rhythm (F32: toolbar icon buttons here drew 32 squares with 16px glyphs). */
|
||||||
|
export function NavRailAction({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
icon: LucideIcon;
|
||||||
|
/** The tooltip and the accessible name. */
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip hint={label} side="right" always openDelay={350}>
|
||||||
|
<Pressable
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label={label}
|
||||||
|
className={clsx(RAIL_ROW, "h-9 text-muted state-layer hover:text-fg")}
|
||||||
|
>
|
||||||
|
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||||||
|
</Pressable>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user