This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

Navigation components

TableOfContentsNavigation and OutlineNavigation are the two Razor components in Pennington.UI that render, respectively, the sidebar page tree and the floating in-page heading outline. Both live in namespace Pennington.UI.Components.Navigation and are consumed by Pennington.DocSite's MainLayout, but are available to any host referencing Pennington.UI. TableOfContentsNavigation binds to an ImmutableList<NavigationTreeItem> produced by NavigationBuilder; OutlineNavigation binds to a client-side DOM selector at runtime. Neither accepts a NavigationInfo directly.

TableOfContentsNavigation

Declaration

@if (TableOfContents != null)
{
    <nav>
        <ul class="flex flex-col @ListGapClass">
            @foreach (var tableOfContentEntry in TableOfContents.OrderBy(i => i.Order))
            {
                @TocEntry(tableOfContentEntry)
            }
        </ul>
    </nav>
}
  
@code {
    /// <summary>Navigation tree to render; when null the component renders nothing, and entries are sorted by <see cref="NavigationTreeItem.Order"/> at each level.</summary>
    [Parameter] public ImmutableList<NavigationTreeItem>? TableOfContents { get; set; }
  
    /// <summary>Optional label forwarded from the caller's <c>NavigationInfo.SectionName</c>; not rendered by the default template.</summary>
    [Parameter] public string? SectionLabel { get; set; }
  
    /// <summary>CSS classes applied to the outer <c>&lt;ul&gt;</c> that holds the top-level navigation entries.</summary>
    [Parameter] public string ListGapClass { get; set; } = "gap-4";
  
    /// <summary>CSS classes applied to the nested <c>&lt;ul&gt;</c> that holds a section's child entries.</summary>
    [Parameter] public string ChildListClass { get; set; } = "mt-4";
  
    /// <summary>Layout and typography classes applied to the section-header element.</summary>
    [Parameter] public string SectionHeaderStructureClass { get; set; } = "font-display font-medium first:pt-0";
  
    /// <summary>CSS classes applied to section-header text — both the plain <c>&lt;div&gt;</c> for empty-route entries and the <c>&lt;a&gt;</c> when a top-level entry has children.</summary>
    [Parameter] public string SectionHeaderColorClass { get; set; } = "text-base-900 dark:text-base-50";
  
    /// <summary>Layout and typography classes applied to each child-level <c>&lt;a&gt;</c> element under a section.</summary>
    [Parameter] public string LinkStructureClass { get; set; } = "block text-sm w-full border-l pl-3.5 py-1.5";
  
    /// <summary>CSS classes applied to each child-level <c>&lt;a&gt;</c> for color and <c>data-current=true</c> state, composed after <see cref="LinkStructureClass"/>.</summary>
    [Parameter] public string LinkColorClass { get; set; } = "transition-colors transition-300 border-base-300 dark:border-base-800 data-[current=true]:border-primary-400 text-base-500 dark:text-base-400 data-[current=true]:text-primary-800 dark:data-[current=true]:text-primary-500 hover:text-accent-400 dark:hover:text-base-50";
  
    /// <summary>Layout classes applied to a leaf root-level <c>&lt;a&gt;</c> when a top-level entry has no children.</summary>
    [Parameter] public string RootLinkStructureClass { get; set; } = "block w-full py-1";
  
    /// <summary>CSS classes applied to a leaf root-level <c>&lt;a&gt;</c> (a top-level entry with no children), composed after <see cref="RootLinkStructureClass"/>.</summary>
    [Parameter] public string RootLinkColorClass { get; set; } = "transition-colors transition-300 text-base-700 dark:text-base-400 data-[current=true]:text-primary-800 dark:data-[current=true]:text-primary-500 hover:text-accent-400 dark:hover:text-base-50";
  
  
    private RenderFragment TocEntry(NavigationTreeItem tocEntry) =>
        @<li class="block">
            @if (tocEntry.Route.CanonicalPath.Value == "")
            {
                <div class="@SectionHeaderStructureClass @SectionHeaderColorClass">@tocEntry.Title</div>
            }
            else
            {
                <a data-current="@tocEntry.IsSelected.ToString().ToLowerInvariant()" href="@tocEntry.Route.CanonicalPath.Value" class="@(tocEntry.Children.Count == 0 ? RootLinkStructureClass + " " + RootLinkColorClass : SectionHeaderStructureClass + " " + SectionHeaderColorClass)">@tocEntry.Title</a>
            }
            @if (tocEntry.Children.Count > 0)
            {
                <ul class="@ChildListClass">
                    @foreach (var childEntry in tocEntry.Children.OrderBy(i => i.Order).Where(i => i.Route.CanonicalPath.Value != ""))
                    {
                        <li class="block">
                            <a data-current="@childEntry.IsSelected.ToString().ToLowerInvariant()" href="@childEntry.Route.CanonicalPath.Value" class="@LinkStructureClass @LinkColorClass">@childEntry.Title</a>
                        </li>
                    }
                </ul>
            }
        </li>;
}

Renders an ordered <nav><ul> of NavigationTreeItem entries, recursing one level into each entry's Children collection. Root entries with an empty Route.CanonicalPath render as plain section headers; entries with a path render as anchor links carrying data-current="true" when IsSelected is set.

Parameters

Name Type Default Description
TableOfContents ImmutableList<NavigationTreeItem>? null Navigation tree to render; when null the component renders nothing, and entries are sorted by NavigationTreeItem.Order at each level.
SectionLabel string? null Optional label forwarded from the caller's NavigationInfo.SectionName; not rendered by the default template.
ListGapClass string "gap-4" CSS classes applied to the outer <ul> that holds the top-level navigation entries.
ChildListClass string "mt-4" CSS classes applied to the nested <ul> that holds a section's child entries.
SectionHeaderStructureClass string "font-display font-medium first:pt-0" Layout and typography classes applied to the section-header element.
SectionHeaderColorClass string "text-base-900 dark:text-base-50" CSS classes applied to section-header text — both the plain <div> for empty-route entries and the <a> when a top-level entry has children.
LinkStructureClass string "block text-sm w-full border-l pl-3.5 py-1.5" Layout and typography classes applied to each child-level <a> element under a section.
LinkColorClass string see source CSS classes applied to each child-level <a> for color and data-current=true state, composed after LinkStructureClass.
RootLinkStructureClass string "block w-full py-1" Layout classes applied to a leaf root-level <a> when a top-level entry has no children.
RootLinkColorClass string see source CSS classes applied to a leaf root-level <a> (a top-level entry with no children), composed after RootLinkStructureClass.

Slots

This component has no RenderFragment slots; all customization is performed through the class-name parameters above.

Example

The DocSite MainLayout (src/Pennington.DocSite/Components/Layout/MainLayout.razor) instantiates TableOfContentsNavigation twice — once per area when DocSiteOptions.Areas is populated and once against the root tree otherwise — passing the tree produced by NavigationBuilder.BuildTree.

OutlineNavigation

Declaration

<div data-role="page-outline" data-content-selector="@ContentSelector" class="relative @ContainerStructureClass @ContainerColorClass">
    <div data-role="page-outline-highlighter" class="absolute transition-all duration-500 opacity-0 left-[-1px] w-[1px] bg-primary-400"></div>
    <div>
        <ul class="@ListStructureClass @ListColorClass"
            data-outline-link-structure-class="@OutlineLinkStructureClass"
            data-outline-link-color-class="@OutlineLinkColorClass">
            @* Outline links will be dynamically generated by JavaScript *@
        </ul>
    </div>
</div>
  
@code {
    /// <summary>CSS selector the client-side outline script queries to discover heading elements; must be non-empty for the outline to populate.</summary>
    [Parameter, EditorRequired] public string ContentSelector { get; set; } = "";
  
    /// <summary>Textual label accepted for parity with other outline skins; not rendered by the default template.</summary>
    [Parameter] public string Title { get; set; } = "On This Page";
  
    /// <summary>Layout and border classes applied to the outer <c>data-role="page-outline"</c> container.</summary>
    [Parameter] public string ContainerStructureClass { get; set; } = "border-l border-base-200 dark:border-base-800";
  
    /// <summary>CSS classes applied to the outer container for color treatment, composed after <see cref="ContainerStructureClass"/>.</summary>
    [Parameter] public string ContainerColorClass { get; set; } = "";
  
    /// <summary>Layout classes applied to the outline <c>&lt;ul&gt;</c>.</summary>
    [Parameter] public string ListStructureClass { get; set; } = "list-none pl-4" ;
  
    /// <summary>CSS classes applied to the <c>&lt;ul&gt;</c> that holds outline links, composed after <see cref="ListStructureClass"/>.</summary>
    [Parameter] public string ListColorClass { get; set; } = "text-neutral-500 dark:text-neutral-400";
  
    /// <summary>CSS classes emitted on the container as <c>data-outline-link-color-class</c> and applied by the client-side script to each generated <c>&lt;li&gt;&lt;a&gt;</c> for color and <c>data-selected=true</c> state.</summary>
    [Parameter] public string OutlineLinkColorClass { get; set; } = "transition-colors duration-250 hover:text-base-900 dark:hover:text-base-50/90 data-[selected=true]:text-base-800 dark:data-[selected=true]:text-base-50";
  
    /// <summary>Layout classes emitted on the container as <c>data-outline-link-structure-class</c> and applied by the client-side script to each generated <c>&lt;li&gt;&lt;a&gt;</c>.</summary>
    [Parameter] public string OutlineLinkStructureClass { get; set; } = "py-1 ml-[calc(-1*(4em-1px))] pl-[calc(4em+1px)] ";
}

Emits a data-role="page-outline" container and an empty <ul> whose items are populated client-side by scraping headings from the element matched by ContentSelector. The component performs no server-side heading extraction; the companion script in Pennington.UI/wwwroot/ reads data-content-selector, data-outline-link-structure-class, and data-outline-link-color-class to build and highlight the outline in the browser.

Parameters

ContentSelector is [EditorRequired]; all other parameters carry defaults tuned for the DocSite main-content column.

Name Type Default Description
ContentSelector string "" (required) CSS selector the client-side outline script queries to discover heading elements; must be non-empty for the outline to populate.
Title string "On This Page" Textual label accepted for parity with other outline skins; not rendered by the default template.
ContainerStructureClass string "border-l border-base-200 dark:border-base-800" Layout and border classes applied to the outer data-role="page-outline" container.
ContainerColorClass string "" CSS classes applied to the outer container for color treatment, composed after ContainerStructureClass.
ListStructureClass string "list-none pl-4" Layout classes applied to the outline <ul>.
ListColorClass string "text-neutral-500 dark:text-neutral-400" CSS classes applied to the <ul> that holds outline links, composed after ListStructureClass.
OutlineLinkColorClass string see source CSS classes emitted on the container as data-outline-link-color-class and applied by the client-side script to each generated <li><a> for color and data-selected=true state.
OutlineLinkStructureClass string see source Layout classes emitted on the container as data-outline-link-structure-class and applied by the client-side script to each generated <li><a>.

Slots

This component has no RenderFragment slots; the outline list is populated at runtime by the companion client script.

Example

The DocSite MainLayout drops a single <OutlineNavigation ContentSelector="article main" /> into the right-hand rail so the script binds to headings inside the rendered article.

Binding to NavigationInfo

NavigationInfo is the per-request record exposed by NavigationBuilder.BuildNavigationInfo; it carries SectionName, SectionRoute, Breadcrumbs, PageTitle, PreviousPage, and NextPage, not a navigation tree.

/// <summary>Page-scoped navigation context exposed to layouts and components.</summary>
/// <param name="SectionName">Label of the containing top-level section, or null if none.</param>
/// <param name="SectionRoute">Route of the containing top-level section, or null if none.</param>
/// <param name="Breadcrumbs">Breadcrumb trail from the site root to the current page.</param>
/// <param name="PageTitle">Title of the current page.</param>
/// <param name="PreviousPage">Previous page in reading order, or null at the start.</param>
/// <param name="NextPage">Next page in reading order, or null at the end.</param>
public record NavigationInfo(
    string? SectionName,
    ContentRoute? SectionRoute,
    ImmutableList<BreadcrumbItem> Breadcrumbs,
    string PageTitle,
    NavigationTreeItem? PreviousPage,
    NavigationTreeItem? NextPage
);

TableOfContentsNavigation.TableOfContents is populated from the tree returned by NavigationBuilder.BuildTree(items, currentRoute, locale), not from a NavigationInfo; NavigationInfo.SectionName is the value callers typically pass to SectionLabel. OutlineNavigation does not read NavigationInfo at all — it is a client-side component bound to a DOM selector — so previous/next navigation and breadcrumbs flow through other components or layout slots.

See also