Skip to content

feat: auto-render TOC / PrevNext / Breadcrumb from a shared DocsPageState - #16

Merged
Shewart merged 7 commits into
mainfrom
feat/auto-docs-chrome
Jul 24, 2026
Merged

feat: auto-render TOC / PrevNext / Breadcrumb from a shared DocsPageState#16
Shewart merged 7 commits into
mainfrom
feat/auto-docs-chrome

Conversation

@Shewart

@Shewart Shewart commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Kills the per-page wiring ceremony consumers were paying to render TOC, PrevNext, and Breadcrumb chrome. DocsLayout now auto-renders all three from a shared DocsPageState scoped service that MarkdownContent publishes into. New primitive: <DocsBreadcrumb />. Preview app's DocsPage.razor collapses from ~15 lines of explicit chrome wiring to a single <MarkdownContent Document="_document" />.

The old ceremony

Before this branch, every consumer's DocsPage.razor had to:

@if (_document is not null)
{
    <MarkdownContent Document="_document" />
    <PrevNextNav Prev="_prev" Next="_next" />

    <SectionContent SectionName="docs-toc">
        <TableOfContents Headings="_document.Headings" />
    </SectionContent>
}

// …plus fields, plus manual (_prev, _next) = Graph.GetPrevNext(node) resolution

That's boilerplate for a docs framework to force on every consumer. And plain .md files with no accompanying DocsPage wiring got no chrome at all. And there was no DocsBreadcrumb primitive.

The new shape

DocsPage.razor (existing consumers migrate to this; new scaffolds get it out of the box via the updated CLI template):

@page "/docs/{*Path:nonfile}"
@layout DocsLayout
@inject NavigationGraph Graph
@inject MarkdownRenderer Renderer

@if (_document is not null)
{
    <MarkdownContent Document="_document" />
}
// ... not-found branch unchanged ...

That's it. TOC, PrevNext, Breadcrumb all render automatically from DocsLayout.

How it works

New scoped service DocsPageState

  • MarkdownContent calls PageState.SetDocument(doc) in OnParametersSet — so anything that renders a docs page publishes its document into the shared state.
  • The state also subscribes to NavigationManager.LocationChanged and recomputes CurrentNode / Prev / Next / Breadcrumbs from the injected NavigationGraph — so route changes propagate without waiting for MarkdownContent to re-render.
  • Fires OnChange on every recompute so subscribers (TOC, PrevNext, Breadcrumb) re-render.
  • IDisposable unsubscribes on scope teardown.

Chrome components read from state when params are omitted

  • TableOfContentsHeadings param is now optional; falls back to PageState.Document?.Headings. Subscribes to OnChange for route transitions.
  • PrevNextNavPrev/Next params optional; fall back to PageState.Prev/PageState.Next. Same subscription.
  • DocsBreadcrumb (new) — reads PageState.Breadcrumbs. Section nodes render as <span class="docs-breadcrumb-section"> (no URL, not clickable — sections aren't pages); current page as <span aria-current="page">; only leaf pages become <a> links. Hides entirely when the trail has ≤ 1 nodes.

DocsLayout auto-mounts all three

Removed the <SectionOutlet SectionName="docs-toc" /> slot indirection — the TOC now renders as <TableOfContents /> directly. Added <DocsBreadcrumb /> above @Body and <PrevNextNav /> below @Body inside .docs-content.

Scaffold template updated

ScaffoldTemplates.DocsPageRazor (emitted by shelldocs init) now mirrors the collapsed shape. Newly-scaffolded consumers get auto-chrome from t=0.

Backward compat

  • Consumers whose DocsPage.razor still explicitly wires <PrevNextNav Prev="_prev" Next="_next" /> continue to work — the explicit-param path is preserved. Prev / Next params override state.
  • Consumers whose DocsPage.razor still does <SectionContent SectionName="docs-toc"> will no longer render — the SectionOutlet is gone, so orphan SectionContent fires into the void. Recommended migration: delete the explicit wiring.
  • No breaking API changes on ShellDocsOptions or the primitives themselves.

Test plan

  • dotnet build shelldocs.slnx — clean, 0 warnings, 0 errors
  • dotnet test shelldocs.slnx129 / 129 passing (+3 new in DocsPageStateTests)
  • Preview app end-to-end sweep:
    • /docs/components/callout → breadcrumb reads "Docs > Components > Callout" (sections as text, current bold), TOC populated on right rail, prev/next cards below content
    • /docs/introduction → prev placeholder (first page in nav order), next card, TOC, breadcrumb "Docs > Introduction"
    • All primitive pages (callout, card, steps, filetree, code-group, type-table, component-preview) render with zero exceptions — no regressions from the layout refactor

New tests (DocsPageStateTests)

  • SetDocument fires OnChange and stores the document
  • Unknown URL yields null CurrentNode, null Prev/Next, empty Breadcrumbs
  • Dispose() unsubscribes cleanly

Files touched

  • src/ShellDocs.Components/DocsPageState.cs (new)
  • src/ShellDocs.Components/Chrome/DocsBreadcrumb.razor (new) + .razor.css (new)
  • src/ShellDocs.Components/ServiceCollectionExtensions.csAddScoped<DocsPageState>()
  • src/ShellDocs.Components/Content/MarkdownContent.razor — publishes to state
  • src/ShellDocs.Components/Chrome/TableOfContents.razor — state fallback + subscription
  • src/ShellDocs.Components/Chrome/PrevNextNav.razor — state fallback + subscription
  • src/ShellDocs.Components/Layouts/DocsLayout.razor — auto-renders all three, drops SectionOutlet
  • src/ShellDocs.Templates/ScaffoldTemplates.csDocsPageRazor template collapsed to match
  • examples/ShellDocs.Preview/Components/Pages/DocsPage.razor — proof-of-shape: collapsed to <MarkdownContent />
  • tests/ShellDocs.Tests/DocsPageStateTests.cs (new, 3 tests)

@Shewart
Shewart merged commit 03fbcc2 into main Jul 24, 2026
1 check passed
@Shewart
Shewart deleted the feat/auto-docs-chrome branch July 25, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant