Files
siftlode/frontend/src/components/ExcludedChannelList.tsx
T
peter eddeca82d3 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.
2026-09-26 00:39:01 +02:00

74 lines
2.2 KiB
TypeScript

import { type ReactNode } from "react";
import ChannelLink from "./ChannelLink";
import NavGrid from "./ui/NavGrid";
import { navItemAttrs } from "../lib/listNav";
import { Button } from "./ui/Button";
import { SURFACE } from "./ui/surfaces";
import { SectionLabel } from "./ui/Headings";
// The presentation shared by the channel manager's "excluded channel" tabs — Hidden and Blocked.
// A responsive grid of proper channel cards (avatar + name + open-on-YouTube, via ChannelLink) each
// with one reversing action, instead of a bare row of name chips. Both tabs pass the same shape; only
// the header icon/label, the action, and the copy differ.
export interface ExcludedChannel {
id: string;
title: string | null;
handle?: string | null;
thumbnail_url: string | null;
}
export default function ExcludedChannelList({
channels,
icon,
title,
hint,
emptyText,
actionLabel,
actionIcon,
onAction,
onView,
}: {
channels: ExcludedChannel[];
icon: ReactNode;
title: string;
hint: string;
emptyText: string;
actionLabel: string;
actionIcon: ReactNode;
onAction: (id: string) => void;
onView?: (c: ExcludedChannel) => void;
}) {
if (channels.length === 0) return <p className="text-sm text-muted py-8">{emptyText}</p>;
return (
<>
<div className="flex items-center gap-2 mb-3">
{icon}
<SectionLabel>{title}</SectionLabel>
</div>
<NavGrid className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{channels.map((c, i) => (
<div
key={c.id}
{...navItemAttrs(i)}
className={`${SURFACE.card} flex items-center gap-2 px-3 py-2`}
>
<div className="min-w-0 flex-1">
<ChannelLink
id={c.id}
title={c.title}
handle={c.handle}
thumbnailUrl={c.thumbnail_url}
onView={onView ? () => onView(c) : undefined}
/>
</div>
<Button variant="outline" size="xs" icon={actionIcon} onClick={() => onAction(c.id)}>
{actionLabel}
</Button>
</div>
))}
</NavGrid>
<p className="text-2xs text-muted mt-3">{hint}</p>
</>
);
}