feat(shell): fix headers on Notifications, Downloads, Discovery

Apply the fixed-chrome pattern to three more list modules:
- Notifications: the inbox header (title + mark-all / clear-all) pins in
  the band; only the notification list scrolls.
- Downloads: the title + subtitle + tabs pin; the tab content scrolls.
- Channel discovery: the intro + table controls pin (controlsInBand), the
  table rows scroll under the sticky header (fills out the manager's
  Discover tab to match Subscriptions).

Verified: headers land in the fixed band, content in the scroller. Suite
15/15, tsc + knip clean.
This commit is contained in:
2026-07-18 06:16:16 +02:00
parent ed3990a9af
commit 685c7b4c0b
3 changed files with 45 additions and 21 deletions
+25 -17
View File
@@ -10,6 +10,7 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import ChannelLink from "./ChannelLink"; import ChannelLink from "./ChannelLink";
import DataTable, { type Column } from "./DataTable"; import DataTable, { type Column } from "./DataTable";
import { PageToolbar } from "./PageShell";
import { useConfirm } from "./ConfirmProvider"; import { useConfirm } from "./ConfirmProvider";
// The Channel manager's "Discovery" tab: channels that turn up in the user's playlists but // The Channel manager's "Discovery" tab: channels that turn up in the user's playlists but
@@ -154,22 +155,29 @@ export default function ChannelDiscovery({
]; ];
return ( return (
<div className="px-4 pb-4 pt-3 max-w-7xl mx-auto"> <>
<p className="text-xs text-muted mb-4 leading-relaxed"> {/* Intro + the table controls are fixed chrome; only the table rows scroll (sticky header). */}
{t("channels.discovery.intro")} <PageToolbar>
</p> <div className="px-4 pt-3 max-w-7xl mx-auto">
{query.isLoading ? ( <p className="text-xs text-muted leading-relaxed">{t("channels.discovery.intro")}</p>
<div className="text-muted py-8">{t("channels.discovery.loading")}</div> </div>
) : ( </PageToolbar>
<DataTable <div className="px-4 pb-4 max-w-7xl mx-auto">
rows={rows} {query.isLoading ? (
columns={columns} <div className="text-muted py-8">{t("channels.discovery.loading")}</div>
rowKey={(c) => c.id} ) : (
persistKey={accountKey(LS.channelDiscoveryTable) ?? undefined} <DataTable
controlsPosition="top" rows={rows}
emptyText={t("channels.discovery.empty")} columns={columns}
/> rowKey={(c) => c.id}
)} persistKey={accountKey(LS.channelDiscoveryTable) ?? undefined}
</div> controlsPosition="top"
controlsInBand
controlsBandClassName="px-4 max-w-7xl mx-auto"
emptyText={t("channels.discovery.empty")}
/>
)}
</div>
</>
); );
} }
+9 -1
View File
@@ -17,6 +17,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import clsx from "clsx"; import clsx from "clsx";
import Tabs, { usePersistedTab } from "./Tabs"; import Tabs, { usePersistedTab } from "./Tabs";
import { PageToolbar } from "./PageShell";
import Modal from "./Modal"; import Modal from "./Modal";
// Lazy: these dialogs/editors open on demand, so they stay out of the Downloads page chunk until // Lazy: these dialogs/editors open on demand, so they stay out of the Downloads page chunk until
// used (the video editor in particular is heavy — filmstrip + scrubber). // used (the video editor in particular is heavy — filmstrip + scrubber).
@@ -600,7 +601,10 @@ export default function DownloadCenter({ me }: { me: Me }) {
); );
return ( return (
<div className="p-4 max-w-4xl w-full mx-auto"> <>
{/* Page title + subtitle + tabs are fixed chrome; the tab content scrolls under them. */}
<PageToolbar>
<div className="px-4 pt-3 max-w-4xl w-full mx-auto">
<div className="flex items-center justify-between gap-3 mb-1"> <div className="flex items-center justify-between gap-3 mb-1">
<h1 className="text-xl font-semibold">{t("downloads.page.title")}</h1> <h1 className="text-xl font-semibold">{t("downloads.page.title")}</h1>
<button <button
@@ -613,7 +617,10 @@ export default function DownloadCenter({ me }: { me: Me }) {
<p className="text-sm text-muted mb-4">{t("downloads.page.subtitle")}</p> <p className="text-sm text-muted mb-4">{t("downloads.page.subtitle")}</p>
<Tabs tabs={tabs} active={tab} onChange={setTab} /> <Tabs tabs={tabs} active={tab} onChange={setTab} />
</div>
</PageToolbar>
<div className="px-4 pb-4 pt-3 max-w-4xl w-full mx-auto">
{tab === "queue" && ( {tab === "queue" && (
<> <>
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
@@ -739,5 +746,6 @@ export default function DownloadCenter({ me }: { me: Me }) {
{editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />} {editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />}
</Suspense> </Suspense>
</div> </div>
</>
); );
} }
+11 -3
View File
@@ -21,6 +21,7 @@ import {
} from "../lib/notifications"; } from "../lib/notifications";
import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format"; import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format";
import { LEVEL_STYLE } from "./Toaster"; import { LEVEL_STYLE } from "./Toaster";
import { PageToolbar } from "./PageShell";
import { useNavigationActions } from "./NavigationProvider"; import { useNavigationActions } from "./NavigationProvider";
// The single notification center. "System" = durable, server-backed notifications (inbox // The single notification center. "System" = durable, server-backed notifications (inbox
@@ -102,8 +103,11 @@ export default function NotificationsPanel({
} }
return ( return (
<div className="p-4 max-w-3xl w-full mx-auto space-y-4"> <>
<div className="glass rounded-2xl p-4 flex items-center gap-3"> {/* The inbox header (title + mark-all / clear-all) is fixed chrome; only the list scrolls. */}
<PageToolbar>
<div className="px-4 pt-3 max-w-3xl w-full mx-auto">
<div className="glass rounded-2xl p-4 flex items-center gap-3">
<Bell className="w-5 h-5 text-accent shrink-0" /> <Bell className="w-5 h-5 text-accent shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold">{t("inbox.title")}</div> <div className="font-semibold">{t("inbox.title")}</div>
@@ -130,7 +134,10 @@ export default function NotificationsPanel({
</div> </div>
)} )}
</div> </div>
</div>
</PageToolbar>
<div className="px-4 pb-4 pt-3 max-w-3xl w-full mx-auto space-y-4">
{q.isLoading && !q.data && clientItems.length === 0 ? ( {q.isLoading && !q.data && clientItems.length === 0 ? (
<div className="p-8 text-muted text-center">{t("common.loading")}</div> <div className="p-8 text-muted text-center">{t("common.loading")}</div>
) : !hasAny ? ( ) : !hasAny ? (
@@ -185,7 +192,8 @@ export default function NotificationsPanel({
)} )}
</div> </div>
)} )}
</div> </div>
</>
); );
} }