Skip to content

Commit 18330cf

Browse files
authored
Merge pull request #14 from shellui-dev/fix/consumer-package-selector
fix: consumer-configurable sidebar package selector
2 parents 9100216 + 065514d commit 18330cf

4 files changed

Lines changed: 106 additions & 47 deletions

File tree

examples/ShellDocs.Preview/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
3333
o.AddNavLink("Showcase", "/showcase");
3434
o.AddNavLink("Blog", "/blog");
3535

36+
o.AddPackage("shelldocs", "ShellDocs", "The docs framework itself.", "/docs/introduction",
37+
"M12 2 4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6z");
38+
o.AddPackage("shelldocs.markdown", "ShellDocs.Markdown", "Markdig extensions, slots, MDX.", "/docs/markdown",
39+
"M4 4h16v16H4z M4 9h16 M9 4v16");
40+
o.AddPackage("shelldocs.core", "ShellDocs.Core", "Navigation graph + content model.", "/docs/core",
41+
"M12 2 2 7l10 5 10-5-10-5z M2 17l10 5 10-5 M2 12l10 5 10-5");
42+
o.AddPackage("shelldocs.cli", "ShellDocs.CLI", "Scaffold, build, publish.", "/docs/cli",
43+
"m8 6-6 6 6 6 M16 6l6 6-6 6");
44+
o.AddPackage("shelldocs.components","ShellDocs.Components","Layouts, header, sidebar, TOC.", "/docs/components",
45+
"M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z");
46+
3647
// Callout, Card, Steps, FileTree, CodeGroup, LinkCard etc. are
3748
// auto-registered by AddShellDocs — no extra work needed here.
3849
});

src/ShellDocs.Components/Chrome/PackageSelector.razor

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,70 @@
11
@inject NavigationManager Nav
2+
@inject ShellDocsOptions Options
23

3-
<div class="pkg" @onfocusout="OnBlur">
4-
<button type="button" class="pkg-trigger" @onclick="Toggle" aria-haspopup="listbox" aria-expanded="@_open">
5-
<span class="pkg-mark">
6-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
7-
<path d="@Selected.IconPath"/>
4+
@if (Options.Packages.Count > 1)
5+
{
6+
<div class="pkg" @onfocusout="OnBlur">
7+
<button type="button" class="pkg-trigger" @onclick="Toggle" aria-haspopup="listbox" aria-expanded="@_open">
8+
<span class="pkg-mark">
9+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
10+
<path d="@(Selected.IconPath ?? DefaultIcon)"/>
11+
</svg>
12+
</span>
13+
<span class="pkg-value">@Selected.Title</span>
14+
<svg class="pkg-chevron @(_open ? "open" : "")" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
15+
<polyline points="6 9 12 15 18 9"/>
816
</svg>
9-
</span>
10-
<span class="pkg-value">@Selected.Title</span>
11-
<svg class="pkg-chevron @(_open ? "open" : "")" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
12-
<polyline points="6 9 12 15 18 9"/>
13-
</svg>
14-
</button>
17+
</button>
1518

16-
@if (_open)
17-
{
18-
<div class="pkg-menu" role="listbox">
19-
@foreach (var pkg in Packages)
20-
{
21-
var isSel = pkg.Id == Selected.Id;
22-
<button type="button" class="pkg-option @(isSel ? "selected" : "")" role="option" aria-selected="@isSel" @onclick="() => Choose(pkg)">
23-
<span class="pkg-option-icon">
24-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
25-
<path d="@pkg.IconPath"/>
26-
</svg>
27-
</span>
28-
<span class="pkg-option-body">
29-
<span class="pkg-option-title">@pkg.Title</span>
30-
<span class="pkg-option-desc">@pkg.Description</span>
31-
</span>
32-
@if (isSel)
33-
{
34-
<svg class="pkg-option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
35-
}
36-
</button>
37-
}
38-
</div>
39-
}
40-
</div>
19+
@if (_open)
20+
{
21+
<div class="pkg-menu" role="listbox">
22+
@foreach (var pkg in Options.Packages)
23+
{
24+
var isSel = pkg.Id == Selected.Id;
25+
<button type="button" class="pkg-option @(isSel ? "selected" : "")" role="option" aria-selected="@isSel" @onclick="() => Choose(pkg)">
26+
<span class="pkg-option-icon">
27+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
28+
<path d="@(pkg.IconPath ?? DefaultIcon)"/>
29+
</svg>
30+
</span>
31+
<span class="pkg-option-body">
32+
<span class="pkg-option-title">@pkg.Title</span>
33+
<span class="pkg-option-desc">@pkg.Description</span>
34+
</span>
35+
@if (isSel)
36+
{
37+
<svg class="pkg-option-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
38+
}
39+
</button>
40+
}
41+
</div>
42+
}
43+
</div>
44+
}
4145

4246
@code {
4347
private bool _open;
4448

45-
private record Package(string Id, string Title, string Description, string IconPath, string RootUrl);
49+
private const string DefaultIcon = "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z M3.27 6.96 12 12.01l8.73-5.05 M12 22.08V12";
4650

47-
private static readonly List<Package> Packages = new()
51+
// Longest RootUrl prefix wins — "/docs/components" beats "/docs" for "/docs/components/callout".
52+
private DocsPackage Selected
4853
{
49-
new("shelldocs", "ShellDocs", "The docs framework itself.", "M12 2 4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6z", "/docs/introduction"),
50-
new("shelldocs.markdown", "ShellDocs.Markdown", "Markdig extensions, slots, MDX.", "M4 4h16v16H4z M4 9h16 M9 4v16", "/docs/markdown"),
51-
new("shelldocs.core", "ShellDocs.Core", "Navigation graph + content model.","M12 2 2 7l10 5 10-5-10-5z M2 17l10 5 10-5 M2 12l10 5 10-5", "/docs/core"),
52-
new("shelldocs.cli", "ShellDocs.CLI", "Scaffold, build, publish.", "m8 6-6 6 6 6 M16 6l6 6-6 6", "/docs/cli"),
53-
new("shelldocs.components","ShellDocs.Components","Layouts, header, sidebar, TOC.","M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z", "/docs/components")
54-
};
55-
56-
private Package Selected => Packages.FirstOrDefault(p => Nav.Uri.Contains(p.RootUrl.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "")) ?? Packages[0];
54+
get
55+
{
56+
var path = new Uri(Nav.Uri).AbsolutePath;
57+
return Options.Packages
58+
.Where(p => path.StartsWith(p.RootUrl, StringComparison.OrdinalIgnoreCase))
59+
.OrderByDescending(p => p.RootUrl.Length)
60+
.FirstOrDefault()
61+
?? Options.Packages[0];
62+
}
63+
}
5764

5865
private void Toggle() => _open = !_open;
5966

60-
private void Choose(Package pkg)
67+
private void Choose(DocsPackage pkg)
6168
{
6269
_open = false;
6370
Nav.NavigateTo(pkg.RootUrl);

src/ShellDocs.Components/ShellDocsOptions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class ShellDocsOptions
1414
public DocsLayoutVariant LayoutVariant { get; set; } = DocsLayoutVariant.TopNav;
1515

1616
public List<NavLink> PrimaryNav { get; } = new();
17+
// 0 or 1 entries hides the sidebar package selector entirely.
18+
public List<DocsPackage> Packages { get; } = new();
1719
public List<Type> RegisteredComponents { get; } = new();
1820
/* Per-type tag alias — when a type appears here, BuildTypeRegistry uses this
1921
string as the markdown-facing tag name instead of the type's short name.
@@ -109,6 +111,12 @@ public ShellDocsOptions AddNavMenu(string label, params NavMenuItem[] items)
109111
return this;
110112
}
111113

114+
public ShellDocsOptions AddPackage(string id, string title, string description, string rootUrl, string? iconPath = null)
115+
{
116+
Packages.Add(new DocsPackage(id, title, description, rootUrl, iconPath));
117+
return this;
118+
}
119+
112120
internal TypeRegistry BuildTypeRegistry()
113121
{
114122
var registry = new TypeRegistry();
@@ -126,6 +134,9 @@ internal TypeRegistry BuildTypeRegistry()
126134
public record NavLink(string Label, string Href, List<NavMenuItem>? Children = null);
127135
public record NavMenuItem(string Label, string Href, string? Description = null, string? IconSvg = null);
128136

137+
// IconPath is a raw SVG `d` attribute value on a 24×24 viewBox — not a URL.
138+
public record DocsPackage(string Id, string Title, string Description, string RootUrl, string? IconPath = null);
139+
129140
public enum ShellDocsTheme
130141
{
131142
Shadcn,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using ShellDocs.Components;
2+
using Xunit;
3+
4+
namespace ShellDocs.Tests;
5+
6+
public class PackageRegistrationTests
7+
{
8+
[Fact]
9+
public void AddPackage_FluentChain_AppendsInOrder()
10+
{
11+
var options = new ShellDocsOptions()
12+
.AddPackage("core", "Core", "Foundation.", "/docs/core")
13+
.AddPackage("components", "Components", "Widgets.", "/docs/components", iconPath: "M0 0h10v10H0z");
14+
15+
Assert.Equal(2, options.Packages.Count);
16+
Assert.Equal("core", options.Packages[0].Id);
17+
Assert.Equal("Core", options.Packages[0].Title);
18+
Assert.Equal("/docs/core", options.Packages[0].RootUrl);
19+
Assert.Null(options.Packages[0].IconPath);
20+
Assert.Equal("components", options.Packages[1].Id);
21+
Assert.Equal("M0 0h10v10H0z", options.Packages[1].IconPath);
22+
}
23+
24+
[Fact]
25+
public void Packages_DefaultToEmpty_SoSelectorHides()
26+
{
27+
var options = new ShellDocsOptions();
28+
Assert.Empty(options.Packages);
29+
}
30+
}

0 commit comments

Comments
 (0)