diff --git a/frontend/src/App.css b/frontend/src/App.css index 5709ef7..28ff90a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2021,6 +2021,76 @@ a.app-footer__repo:hover { } } +/* ── Reading Header ────────────────────────────────────────────────────── */ + +.reading-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 60; + height: 40px; + background: var(--color-bg-surface); + border-bottom: 1px solid var(--color-border); + transform: translateY(-100%); + transition: transform 300ms ease; + display: flex; + align-items: center; + pointer-events: none; +} + +.reading-header--visible { + transform: translateY(0); + pointer-events: auto; +} + +.reading-header__inner { + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 0 1.5rem; + display: flex; + align-items: center; + gap: 0; + overflow: hidden; + white-space: nowrap; + font-size: 0.8125rem; + line-height: 1; +} + +.reading-header__title { + color: var(--color-text-primary); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 1; + min-width: 0; +} + +.reading-header__sep { + color: var(--color-text-secondary); + flex-shrink: 0; +} + +.reading-header__section { + color: var(--color-accent); + flex-shrink: 0; +} + +@media (max-width: 600px) { + .reading-header__inner { + font-size: 0.75rem; + padding: 0 1rem; + } + + .reading-header__section { + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 1; + min-width: 0; + } +} + /* ── Table of Contents ────────────────────────────────────────────────────── */ .technique-toc { diff --git a/frontend/src/components/ReadingHeader.tsx b/frontend/src/components/ReadingHeader.tsx new file mode 100644 index 0000000..666c117 --- /dev/null +++ b/frontend/src/components/ReadingHeader.tsx @@ -0,0 +1,64 @@ +/** + * Thin sticky reading header that slides in when the user scrolls past the + * technique page title bar. Shows truncated article title + current section + * name derived from the activeId scroll-spy state. + */ + +import type { BodySectionV2 } from "../api"; +import { slugify } from "./TableOfContents"; + +interface ReadingHeaderProps { + title: string; + activeId: string; + sections: BodySectionV2[]; + visible: boolean; +} + +/** Resolve an activeId slug back to a human-readable section or subsection name. */ +function resolveActiveSection( + activeId: string, + sections: BodySectionV2[], +): string { + if (!activeId) return ""; + + for (const section of sections) { + const sectionSlug = slugify(section.heading); + if (activeId === sectionSlug) return section.heading; + + // Check compound slug for subsections: "sectionSlug--subSlug" + if (activeId.startsWith(sectionSlug + "--")) { + for (const sub of section.subsections) { + const subSlug = `${sectionSlug}--${slugify(sub.heading)}`; + if (activeId === subSlug) return sub.heading; + } + } + } + + return ""; +} + +export default function ReadingHeader({ + title, + activeId, + sections, + visible, +}: ReadingHeaderProps) { + const sectionName = resolveActiveSection(activeId, sections); + + return ( +