feat(ui): E31 S4b — the admin pages behind one rail row, Configuration as a sectioned list, Statistics as tiles and bars
- NavRailGroup: one "Admin" rail row opening a panel of the admin pages (links, so a middle-click still opens a tab); the rail fits 133% zoom without a hidden scroll - sysconfig.SECTIONS orders the config groups under four headings (every group in exactly one, test-held); the page shows them as Settings' side list (Tabs `group` headings); the lone registration group reads "Registration" under "Access" - StatTiles / StatTile / StatBars: the Statistics overview's figures as tiles, the per-operation quota use as horizontal bars (a zero draws none)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import clsx from "clsx";
|
||||
import type { CSSProperties, ReactNode, Ref } from "react";
|
||||
import { useState, type CSSProperties, type ReactNode, type Ref } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { PopoverSurface } from "./AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||
import { Pressable } from "./Pressable";
|
||||
import { SURFACE } from "./surfaces";
|
||||
import Tooltip from "./Tooltip";
|
||||
@@ -171,3 +173,115 @@ export function NavRailAction({
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/** One page of a `NavRailGroup`'s panel. */
|
||||
export interface NavRailGroupPage {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
/** The page's full name, written out in the panel. */
|
||||
label: string;
|
||||
href: string;
|
||||
active: boolean;
|
||||
/** The in-app navigation, run only for a plain left click. */
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
/** One rail row standing for a set of rare pages (E31, the owner's pick: the admin pages), opening a
|
||||
* panel of them to its right. The rail fits a 133–150% zoom without a scroll nobody sees: five admin
|
||||
* rows sat below the fold behind a fade alone. The row is "you are here" while any of its pages is.
|
||||
* The panel's pages are real links, as the rail's own rows are (a middle-click opens a tab), so it is a
|
||||
* `dialog` popover with its own Tab order, not a roving menu of buttons. */
|
||||
export function NavRailGroup({
|
||||
icon: Icon,
|
||||
label,
|
||||
shortLabel,
|
||||
showLabel,
|
||||
pages,
|
||||
testId,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
/** The group's full name: the tooltip, the row's and the panel's accessible name, the panel heading. */
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
showLabel: boolean;
|
||||
pages: readonly NavRailGroupPage[];
|
||||
testId?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pop = useAnchoredPopover({
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
mode: "dialog",
|
||||
role: "dialog",
|
||||
placement: "right-start",
|
||||
offset: 8,
|
||||
});
|
||||
const active = pages.some((p) => p.active);
|
||||
return (
|
||||
<>
|
||||
<Tooltip hint={label} side="right" always openDelay={350}>
|
||||
<Pressable
|
||||
ref={pop.refs.setReference}
|
||||
{...pop.getReferenceProps()}
|
||||
data-testid={testId}
|
||||
aria-label={showLabel ? shortLabel : label}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={clsx(
|
||||
RAIL_ROW,
|
||||
showLabel ? "py-1.5" : "h-9",
|
||||
active ? "bg-accent/15 text-accent" : "text-muted state-layer hover:text-fg",
|
||||
)}
|
||||
>
|
||||
{active && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute top-1.5 bottom-1.5 -left-1 w-0.75 rounded-r-full bg-accent"
|
||||
/>
|
||||
)}
|
||||
<Icon className={RAIL_GLYPH} aria-hidden="true" />
|
||||
{showLabel && (
|
||||
<span className="max-w-full truncate text-3xs font-medium tracking-tight">
|
||||
{shortLabel}
|
||||
</span>
|
||||
)}
|
||||
</Pressable>
|
||||
</Tooltip>
|
||||
<PopoverSurface
|
||||
popover={pop}
|
||||
ariaLabel={label}
|
||||
className={`${SURFACE.popover} w-56 p-1.5 animate-[popIn_0.16s_ease]`}
|
||||
>
|
||||
<div className="px-2 pt-1 pb-1.5 text-2xs font-semibold tracking-wide text-muted uppercase">
|
||||
{label}
|
||||
</div>
|
||||
<ul className="flex flex-col gap-0.5">
|
||||
{pages.map(({ id, icon: PageIcon, label: name, href, active: here, onSelect }) => (
|
||||
<li key={id}>
|
||||
<a
|
||||
data-testid={`${testId ?? "group"}-${id}`}
|
||||
href={href}
|
||||
aria-current={here ? "page" : undefined}
|
||||
onClick={(e) => {
|
||||
if (!isPlainLeftClick(e)) return; // the browser opens its tab or window
|
||||
e.preventDefault();
|
||||
setOpen(false);
|
||||
onSelect();
|
||||
}}
|
||||
className={clsx(
|
||||
"flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm transition touch-target",
|
||||
here ? "bg-accent/15 text-accent" : "text-fg state-layer",
|
||||
)}
|
||||
>
|
||||
<PageIcon
|
||||
className={clsx(RAIL_GLYPH, "shrink-0", !here && "text-muted")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="min-w-0 truncate">{name}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</PopoverSurface>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user