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:
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user