import { Fragment } from "react";
import Link from "next/link";
import { ExternalLink } from "lucide-react";
import { sourceAnchor, subjectMetaParts, type SourceView } from "yt-dlp-transcript-common/lib/report/views";
import type { SourceArchive } from "yt-dlp-transcript-common/lib/citations/schema";
import { reportIds } from "../../lib/reports";
// Small pieces the report, index and moment pages share.
// `2026-10-04T12:00:00Z` → `2026-10-04`; a partial date (`2026-10`) as given.
export function dateLabel(v: string | undefined): string | undefined {
if (!v) return undefined;
return /^\d{4}-\d{2}-\d{2}T/.test(v) ? v.slice(0, 10) : v;
}
export function Eyebrow({ children }: { children: React.ReactNode }) {
return
{children}
;
}
// A report's name as a heading, on its page, in the index and on the history
// page. With a series, the series is its own line in the accent (the tag,
// like a wordmark's lead) and the title the next, in regular weight — both
// inside the heading, so its name reads ""; without one, the
// title alone.
export function ReportName({ series, title }: { series?: string; title: string }) {
if (!series) return <>{title}>;
return (
<>
{series}
{" "}
{title}
>
);
}
export const textLink =
"text-brand underline decoration-brand/40 underline-offset-2 hover:decoration-brand";
export function ExternalLinkText({ href, children, title }: { href: string; children: React.ReactNode; title?: string }) {
return (
{children}
);
}
// A document a report quotes: its title (linked), byline, note, and its
// archive links in the context the document gave them — collapsed, since a
// long document may carry hundreds. The block is the document's one place on
// the page: its anchor is what a source citation's "from " links to.
// With an `accent`, its left edge wears it, as its sentences' rails do.
export function SourceBlock({ source, label }: { source: SourceView; label: string }) {
const byline = [source.publisher, source.author, dateLabel(source.date)].filter(Boolean).join(" · ");
const n = source.archives.length;
return (
);
}
// The document under review, under the report's heading: its title linked to
// the document, then " · · ", then its archive links
// folded away. No label: its left rail, in the document's colour (its
// `accent`, else the border colour) as its sentences' rails below are, says
// whose it is. It is the document's one place on the page, so it carries the
// anchor a citation's "from " links to.
export function SubjectCard({ source }: { source: SourceView }) {
// Each part stays whole on a narrow screen (a date never splits at its dashes).
const meta = subjectMetaParts(source);
const n = source.archives.length;
return (
);
}
// Archive links, each with the words around it in the document. A document
// may carry hundreds, so a link is text with a CSS arrow, not two inline SVG
// icons apiece.
export function ArchiveList({ archives, className }: { archives: readonly SourceArchive[]; className?: string }) {
return (
);
}
export function EmptyState({ title, children }: { title: string; children: React.ReactNode }) {
return (
{title}
{children}
);
}
// What a corpus-shaped route (search's chat, downloads, duplicates, offline)
// renders on a CITED site, which publishes only its reports and their moments.
// The route still exists in the static build (every route does); it shows
// nothing of the corpus and points at the home page -- the index, or the one
// report.
export function NotOnCitedSite({ title }: { title: string }) {
const one = reportIds().length === 1;
return (
This site publishes its reports only.{" "}
{one ? "Read the report" : "Read the reports"}
.
);
}