app/components/site-header.tsx
179 lines and 12 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| glyph | 25 to 29 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| inside | 37 to 37 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| NavEntry | 45 to 67 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| sheetLinks | 72 to 87 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:77> | 77 to 77 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:78> | 78 to 81 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:86> | 86 to 86 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| SheetRow | 89 to 100 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| SiteHeader | 110 to 191 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:122> | 122 to 124 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:163> | 163 to 172 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:167> | 167 to 169 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/components/enhance.tsx, app/components/search-trigger.tsx, app/components/site-logo.tsx, app/components/site-nav-panel.tsx, app/components/site-speculation.tsx, app/components/theme-toggle.tsx, app/lib/nav.ts, app/lib/seo.ts. Imported by app/components/error-page.tsx, app/components/page-shell.tsx, app/components/taxonomy-listing.tsx, app/routes/blog.$slug.tsx, app/routes/blog._index.tsx, app/routes/home.tsx, app/routes/publications.$slug.tsx, app/routes/publications.tsx, app/routes/search.tsx.
Source
import { Link, useLocation } from "react-router";
import { Enhance } from "~/components/enhance";
import { SearchTrigger } from "~/components/search-trigger";
import { SiteLogoHeader } from "~/components/site-logo";
import { Icon, SiteNavPanel } from "~/components/site-nav-panel";
import { SiteSpeculation } from "~/components/site-speculation";
import { HeaderTip, ThemeToggle } from "~/components/theme-toggle";
import { buildNav, type Menu, type NavIcon, type NavItem } from "~/lib/nav";
import { SITE } from "~/lib/seo";
const NAV_ID = "site-nav";
const SHEET_ID = "site-sheet";
const SHEET_TITLE_ID = "site-sheet-title";
/*
* Invoker commands (`commandfor`, `command`): the menu button opens the sheet and its Close button closes it
* with no script, where the browser has them. React 19 has no props for them yet, so they are spread as
* plain attributes, which React writes as they are.
*/
const opens = { command: "show-modal", commandfor: SHEET_ID } as Record<string, string>;
const closes = { command: "close", commandfor: SHEET_ID } as Record<string, string>;
const glyph = (body: React.ReactNode) => (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
{body}
</svg>
);
const CHEVRON = (
<svg className="cap-site-header-chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6" />
</svg>
);
const inside = (item: NavItem, pathname: string) => pathname === item.to || pathname.startsWith(`${item.to}/`);
/**
* A section and the panel that hangs from it: Capsomer's disclosure button (site-header.md, "Markup"), not a
* link and not a menuitem, because everything the panel shows is a link. `popoverTarget` opens the panel with
* no script; with script, Capsomer's behaviour takes the click (it cancels the native toggle) and writes
* `aria-expanded`. Inside the section the button carries the current page's underline (`data-current`).
*/
function NavEntry({ item, pathname }: { item: NavItem; pathname: string }) {
const here = inside(item, pathname);
const panel = `${NAV_ID}-${item.menu.id}`;
return (
<li data-nav-menu="">
<button
type="button"
className="cap-site-header-link"
data-cap-part="panel-trigger"
popoverTarget={panel}
aria-expanded="false"
aria-controls={panel}
data-nav-button=""
data-current={here ? "" : undefined}
data-section={here ? "" : undefined}
>
<span className="site-nav-button-label">{item.label}</span>
{CHEVRON}
</button>
<SiteNavPanel id={panel} label={item.label} menu={item.menu} pathname={pathname} />
</li>
);
}
type SheetLink = { to: string; label: string; icon?: NavIcon };
/** A section's links for the phone's sheet, grouped as its desktop panel groups them, each once. */
function sheetLinks(menu: Menu): SheetLink[] {
const [hub, ...quick] = menu.footer;
const all: SheetLink[] = [
...(hub ? [hub] : []),
...menu.tiles,
...menu.tiles.flatMap((tile) => (tile.also ? [tile.also] : [])),
...menu.rows.flatMap((row) => [
...(row.to ? [{ to: row.to, label: row.title, icon: row.icon }] : []),
...(row.links ?? []),
]),
...(menu.spotlight ? [{ to: menu.spotlight.to, label: menu.spotlight.title }] : []),
...quick,
];
const seen = new Set<string>();
return all.filter((link) => (seen.has(link.to) ? false : (seen.add(link.to), true)));
}
function SheetRow({ link, pathname }: { link: SheetLink; pathname: string }) {
return (
<li>
<Link to={link.to} aria-current={link.to === pathname ? "page" : undefined}>
<span className="cap-site-header-sheet-icon" aria-hidden="true">
{link.icon ? <Icon name={link.icon} /> : null}
</span>
<span className="cap-site-header-sheet-name">{link.label}</span>
</Link>
</li>
);
}
/**
* Capsomer's site header (capsomer/components/site-header/site-header.md), written by hand because the public
* plane does not hydrate: 56px, sticky, full width; the site's logo, its five sections on the right after the
* divider, then Search and the theme, with the avatar's slot held empty after it so the theme sits where the admin's does. Under 768px it is one row, and a Menu button opens
* the sheet with every section's links. Every link is in this markup with no script; app/enhance/header.ts
* wires Capsomer's behaviour (the panels, the tips, the sheet) on top. No `prefetch="intent"`: it needs React
* handlers and this plane does not hydrate, so hover prefetch comes from `SiteSpeculation`.
*/
export function SiteHeader() {
const { pathname } = useLocation();
const nav = buildNav();
return (
<header className="cap-site-header site-header" data-cap="site-header" data-links="right" data-site-header="">
<Link to="/" className="cap-site-header-logo site-header-brand">
{/* Decorative: the wordmark beside it is the link's name, so naming the mark would read twice. */}
<SiteLogoHeader className="site-header-mark" />
{SITE.name}
</Link>
<nav className="cap-site-header-sections site-header-nav" id={NAV_ID} aria-label="Site" data-site-nav="">
<ul>
{nav.map((item) => (
<NavEntry key={item.to} item={item} pathname={pathname} />
))}
</ul>
</nav>
<div className="cap-site-header-tools site-header-tools">
<div className="cap-site-header-extras">
<SearchTrigger />
</div>
<ThemeToggle />
{/* The avatar's slot, held empty: the admin header has the avatar here, so the theme button sits at the
same place on every page. Sign in is the footer's Admin link (Dustin, 2026-10-15). */}
<div className="cap-site-header-account site-header-account-slot" aria-hidden="true" />
<button
type="button"
className="cap-site-header-icon cap-site-header-menu"
aria-label="Menu"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls={SHEET_ID}
data-header-menu=""
{...opens}
>
{glyph(<path d="M4 7h16M4 12h16M4 17h16" />)}
<HeaderTip text="Menu" />
</button>
</div>
<dialog
className="cap-dialog cap-site-header-sheet"
data-cap="dialog"
data-placement="bottom"
id={SHEET_ID}
aria-labelledby={SHEET_TITLE_ID}
>
<div className="cap-dialog-header">
<h2 id={SHEET_TITLE_ID} className="cap-dialog-title cap-site-header-sheet-title" tabIndex={-1}>
{SITE.name} menu
</h2>
</div>
<div className="cap-dialog-body">
<nav aria-labelledby={SHEET_TITLE_ID}>
{nav.map((item) => (
<div key={item.to}>
<div className="cap-site-header-sheet-label">{item.label}</div>
<ul className="cap-site-header-sheet-list">
{sheetLinks(item.menu).map((link) => (
<SheetRow key={link.to} link={link} pathname={pathname} />
))}
</ul>
</div>
))}
</nav>
</div>
<button
type="button"
className="cap-site-header-icon cap-dialog-close"
data-cap-part="close"
aria-label="Close"
{...closes}
>
{glyph(<path d="M6 6l12 12M18 6 6 18" />)}
</button>
</dialog>
{/* Here rather than in root's Layout, so it never reaches the admin plane. */}
<SiteSpeculation />
{/* Absent, every link is still in the markup and every panel still opens; see app/enhance/header.ts. */}
<Enhance module="header" />
</header>
);
}