- 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.
74 lines
2.2 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|