Skip to content
11 changes: 0 additions & 11 deletions examples/ShellDocs.Preview/Components/Pages/DocsPage.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/docs/{*Path:nonfile}"
@layout DocsLayout
@using Microsoft.AspNetCore.Components.Sections
@inject NavigationGraph Graph
@inject MarkdownRenderer Renderer

Expand All @@ -9,11 +8,6 @@
@if (_document is not null)
{
<MarkdownContent Document="_document" />
<PrevNextNav Prev="_prev" Next="_next" />

<SectionContent SectionName="docs-toc">
<TableOfContents Headings="_document.Headings" />
</SectionContent>
}
else
{
Expand All @@ -35,8 +29,6 @@ else

private RenderedDocument? _document;
private string _title = "";
private NavigationNode? _prev;
private NavigationNode? _next;

protected override void OnParametersSet()
{
Expand All @@ -46,14 +38,11 @@ else
{
_document = Renderer.RenderFile(node.Path);
_title = node.Title + " — ShellDocs";
(_prev, _next) = Graph.GetPrevNext(node);
}
else
{
_document = null;
_title = "Not found — ShellDocs";
_prev = null;
_next = null;
}
}
}
40 changes: 40 additions & 0 deletions src/ShellDocs.Components/Chrome/DocsBreadcrumb.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@inject DocsPageState PageState
@implements IDisposable
@using ShellDocs.Core

@if (PageState.Breadcrumbs.Count > 1)
{
<nav class="docs-breadcrumb" aria-label="Breadcrumb">
<ol class="docs-breadcrumb-list">
@for (var i = 0; i < PageState.Breadcrumbs.Count; i++)
{
var node = PageState.Breadcrumbs[i];
var isLast = i == PageState.Breadcrumbs.Count - 1;
<li class="docs-breadcrumb-item">
@if (isLast)
{
<span class="docs-breadcrumb-current" aria-current="page">@node.Title</span>
}
else
{
@if (!string.IsNullOrEmpty(node.Url))
{
<a class="docs-breadcrumb-link" href="@node.Url">@node.Title</a>
}
else
{
<span class="docs-breadcrumb-section">@node.Title</span>
}
<svg class="docs-breadcrumb-sep" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
}
</li>
}
</ol>
</nav>
}

@code {
protected override void OnInitialized() => PageState.OnChange += Rerender;
private void Rerender() => InvokeAsync(StateHasChanged);
public void Dispose() => PageState.OnChange -= Rerender;
}
40 changes: 40 additions & 0 deletions src/ShellDocs.Components/Chrome/DocsBreadcrumb.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.docs-breadcrumb {
margin: 0 0 1.25rem;
font-size: 0.8125rem;
color: var(--muted-foreground);
}

.docs-breadcrumb-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
}

.docs-breadcrumb-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}

.docs-breadcrumb-link,
.docs-breadcrumb-section {
color: var(--muted-foreground);
text-decoration: none;
transition: color 150ms;
}
.docs-breadcrumb-link:hover { color: var(--foreground); }

.docs-breadcrumb-current {
color: var(--foreground);
font-weight: 500;
}

.docs-breadcrumb-sep {
width: 0.75rem;
height: 0.75rem;
opacity: 0.6;
}
33 changes: 22 additions & 11 deletions src/ShellDocs.Components/Chrome/PrevNextNav.razor
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
@inject DocsPageState PageState
@implements IDisposable
@using ShellDocs.Core

@if (Prev is not null || Next is not null)
@{
var prev = Prev ?? PageState.Prev;
var next = Next ?? PageState.Next;
}

@if (prev is not null || next is not null)
{
<nav class="prevnext" aria-label="Previous and next">
@if (Prev is not null)
@if (prev is not null)
{
<a class="prevnext-card prev" href="@Prev.Url">
<a class="prevnext-card prev" href="@prev.Url">
<span class="prevnext-hint">
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
<span>@Prev.Title</span>
<span>@prev.Title</span>
</span>
@if (!string.IsNullOrEmpty(Prev.Description))
@if (!string.IsNullOrEmpty(prev.Description))
{
<span class="prevnext-desc">@Prev.Description</span>
<span class="prevnext-desc">@prev.Description</span>
}
</a>
}
else { <span class="prevnext-placeholder" aria-hidden="true"></span> }

@if (Next is not null)
@if (next is not null)
{
<a class="prevnext-card next" href="@Next.Url">
<a class="prevnext-card next" href="@next.Url">
<span class="prevnext-hint">
<span>@Next.Title</span>
<span>@next.Title</span>
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</span>
@if (!string.IsNullOrEmpty(Next.Description))
@if (!string.IsNullOrEmpty(next.Description))
{
<span class="prevnext-desc">@Next.Description</span>
<span class="prevnext-desc">@next.Description</span>
}
</a>
}
Expand All @@ -38,4 +45,8 @@
@code {
[Parameter] public NavigationNode? Prev { get; set; }
[Parameter] public NavigationNode? Next { get; set; }

protected override void OnInitialized() => PageState.OnChange += Rerender;
private void Rerender() => InvokeAsync(StateHasChanged);
public void Dispose() => PageState.OnChange -= Rerender;
}
17 changes: 13 additions & 4 deletions src/ShellDocs.Components/Chrome/TableOfContents.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@inject IJSRuntime JS
@inject DocsPageState PageState
@implements IDisposable
@implements IAsyncDisposable
@using ShellDocs.Core

Expand Down Expand Up @@ -30,11 +32,16 @@
private IJSObjectReference? _handle;
private string _sig = "";

protected override void OnParametersSet()
protected override void OnInitialized() => PageState.OnChange += OnPageChanged;

protected override void OnParametersSet() => Refresh();

private void OnPageChanged() => InvokeAsync(() => { Refresh(); StateHasChanged(); });

private void Refresh()
{
_items = (Headings ?? Array.Empty<Heading>())
.Where(h => h.Level == 2 || h.Level == 3)
.ToList();
var source = Headings ?? PageState.Document?.Headings ?? Array.Empty<Heading>();
_items = source.Where(h => h.Level == 2 || h.Level == 3).ToList();
}

protected override async Task OnAfterRenderAsync(bool _)
Expand All @@ -59,6 +66,8 @@

private Task Scroll(string id) => JS.InvokeVoidAsync("shelldocsToc.scrollTo", id).AsTask();

public void Dispose() => PageState.OnChange -= OnPageChanged;

public async ValueTask DisposeAsync()
{
if (_handle is null) return;
Expand Down
2 changes: 2 additions & 0 deletions src/ShellDocs.Components/Content/MarkdownContent.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@attribute [ShellDocsIgnore]
@inject MarkdownRenderer Renderer
@inject IJSRuntime JS
@inject DocsPageState PageState

<div class="shelldocs-prose">
@if (_parts is null)
Expand Down Expand Up @@ -40,6 +41,7 @@
{
var doc = Document ?? (Markdown is not null ? Renderer.Render(Markdown) : null);
_parts = doc is null ? null : SlotSplitter.Split(doc);
PageState.SetDocument(doc);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down
56 changes: 56 additions & 0 deletions src/ShellDocs.Components/DocsPageState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using ShellDocs.Core;
using ShellDocs.Markdown;

namespace ShellDocs.Components;

// Populated by MarkdownContent on each page render, read by DocsLayout's chrome
// (TOC, PrevNext, Breadcrumb) so those components don't need per-page wiring.
public sealed class DocsPageState : IDisposable
{
private readonly NavigationGraph _graph;
private readonly NavigationManager _nav;

public DocsPageState(NavigationGraph graph, NavigationManager nav)
{
_graph = graph;
_nav = nav;
_nav.LocationChanged += OnLocationChanged;
}

public RenderedDocument? Document { get; private set; }
public NavigationNode? CurrentNode { get; private set; }
public NavigationNode? Prev { get; private set; }
public NavigationNode? Next { get; private set; }
public IReadOnlyList<NavigationNode> Breadcrumbs { get; private set; } = Array.Empty<NavigationNode>();

public event Action? OnChange;

public void SetDocument(RenderedDocument? document)
{
Document = document;
Recompute();
}

private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
=> Recompute();

private void Recompute()
{
var path = new Uri(_nav.Uri).AbsolutePath;
CurrentNode = _graph.ResolveByUrl(path);
if (CurrentNode is null)
{
Prev = Next = null;
Breadcrumbs = Array.Empty<NavigationNode>();
}
else
{
(Prev, Next) = _graph.GetPrevNext(CurrentNode);
Breadcrumbs = _graph.GetBreadcrumb(CurrentNode);
}
OnChange?.Invoke();
}

public void Dispose() => _nav.LocationChanged -= OnLocationChanged;
}
4 changes: 3 additions & 1 deletion src/ShellDocs.Components/Layouts/DocsLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
}
<main class="docs-main">
<div class="docs-content">
<DocsBreadcrumb />
@Body
<PrevNextNav />
</div>
</main>
<aside class="docs-toc-slot" aria-label="On this page">
<SectionOutlet SectionName="docs-toc" />
<TableOfContents />
</aside>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/ShellDocs.Components/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static IServiceCollection AddShellDocs(this IServiceCollection services,
services.AddScoped<SearchState>();
services.AddScoped<SidebarCollapseState>();
services.AddScoped<CodeGroupSyncState>();
services.AddScoped<DocsPageState>();

/* Auto-register the shipped content primitives so `razor:preview` blocks
in markdown can reference <Callout>, <Card>, <Steps>, <FileTree> etc.
Expand Down
12 changes: 0 additions & 12 deletions src/ShellDocs.Templates/ScaffoldTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ public static class ScaffoldTemplates
public static string DocsPageRazor => """
@page "/docs/{*Path:nonfile}"
@layout DocsLayout
@using Microsoft.AspNetCore.Components.Sections
@using ShellDocs.Components
@using ShellDocs.Components.Chrome
@using ShellDocs.Components.Content
@using ShellDocs.Components.Layouts
@using ShellDocs.Core
Expand All @@ -54,11 +52,6 @@ @inject MarkdownRenderer Renderer
@if (_document is not null)
{
<MarkdownContent Document="_document" />
<PrevNextNav Prev="_prev" Next="_next" />

<SectionContent SectionName="docs-toc">
<TableOfContents Headings="_document.Headings" />
</SectionContent>
}
else
{
Expand All @@ -74,8 +67,6 @@ @inject MarkdownRenderer Renderer

private RenderedDocument? _document;
private string _title = "";
private NavigationNode? _prev;
private NavigationNode? _next;

protected override void OnParametersSet()
{
Expand All @@ -85,14 +76,11 @@ protected override void OnParametersSet()
{
_document = Renderer.RenderFile(node.Path);
_title = node.Title + " — Docs";
(_prev, _next) = Graph.GetPrevNext(node);
}
else
{
_document = null;
_title = "Not found";
_prev = null;
_next = null;
}
}
}
Expand Down
Loading
Loading