import Link from "next/link"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { resolveSocialLinks } from "yt-dlp-transcript-common/lib/site"; import { headerSocialLinks } from "yt-dlp-transcript-common/lib/socialLinks"; import { INSTANCES_URL } from "yt-dlp-transcript-common/lib/project"; import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle"; import { SocialLinks } from "yt-dlp-transcript-common/components/SocialLinks"; import { SocialScroll } from "yt-dlp-transcript-common/components/SocialScroll"; import { BrandMark } from "yt-dlp-transcript-common/components/BrandMark"; import { Wordmark } from "yt-dlp-transcript-common/components/Wordmark"; import { wordmarkWidthEm } from "yt-dlp-transcript-common/lib/wordmarkWidth"; import { currentSite } from "../lib/site"; import { headerMarkPalette } from "../lib/brand"; import { instanceMode } from "../lib/mode"; import { hasArchives } from "../lib/archives"; import { hasDuplicates } from "../lib/duplicates"; import { hasReports, reportIds } from "../lib/reports"; import { headerNavLinks } from "../lib/nav"; import { isCitedSite } from "yt-dlp-transcript-common/lib/siteSchema"; import MobileMenu from "./MobileMenu"; // The export site's masthead (every site, and the hub): the Found-line mark + // the split wordmark, the nav, a link to the Archilyzer home's official // instances, and the homepage's group — the operator's social links // (common/components/SocialLinks.tsx) and the theme toggle // (common/components/ThemeToggle.tsx, `variant="bare"`) as the group's last // key, their boxes touching, every glyph 16 px from the next. The mark's lit // line follows the site's accent (lib/brand.ts headerMarkPalette); the hub // wears the parent mark. The brand link's accessible name is the header title // exactly — the mark is decorative and the wordmark's two spans join without a // space. // // ≥ lg brand · nav · Archilyzer · group // < lg brand · group · menu trigger (the nav, and the Archilyzer link, in // the slide-out menu). The nav, the link and a long title with four // icons do not fit a 768 px bar, and the nav gives way before any // icon scrolls. // // THE HEADER KEEPS THE SITE'S NAME ON A NARROW SCREEN (the ruling of // 2026-09-28): below the switch the social row holds only the links marked // `featured` (none marked → none; the footer always shows every link); from // it the row holds every link, up to four, the marked ones kept first // (headerSocialLinks, "narrow" and "wide"). Both rows are rendered and CSS // shows one; the other is display:none, so exactly one is focusable and in // the accessibility tree. The switch is 32.5rem — 520 px at the default text // size, where every link (up to four, 44 px keys), the toggle, the menu // button and the longest real site title fit the bar (release-14.md, the // measured table) — in rem so it moves with the reader's text size, as the // keys and the title do. Below it the bar's two gaps (name → row, toggle → // menu) are 8 px, not 12 (the ruling of 2026-09-29), so every real title // shows in full at 360 px with one marked link under touch; type, the // mark–name gap and the reserved width are unchanged. The // Archilyzer link replaces the old sites dropdown and the hub backlink; it is // absent on the hub, which lists the instances itself. Changelog is in the // footer. // // THE LAST RESORTS, now rare (a very long title on a phone): the wordmark's // TEXT drops and the mark stays, exactly when the text does not fit — whatever // the site's title. Titles differ in length ("Bonnellyzer", "Rekietalyzer"), so no width // threshold is written down: the text sits in a one-line box (`h-7`, // `overflow-hidden`, `flex-wrap`) behind a zero-width strut, and a flex item // that does not fit beside the strut wraps to the box's second line, which is // clipped. The text keeps a MINIMUM WIDTH reserved from the display face's // own metrics (lib/wordmarkWidth.ts, in em, so it scales with the reader's // text size): the decision is the same in the fallback face, which is // narrower, and after Archivo loads — a title never shows and then vanishes. // A minimum, not a width: where the text renders wider than the reservation, // its box grows with it instead of clipping the last letter. The text stays // in the DOM, so the link keeps its name. // Below `lg` the brand link takes the bar's free space (basis 0, grow 1) and // can shrink to the mark alone; only then does the group give way, and its // social row scrolls in SocialScroll's box, the last resort, its END shown // first, the toggle outside it. From `lg` the link is sized by its content, // so it shrinks first by weight (`shrink-[999]`): with a very long title the // text drops before any icon scrolls there too. export default function Header() { const site = currentSite(); const isSite = instanceMode() === "site"; const socialLinks = resolveSocialLinks(site, getSettings()); const narrow = headerSocialLinks(socialLinks, "narrow").length; const wide = headerSocialLinks(socialLinks, "wide").length; // The inline nav from lg: lib/nav.ts says which links this build has. const cited = isSite && isCitedSite(site); const navLinks = headerNavLinks({ cited, singleReport: cited && reportIds().length === 1, reports: isSite && hasReports(), duplicates: !cited && hasDuplicates(), downloads: !cited && hasArchives(), }); // The sheet also takes Offline on a PWA-shipping site — gated exactly as // Footer.tsx gates it, so the two never disagree about whether this instance // has an offline mode. const menuLinks = [ ...navLinks, ...(site.pwa ? [{ href: "/offline/", label: "Offline" }] : []), ]; return (
{/* min-h-14, not h-14, and never wrapping: a fixed-height flex-wrap row put the overflow rows OUTSIDE the sticky header's background at phone widths, and the page scrolled through them. */}
{isSite && ( Archilyzer )}
{narrow > 0 && ( )} {wide > 0 && ( )}
); }