feat(design): the page band (B4) — one h1 per page, section headings, skip to content, filters fold on a short window (E28 S3d4)
- PageShell names the page with a visually hidden h1; a view with its own title renders it as PageHeading and the hidden one steps aside (contract: no hand <h1> inside the shell). - Section captions are SectionLabel headings (h2, h3 in a dialog), ui/form Section included. - "Skip to content" is the first Tab stop and focuses the scroller's <main>. - Below 600px of height the band's filter row folds into "Filters · n" (Feed, Plex, server log), the same row shown in place. - The band holds controls only: Downloads and Notifications lose their title cards, the Config and channel-discovery intros scroll with the content, the Stats tabs and the audit pager join the band, and the server log's toolbar sticks with the fade under it.
This commit is contained in:
@@ -46,6 +46,7 @@ import { useNavigationActions } from "./navigationContext";
|
||||
import { useChannelsActions } from "./channelsContext";
|
||||
import { useFeedFilters, useFeedFiltersActions } from "./feedFiltersContext";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import { SectionLabel } from "./ui/Headings";
|
||||
|
||||
// The single notification center. "System" = durable, server-backed notifications (inbox
|
||||
// phase 1). "Activity" = the client-side transient events (action confirmations, errors, with
|
||||
@@ -140,40 +141,32 @@ export default function NotificationsPanel() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* The inbox header (title + mark-all / clear-all) is fixed chrome; only the list scrolls. */}
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-2 max-w-3xl w-full mx-auto">
|
||||
<div className={`${SURFACE.panel} p-4 flex items-center gap-3`}>
|
||||
<Bell className="w-5 h-5 text-accent shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold">{t("inbox.title")}</div>
|
||||
<div className="text-xs text-muted">{t("inbox.subtitle")}</div>
|
||||
</div>
|
||||
{hasAny && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={markAll}
|
||||
disabled={!hasUnread || readAllMut.isPending}
|
||||
icon={<CheckCheck className="w-4 h-4" />}
|
||||
>
|
||||
{t("inbox.markAllRead")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={clearEverything}
|
||||
disabled={clearMut.isPending}
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
>
|
||||
{t("inbox.clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* The inbox band (B4): its two actions, fixed while the list scrolls. No title card: the nav
|
||||
rail and the tab title name the page, and the shell's hidden h1 names it to a screen reader. */}
|
||||
{hasAny && (
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-2 pb-1 max-w-3xl w-full mx-auto flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={markAll}
|
||||
disabled={!hasUnread || readAllMut.isPending}
|
||||
icon={<CheckCheck />}
|
||||
>
|
||||
{t("inbox.markAllRead")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearEverything}
|
||||
disabled={clearMut.isPending}
|
||||
icon={<Trash2 />}
|
||||
>
|
||||
{t("inbox.clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PageToolbar>
|
||||
</PageToolbar>
|
||||
)}
|
||||
|
||||
<div className="px-4 pb-4 pt-3 max-w-3xl w-full mx-auto space-y-4">
|
||||
{q.isError && !hasAny ? (
|
||||
@@ -191,9 +184,7 @@ export default function NotificationsPanel() {
|
||||
<div className="space-y-4">
|
||||
{serverItems.length > 0 && (
|
||||
<section>
|
||||
<div className="text-xs uppercase tracking-wide text-muted mb-2">
|
||||
{t("inbox.sectionSystem")}
|
||||
</div>
|
||||
<SectionLabel className="mb-2">{t("inbox.sectionSystem")}</SectionLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverItems.map((n) => (
|
||||
<NotificationRow
|
||||
@@ -211,9 +202,7 @@ export default function NotificationsPanel() {
|
||||
)}
|
||||
{clientItems.length > 0 && (
|
||||
<section>
|
||||
<div className="text-xs uppercase tracking-wide text-muted mb-2">
|
||||
{t("inbox.sectionActivity")}
|
||||
</div>
|
||||
<SectionLabel className="mb-2">{t("inbox.sectionActivity")}</SectionLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
{clientItems.map((n) => (
|
||||
<ClientActivityRow
|
||||
|
||||
Reference in New Issue
Block a user