Skip to content

Commit 03fbcc2

Browse files
authored
Merge pull request #16 from shellui-dev/feat/auto-docs-chrome
feat: auto-render TOC / PrevNext / Breadcrumb from a shared DocsPageState
2 parents ba33c44 + 0df411e commit 03fbcc2

11 files changed

Lines changed: 239 additions & 39 deletions

File tree

examples/ShellDocs.Preview/Components/Pages/DocsPage.razor

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@page "/docs/{*Path:nonfile}"
22
@layout DocsLayout
3-
@using Microsoft.AspNetCore.Components.Sections
43
@inject NavigationGraph Graph
54
@inject MarkdownRenderer Renderer
65

@@ -9,11 +8,6 @@
98
@if (_document is not null)
109
{
1110
<MarkdownContent Document="_document" />
12-
<PrevNextNav Prev="_prev" Next="_next" />
13-
14-
<SectionContent SectionName="docs-toc">
15-
<TableOfContents Headings="_document.Headings" />
16-
</SectionContent>
1711
}
1812
else
1913
{
@@ -35,8 +29,6 @@ else
3529

3630
private RenderedDocument? _document;
3731
private string _title = "";
38-
private NavigationNode? _prev;
39-
private NavigationNode? _next;
4032

4133
protected override void OnParametersSet()
4234
{
@@ -46,14 +38,11 @@ else
4638
{
4739
_document = Renderer.RenderFile(node.Path);
4840
_title = node.Title + " — ShellDocs";
49-
(_prev, _next) = Graph.GetPrevNext(node);
5041
}
5142
else
5243
{
5344
_document = null;
5445
_title = "Not found — ShellDocs";
55-
_prev = null;
56-
_next = null;
5746
}
5847
}
5948
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@inject DocsPageState PageState
2+
@implements IDisposable
3+
@using ShellDocs.Core
4+
5+
@if (PageState.Breadcrumbs.Count > 1)
6+
{
7+
<nav class="docs-breadcrumb" aria-label="Breadcrumb">
8+
<ol class="docs-breadcrumb-list">
9+
@for (var i = 0; i < PageState.Breadcrumbs.Count; i++)
10+
{
11+
var node = PageState.Breadcrumbs[i];
12+
var isLast = i == PageState.Breadcrumbs.Count - 1;
13+
<li class="docs-breadcrumb-item">
14+
@if (isLast)
15+
{
16+
<span class="docs-breadcrumb-current" aria-current="page">@node.Title</span>
17+
}
18+
else
19+
{
20+
@if (!string.IsNullOrEmpty(node.Url))
21+
{
22+
<a class="docs-breadcrumb-link" href="@node.Url">@node.Title</a>
23+
}
24+
else
25+
{
26+
<span class="docs-breadcrumb-section">@node.Title</span>
27+
}
28+
<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>
29+
}
30+
</li>
31+
}
32+
</ol>
33+
</nav>
34+
}
35+
36+
@code {
37+
protected override void OnInitialized() => PageState.OnChange += Rerender;
38+
private void Rerender() => InvokeAsync(StateHasChanged);
39+
public void Dispose() => PageState.OnChange -= Rerender;
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.docs-breadcrumb {
2+
margin: 0 0 1.25rem;
3+
font-size: 0.8125rem;
4+
color: var(--muted-foreground);
5+
}
6+
7+
.docs-breadcrumb-list {
8+
list-style: none;
9+
padding: 0;
10+
margin: 0;
11+
display: flex;
12+
align-items: center;
13+
flex-wrap: wrap;
14+
gap: 0.25rem;
15+
}
16+
17+
.docs-breadcrumb-item {
18+
display: inline-flex;
19+
align-items: center;
20+
gap: 0.25rem;
21+
}
22+
23+
.docs-breadcrumb-link,
24+
.docs-breadcrumb-section {
25+
color: var(--muted-foreground);
26+
text-decoration: none;
27+
transition: color 150ms;
28+
}
29+
.docs-breadcrumb-link:hover { color: var(--foreground); }
30+
31+
.docs-breadcrumb-current {
32+
color: var(--foreground);
33+
font-weight: 500;
34+
}
35+
36+
.docs-breadcrumb-sep {
37+
width: 0.75rem;
38+
height: 0.75rem;
39+
opacity: 0.6;
40+
}
Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
1+
@inject DocsPageState PageState
2+
@implements IDisposable
13
@using ShellDocs.Core
24

3-
@if (Prev is not null || Next is not null)
5+
@{
6+
var prev = Prev ?? PageState.Prev;
7+
var next = Next ?? PageState.Next;
8+
}
9+
10+
@if (prev is not null || next is not null)
411
{
512
<nav class="prevnext" aria-label="Previous and next">
6-
@if (Prev is not null)
13+
@if (prev is not null)
714
{
8-
<a class="prevnext-card prev" href="@Prev.Url">
15+
<a class="prevnext-card prev" href="@prev.Url">
916
<span class="prevnext-hint">
1017
<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>
11-
<span>@Prev.Title</span>
18+
<span>@prev.Title</span>
1219
</span>
13-
@if (!string.IsNullOrEmpty(Prev.Description))
20+
@if (!string.IsNullOrEmpty(prev.Description))
1421
{
15-
<span class="prevnext-desc">@Prev.Description</span>
22+
<span class="prevnext-desc">@prev.Description</span>
1623
}
1724
</a>
1825
}
1926
else { <span class="prevnext-placeholder" aria-hidden="true"></span> }
2027

21-
@if (Next is not null)
28+
@if (next is not null)
2229
{
23-
<a class="prevnext-card next" href="@Next.Url">
30+
<a class="prevnext-card next" href="@next.Url">
2431
<span class="prevnext-hint">
25-
<span>@Next.Title</span>
32+
<span>@next.Title</span>
2633
<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>
2734
</span>
28-
@if (!string.IsNullOrEmpty(Next.Description))
35+
@if (!string.IsNullOrEmpty(next.Description))
2936
{
30-
<span class="prevnext-desc">@Next.Description</span>
37+
<span class="prevnext-desc">@next.Description</span>
3138
}
3239
</a>
3340
}
@@ -38,4 +45,8 @@
3845
@code {
3946
[Parameter] public NavigationNode? Prev { get; set; }
4047
[Parameter] public NavigationNode? Next { get; set; }
48+
49+
protected override void OnInitialized() => PageState.OnChange += Rerender;
50+
private void Rerender() => InvokeAsync(StateHasChanged);
51+
public void Dispose() => PageState.OnChange -= Rerender;
4152
}

src/ShellDocs.Components/Chrome/TableOfContents.razor

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@inject IJSRuntime JS
2+
@inject DocsPageState PageState
3+
@implements IDisposable
24
@implements IAsyncDisposable
35
@using ShellDocs.Core
46

@@ -30,11 +32,16 @@
3032
private IJSObjectReference? _handle;
3133
private string _sig = "";
3234

33-
protected override void OnParametersSet()
35+
protected override void OnInitialized() => PageState.OnChange += OnPageChanged;
36+
37+
protected override void OnParametersSet() => Refresh();
38+
39+
private void OnPageChanged() => InvokeAsync(() => { Refresh(); StateHasChanged(); });
40+
41+
private void Refresh()
3442
{
35-
_items = (Headings ?? Array.Empty<Heading>())
36-
.Where(h => h.Level == 2 || h.Level == 3)
37-
.ToList();
43+
var source = Headings ?? PageState.Document?.Headings ?? Array.Empty<Heading>();
44+
_items = source.Where(h => h.Level == 2 || h.Level == 3).ToList();
3845
}
3946

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

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

69+
public void Dispose() => PageState.OnChange -= OnPageChanged;
70+
6271
public async ValueTask DisposeAsync()
6372
{
6473
if (_handle is null) return;

src/ShellDocs.Components/Content/MarkdownContent.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@attribute [ShellDocsIgnore]
22
@inject MarkdownRenderer Renderer
33
@inject IJSRuntime JS
4+
@inject DocsPageState PageState
45

56
<div class="shelldocs-prose">
67
@if (_parts is null)
@@ -40,6 +41,7 @@
4041
{
4142
var doc = Document ?? (Markdown is not null ? Renderer.Render(Markdown) : null);
4243
_parts = doc is null ? null : SlotSplitter.Split(doc);
44+
PageState.SetDocument(doc);
4345
}
4446

4547
protected override async Task OnAfterRenderAsync(bool firstRender)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.AspNetCore.Components;
2+
using ShellDocs.Core;
3+
using ShellDocs.Markdown;
4+
5+
namespace ShellDocs.Components;
6+
7+
// Populated by MarkdownContent on each page render, read by DocsLayout's chrome
8+
// (TOC, PrevNext, Breadcrumb) so those components don't need per-page wiring.
9+
public sealed class DocsPageState : IDisposable
10+
{
11+
private readonly NavigationGraph _graph;
12+
private readonly NavigationManager _nav;
13+
14+
public DocsPageState(NavigationGraph graph, NavigationManager nav)
15+
{
16+
_graph = graph;
17+
_nav = nav;
18+
_nav.LocationChanged += OnLocationChanged;
19+
}
20+
21+
public RenderedDocument? Document { get; private set; }
22+
public NavigationNode? CurrentNode { get; private set; }
23+
public NavigationNode? Prev { get; private set; }
24+
public NavigationNode? Next { get; private set; }
25+
public IReadOnlyList<NavigationNode> Breadcrumbs { get; private set; } = Array.Empty<NavigationNode>();
26+
27+
public event Action? OnChange;
28+
29+
public void SetDocument(RenderedDocument? document)
30+
{
31+
Document = document;
32+
Recompute();
33+
}
34+
35+
private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
36+
=> Recompute();
37+
38+
private void Recompute()
39+
{
40+
var path = new Uri(_nav.Uri).AbsolutePath;
41+
CurrentNode = _graph.ResolveByUrl(path);
42+
if (CurrentNode is null)
43+
{
44+
Prev = Next = null;
45+
Breadcrumbs = Array.Empty<NavigationNode>();
46+
}
47+
else
48+
{
49+
(Prev, Next) = _graph.GetPrevNext(CurrentNode);
50+
Breadcrumbs = _graph.GetBreadcrumb(CurrentNode);
51+
}
52+
OnChange?.Invoke();
53+
}
54+
55+
public void Dispose() => _nav.LocationChanged -= OnLocationChanged;
56+
}

src/ShellDocs.Components/Layouts/DocsLayout.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
}
4545
<main class="docs-main">
4646
<div class="docs-content">
47+
<DocsBreadcrumb />
4748
@Body
49+
<PrevNextNav />
4850
</div>
4951
</main>
5052
<aside class="docs-toc-slot" aria-label="On this page">
51-
<SectionOutlet SectionName="docs-toc" />
53+
<TableOfContents />
5254
</aside>
5355
</div>
5456
</div>

src/ShellDocs.Components/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static IServiceCollection AddShellDocs(this IServiceCollection services,
1919
services.AddScoped<SearchState>();
2020
services.AddScoped<SidebarCollapseState>();
2121
services.AddScoped<CodeGroupSyncState>();
22+
services.AddScoped<DocsPageState>();
2223

2324
/* Auto-register the shipped content primitives so `razor:preview` blocks
2425
in markdown can reference <Callout>, <Card>, <Steps>, <FileTree> etc.

src/ShellDocs.Templates/ScaffoldTemplates.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ public static class ScaffoldTemplates
3939
public static string DocsPageRazor => """
4040
@page "/docs/{*Path:nonfile}"
4141
@layout DocsLayout
42-
@using Microsoft.AspNetCore.Components.Sections
4342
@using ShellDocs.Components
44-
@using ShellDocs.Components.Chrome
4543
@using ShellDocs.Components.Content
4644
@using ShellDocs.Components.Layouts
4745
@using ShellDocs.Core
@@ -54,11 +52,6 @@ @inject MarkdownRenderer Renderer
5452
@if (_document is not null)
5553
{
5654
<MarkdownContent Document="_document" />
57-
<PrevNextNav Prev="_prev" Next="_next" />
58-
59-
<SectionContent SectionName="docs-toc">
60-
<TableOfContents Headings="_document.Headings" />
61-
</SectionContent>
6255
}
6356
else
6457
{
@@ -74,8 +67,6 @@ @inject MarkdownRenderer Renderer
7467
7568
private RenderedDocument? _document;
7669
private string _title = "";
77-
private NavigationNode? _prev;
78-
private NavigationNode? _next;
7970
8071
protected override void OnParametersSet()
8172
{
@@ -85,14 +76,11 @@ protected override void OnParametersSet()
8576
{
8677
_document = Renderer.RenderFile(node.Path);
8778
_title = node.Title + " — Docs";
88-
(_prev, _next) = Graph.GetPrevNext(node);
8979
}
9080
else
9181
{
9282
_document = null;
9383
_title = "Not found";
94-
_prev = null;
95-
_next = null;
9684
}
9785
}
9886
}

0 commit comments

Comments
 (0)