-
}
- title={t("channels.blocked.title")}
- hint={t("channels.blocked.hint")}
- emptyText={t("channels.blocked.empty")}
- actionLabel={t("channel.unblock")}
- actionIcon={
}
- onAction={(id) => unblock.mutate(id)}
- onView={(c) => onViewChannel(c.id, c.title ?? t("channels.row.thisChannel"))}
+
+ {view === "discovery" ? (
+
-
- ) : view === "hidden" ? (
- /* Hidden channels — kept out of the Mine feed but still in the shared Library; managed here
+ ) : view === "blocked" ? (
+ /* Blocked channels — videos from these are hidden everywhere and dropped from live search. */
+
+ }
+ title={t("channels.blocked.title")}
+ hint={t("channels.blocked.hint")}
+ emptyText={t("channels.blocked.empty")}
+ actionLabel={t("channel.unblock")}
+ actionIcon={}
+ onAction={(id) => unblock.mutate(id)}
+ onView={(c) => onViewChannel(c.id, c.title ?? t("channels.row.thisChannel"))}
+ />
+
+ ) : view === "hidden" ? (
+ /* Hidden channels — kept out of the Mine feed but still in the shared Library; managed here
instead of dimmed in the subscribed table. Same card grid as Blocked (ExcludedChannelList). */
-
- }
- title={t("channels.hidden.title")}
- hint={t("channels.hidden.hint")}
- emptyText={t("channels.hidden.empty")}
- actionLabel={t("channels.hidden.unhide")}
- actionIcon={}
- onAction={channelActions.unhide}
- onView={(c) => onViewChannel(c.id, c.title ?? t("channels.row.thisChannel"))}
- />
-
- ) : (
- /* No top padding: the sticky header sits flush at the scroller top so it doesn't shift when
+
+ }
+ title={t("channels.hidden.title")}
+ hint={t("channels.hidden.hint")}
+ emptyText={t("channels.hidden.empty")}
+ actionLabel={t("channels.hidden.unhide")}
+ actionIcon={}
+ onAction={channelActions.unhide}
+ onView={(c) => onViewChannel(c.id, c.title ?? t("channels.row.thisChannel"))}
+ />
+
+ ) : (
+ /* No top padding: the sticky header sits flush at the scroller top so it doesn't shift when
`position: sticky` engages on the first scroll (the band's controls row above gives the
visual gap). */
-
- {/* Channel table — only the rows scroll; the header row is sticky, and the controls row
+
+ {/* Channel table — only the rows scroll; the header row is sticky, and the controls row
(status chips + pager) rides in the fixed band right above it (controlsInBand). The
card layout swaps the table for a virtualized grid over the same rows (its pager and
status chips ride in the fixed band above instead). */}
- {channelsQuery.isError && !channelsQuery.data ? (
- channelsQuery.refetch()}>
- {t("common.loadError")}
-
- ) : channelsQuery.isLoading ? (
-
- ) : layout === "table" ? (
- c.id}
- persistKey={accountKey(LS.channelsTable) ?? undefined}
- controlsPosition="both"
- controlsLeading={statusChips}
- controlsInBand
- controlsBandClassName="px-4 max-w-384 mx-auto"
- sort={sort}
- onSortChange={setSort}
- filters={filters}
- onFiltersChange={setFilters}
- fixedLayout
- emptyText={t("channels.empty")}
- rowProps={rowMenu.triggerProps}
- // `group`: the row's "…" reveals on its hover or focus (`.reveal`), and stays on touch.
- rowClassName={() => "group"}
- keyboardNav
- reveal={follow}
- />
- ) : (
- c.id}
- leadKey="priority"
- aboutKey="about"
- moreKey="actions"
- emptyText={t("channels.empty")}
- rowProps={rowMenu.triggerProps}
- rowClassName={() => "group"}
- />
- )}
-
-
- )}
+ {channelsQuery.isError && !channelsQuery.data ? (
+
channelsQuery.refetch()}>
+ {t("common.loadError")}
+
+ ) : channelsQuery.isLoading ? (
+
+ ) : layout === "table" ? (
+
c.id}
+ persistKey={accountKey(LS.channelsTable) ?? undefined}
+ controlsPosition="both"
+ controlsLeading={statusChips}
+ controlsInBand
+ controlsBandClassName="px-4 max-w-384 mx-auto"
+ sort={sort}
+ onSortChange={setSort}
+ filters={filters}
+ onFiltersChange={setFilters}
+ fixedLayout
+ emptyText={t("channels.empty")}
+ rowProps={rowMenu.triggerProps}
+ // `group`: the row's "…" reveals on its hover or focus (`.reveal`), and stays on touch.
+ rowClassName={() => "group"}
+ keyboardNav
+ reveal={follow}
+ />
+ ) : (
+ c.id}
+ leadKey="priority"
+ aboutKey="about"
+ moreKey="actions"
+ emptyText={t("channels.empty")}
+ rowProps={rowMenu.triggerProps}
+ rowClassName={() => "group"}
+ />
+ )}
+
+
+ )}
+
>
);
}
diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx
index d2fde2d6..e9b6e28a 100644
--- a/frontend/src/components/ConfigPanel.tsx
+++ b/frontend/src/components/ConfigPanel.tsx
@@ -9,6 +9,7 @@ import Tooltip from "./ui/Tooltip";
import InlineNotice from "./ui/InlineNotice";
import { useMe } from "../lib/useMe";
import Tabs from "./ui/Tabs";
+import { useTabPanel } from "./ui/tabPanel";
import { LoadingState, StateMessage } from "./ui/QueryState";
import { PageToolbar } from "./ui/PageShell";
import { Switch } from "./ui/form";
@@ -30,6 +31,7 @@ import { SURFACE } from "./ui/surfaces";
export default function ConfigPanel() {
const { t } = useTranslation();
+ const panel = useTabPanel();
const qc = useQueryClient();
const me = useMe();
const q = useQuery({ queryKey: qk.adminConfig(), queryFn: api.adminConfig });
@@ -191,13 +193,18 @@ export default function ConfigPanel() {
took three lines of a short window for text read once. */}