blob: fa3ad7c262e6790de1d1fe83124ae37d3c749a15 [file]
---
// Recursive, collapsible docs sidebar tree. Renders arbitrarily deep: a node
// with children becomes a <details> toggle (native fold/unfold, no JS); a leaf
// becomes a page link. Each <details> is server-rendered `open` only when the
// current page lives at or below it, so on load exactly the branch leading to
// the active page is unfolded and everything else is folded. Users can toggle
// any node freely.
export interface TreeNode {
label: string;
/** Page slug (leaf). Mutually exclusive with `children`. */
href?: string;
/** Child nodes (directory / toggle). Absent or empty ⇒ this is a leaf. */
children?: TreeNode[];
/** The current page is this node or somewhere beneath it ⇒ render open. */
hasCurrent?: boolean;
/** This leaf is the current page. */
isCurrent?: boolean;
/** Cached sort key computed by DocsLayout's `finalize` pass (min order of a
* node's subtree). Internal — set during finalization, not by callers. */
_order?: number;
}
interface Props {
nodes: TreeNode[];
base: string;
level?: number;
}
const { nodes, base, level = 0 } = Astro.props;
// Indent each level; leaves sit a touch deeper so they line up past the parent
// summary's chevron.
const pad = (l: number) => `padding-left: ${(0.35 + l * 0.8).toFixed(3)}rem`;
const summaryBase =
"tree-summary flex items-center gap-1.5 cursor-pointer rounded-md py-1 pr-2 select-none";
const summaryTone = (l: number) =>
l === 0
? "text-caption font-caption uppercase tracking-wider text-subtext-color hover:text-default-font"
: "text-body font-body text-default-font hover:bg-neutral-100";
const linkTone = (isCurrent?: boolean) =>
isCurrent
? "bg-brand-100 text-brand-800"
: "text-default-font hover:bg-neutral-100";
---
<ul class={level === 0 ? "flex flex-col gap-1.5" : "flex flex-col gap-0.5"}>
{nodes.map((node) =>
node.children && node.children.length > 0 ? (
<li>
{/* Level-0 (super-categories) stay open so the sidebar always shows at
least two levels — the categories and their sections. Deeper nodes
open only along the branch leading to the current page. */}
<details open={level === 0 || node.hasCurrent}>
<summary class={`${summaryBase} ${summaryTone(level)}`} style={pad(level)}>
<svg
class="tree-chevron flex-none"
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M9 6l6 6-6 6" />
</svg>
<span class="truncate">{node.label}</span>
</summary>
<Astro.self nodes={node.children} base={base} level={level + 1} />
</details>
</li>
) : (
<li>
<a
href={`${base}/docs/${node.href}`}
aria-current={node.isCurrent ? "page" : undefined}
class={`block rounded-md py-1 pr-2 text-body font-body capitalize ${linkTone(node.isCurrent)}`}
style={pad(level + 0.9)}
>
{node.label}
</a>
</li>
),
)}
</ul>
<style>
.tree-summary {
list-style: none;
}
.tree-summary::-webkit-details-marker {
display: none;
}
.tree-chevron {
transition: transform 0.15s ease;
color: var(--color-subtext-color);
}
details[open] > .tree-summary .tree-chevron {
transform: rotate(90deg);
}
</style>