Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const sidebars: SidebarsConfig = {
{
type: 'link',
href: '/',
label: 'LLM Inference Handbook',
label: 'Welcome',
className: 'sidebar-top-level-link',
},
{ type: 'autogenerated', dirName: '.' },
Expand Down
111 changes: 111 additions & 0 deletions src/components/DocsContentsNav/DocsContentsNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { isValidElement, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavbarSecondaryMenu } from '@docusaurus/theme-common/internal';
import type { PropSidebar } from '@docusaurus/plugin-content-docs';
import HandbookNavTree from '../MarketingHeader/HandbookNavTree';
import styles from './styles.module.scss';

// The docs sidebar tree isn't reachable via React Context from here (see
// HandbookNavTree's original home in MarketingHeaderHost), so we borrow it
// from the secondary-menu "teleport" that Docusaurus's own default
// `@theme/DocSidebar/Mobile` already feeds on every doc page, and read its
// raw props instead of rendering its (Infima-styled) element.
type SecondaryMenuProps = { sidebar?: PropSidebar; path?: string };

function useHandbookSidebarData(): { sidebar?: PropSidebar; activePath?: string } {
const { content } = useNavbarSecondaryMenu();
return useMemo(() => {
if (!isValidElement(content)) {
return {};
}
const props = content.props as SecondaryMenuProps;
return { sidebar: props.sidebar, activePath: props.path };
}, [content]);
}

export default function DocsContentsNav(): JSX.Element | null {
const { sidebar, activePath } = useHandbookSidebarData();
const [open, setOpen] = useState(false);

useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false);
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [open]);

if (!sidebar || sidebar.length === 0) {
return null;
}

return (
<div className={styles.subbar}>
{/* The page's own breadcrumb trail already renders right below this bar
(see DocBreadcrumbs in DocItem/Layout) — repeating it here just
duplicated it, so this side is a plain, static label instead. */}
<span className={styles.crumb}>LLM Inference Handbook</span>
<button
type="button"
className={styles.contentsBtn}
aria-haspopup="dialog"
aria-expanded={open}
onClick={() => setOpen(true)}
>
<ContentsIcon />
Contents
</button>

{open
? createPortal(
<div className={styles.overlay} role="dialog" aria-label="LLM Inference Handbook">
<div className={styles.overlayHeader}>
<span className={styles.overlayTitle}>LLM Inference Handbook</span>
<button
type="button"
className={styles.closeBtn}
aria-label="Close"
onClick={() => setOpen(false)}
>
<CloseIcon />
</button>
</div>
<div className={styles.overlayBody}>
<HandbookNavTree
sidebar={sidebar}
activePath={activePath ?? ''}
onNavigate={() => setOpen(false)}
/>
</div>
</div>,
document.body
)
: null}
</div>
);
}

function ContentsIcon(): JSX.Element {
return (
<svg
className={styles.contentsIcon}
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
>
<path d="M3.33301 5.83333H16.6663" stroke="currentColor" />
<path d="M3.33301 10H16.6663" stroke="currentColor" />
<path d="M3.33301 14.1667H11.6663" stroke="currentColor" />
</svg>
);
}

function CloseIcon(): JSX.Element {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M4.5 4.5L15.5 15.5" stroke="currentColor" />
<path d="M15.5 4.5L4.5 15.5" stroke="currentColor" />
</svg>
);
}
128 changes: 128 additions & 0 deletions src/components/DocsContentsNav/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
.subbar {
display: none;
}

@media (max-width: 991px) {
.subbar {
align-items: center;
background: #fff;
border-bottom: 1px solid rgba(2, 12, 19, 0.08);
display: flex;
gap: 12px;
justify-content: space-between;
// Doc pages wrap content in a `.container` that both adds top padding
// (to space it from the header) and, at several widths inside our own
// ≤991px range, switches to a narrower fixed max-width that's centered
// via auto margins rather than just padded. A static negative margin
// can't cancel that reliably, so break out of the container the classic
// viewport-relative way instead — this bleeds to the true edges at any
// width, same as the header above it.
// Standard "full-bleed" breakout: works because Infima's `.container` is
// itself horizontally centered via auto margins, so shifting by
// (50% of our own — i.e. the container's — width) minus (50% of the
// viewport) always lands exactly on the true viewport edges, regardless
// of which fixed max-width the container happens to be using at the
// current breakpoint.
// No explicit width: a block-level element's `width: auto` already fills
// the rest of the viewport after these margins, which avoids the
// horizontal-scrollbar overflow that `width: 100vw` would introduce
// (`vw` doesn't subtract the scrollbar's own width).
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
margin-top: -1rem;
padding: 10px var(--content-side-padding, 24px);
padding-top: 0;
position: sticky;
top: var(--ifm-navbar-height);
z-index: 150;
}

.crumb {
color: #676d71;
flex: 1 1 auto;
font-size: 13px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.contentsBtn {
align-items: center;
background: #fff;
border: 1px solid rgba(2, 12, 19, 0.16);
// Matches the "On this page" TOC dropdown's corner radius below it.
border-radius: var(--ifm-global-radius);
color: #020c13;
cursor: pointer;
display: inline-flex;
flex: 0 0 auto;
font-size: 13px;
font-weight: 600;
gap: 6px;
padding: 6px 14px;
}

.contentsIcon {
height: 14px;
width: 14px;
}

.overlay {
background: #fff;
bottom: 0;
display: flex;
flex-direction: column;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 300;
}

.overlayHeader {
align-items: center;
border-bottom: 1px solid rgba(2, 12, 19, 0.08);
display: flex;
flex: 0 0 auto;
justify-content: space-between;
padding: 16px var(--content-side-padding, 20px);
}

.overlayTitle {
color: #020c13;
font-size: 16px;
font-weight: 600;
}

.closeBtn {
background: transparent;
border: 0;
color: #020c13;
cursor: pointer;
display: inline-flex;
height: 24px;
padding: 0;
width: 24px;
}

.closeBtn svg {
height: 20px;
width: 20px;
}

.overlayBody {
flex: 1 1 auto;
overflow-y: auto;
padding: 16px var(--content-side-padding, 20px) 28px;
}
}

// Matches the header's own `.mh-nav` breakpoint (see MarketingHeader/styles.ts),
// which narrows its side padding from 24px to 20px at this width.
@media (max-width: 520px) {
.subbar {
padding-left: 20px;
padding-right: 20px;
}
}
82 changes: 82 additions & 0 deletions src/components/MarketingHeader/HandbookNavTree.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* Styled to match `.mh-toc-category-header` below (same font, case, color,
and padding) so "Welcome" reads as just another top-level sidebar item
instead of a visually distinct title. The 4px vertical margin mirrors
`.mh-toc-category`'s own outer spacing, keeping the gap to the first
category identical to the gap between two categories. */
.mh-toc-welcome {
color: #676d71;
display: block;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.04em;
margin: 4px 0;
padding: 10px 4px;
text-decoration: none;
text-transform: uppercase;
}

.mh-toc-welcome.current {
background: rgba(99, 123, 255, 0.12);
color: #637bff;
}

.mh-toc-category {
padding: 4px 0;
}

.mh-toc-category-header {
align-items: center;
background: none;
border: 0;
color: #676d71;
cursor: pointer;
display: flex;
font-size: 11.5px;
font-weight: 700;
justify-content: space-between;
letter-spacing: 0.04em;
padding: 10px 4px;
text-transform: uppercase;
width: 100%;
}

.mh-toc-category-header .mh-toc-chevron {
height: 7px;
transition: transform 160ms ease;
width: 7px;
}

.mh-toc-category.expanded .mh-toc-category-header .mh-toc-chevron {
transform: rotate(180deg);
}

.mh-toc-category-items {
display: none;
}

.mh-toc-category.expanded .mh-toc-category-items {
display: grid;
gap: 2px;
}

.mh-toc-article {
border-radius: 0;
color: #020c13;
display: block;
font-size: 14px;
font-weight: 500;
padding: 8px 4px 8px 18px;
text-decoration: none;
}

.mh-toc-article.current {
background: rgba(99, 123, 255, 0.12);
color: #637bff;
font-weight: 600;
}

.mh-toc-empty {
color: #676d71;
font-size: 13px;
padding: 16px 4px;
}
3 changes: 2 additions & 1 deletion src/components/MarketingHeader/HandbookNavTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
PropSidebarItem,
PropSidebarItemCategory,
} from '@docusaurus/plugin-content-docs';
import './HandbookNavTree.scss';

type Props = {
sidebar: PropSidebar;
Expand Down Expand Up @@ -46,7 +47,7 @@ function CategoryItem({
onClick={() => setExpanded((value) => !value)}
>
<span>{category.label}</span>
<svg className="mh-chevron" viewBox="0 0 7 7" fill="none" aria-hidden="true">
<svg className="mh-toc-chevron" viewBox="0 0 7 7" fill="none" aria-hidden="true">
<path d="M0.5 3L3.5 6L6.5 3" stroke="currentColor" />
</svg>
</button>
Expand Down
Loading
Loading