Add Table of Contents feature for improved navigation

This commit is contained in:
Alexander Shabarshov 2025-04-08 11:14:45 +01:00
parent 878a7feded
commit c1cac17c6b
14 changed files with 389 additions and 220 deletions

View File

@ -2,33 +2,39 @@
{ {
@if (Value.Schemas?.Count > 0) @if (Value.Schemas?.Count > 0)
{ {
<h3>Schemas</h3> <TocMember Title="Schemas">
@foreach (var (name, val) in Value.Schemas) <h3>Schemas</h3>
{ @foreach (var (name, val) in Value.Schemas)
<h4>@name</h4> {
<SchemaControl Value="@val"/> <h4>@name</h4>
} <SchemaControl Value="@val"/>
}
</TocMember>
} }
<ResponsesControl Value="@Value.Responses"/> <ResponsesControl Value="@Value.Responses"/>
@if (Value.Parameters?.Count > 0) @if (Value.Parameters?.Count > 0)
{ {
<TocMember Title="Parameters">
<h3>Parameters</h3> <h3>Parameters</h3>
@foreach (var (name, val) in Value.Parameters) @foreach (var (name, val) in Value.Parameters)
{ {
<h4>@name</h4> <h4>@name</h4>
<ParametersControl Value="@ToList(val)" /> <ParametersControl Value="@ToList(val)" />
} }
</TocMember>
} }
@if (Value.Examples?.Count > 0) @if (Value.Examples?.Count > 0)
{ {
<h3>Examples</h3> <TocMember Title="Examples">
@foreach (var (name, val) in Value.Examples) <h3>Examples</h3>
{ @foreach (var (name, val) in Value.Examples)
<ExampleControl Example="@val" Name="@name"/> {
} <ExampleControl Example="@val" Name="@name"/>
}
</TocMember>
} }
} }

View File

@ -1,5 +1,5 @@
<CascadingValue Name="OpenAPIUI_ExpandoTree_Parent" Value="_anchor"> <CascadingValue Name="OpenAPIUI_TOC_Parent" Value="_anchor">
<div class="expander @Class"> <div id="@_anchor" class="expander @Class">
<div class="ex-header @HeaderClass"> <div class="ex-header @HeaderClass">
@Title @Title
<div onclick="@(() => Toggle())"> <div onclick="@(() => Toggle())">
@ -29,16 +29,18 @@
[Parameter] [Parameter]
public RenderFragment? ChildContent { get; set; } public RenderFragment? ChildContent { get; set; }
[Parameter] [Parameter]
public bool Collapsed { get; set; } = true; public bool Collapsed { get; set; } = true;
[Parameter] [Parameter]
public string Title { get; set; } = ""; public string Title { get; set; } = "";
[Parameter] [Parameter]
public string HeaderClass { get; set; } = ""; public string HeaderClass { get; set; } = "";
[Parameter] [Parameter]
public string Class { get; set; } = ""; public string Class { get; set; } = "";
[Parameter]
public bool AddToTOC { get; set; } = false;
[CascadingParameter(Name = "OpenAPIUI_ExpandoTree")] public IExpandoTree Tree { get; set; } = null!; [CascadingParameter(Name = "OpenAPIUI_TOC")] public ITableOfContentsTree Tree { get; set; } = null!;
[CascadingParameter(Name = "OpenAPIUI_ExpandoTree_Parent")] public string Parent { get; set; } = ""; [CascadingParameter(Name = "OpenAPIUI_TOC_Parent")] public string Parent { get; set; } = "";
private string _anchor = $"anc{Random.Shared.Next():X8}"; private string _anchor = $"anc{Random.Shared.Next():X8}";
@ -47,14 +49,19 @@
if (!firstRender) if (!firstRender)
return; return;
Tree.Add(Title, _anchor, Parent, Collapsed); if (AddToTOC)
Collapsed = Tree.IsCollapsed(Title); {
Tree.Add(Title, _anchor, Parent, Collapsed);
Collapsed = Tree.IsCollapsed(Title);
}
} }
void Toggle() void Toggle()
{ {
Collapsed = !Collapsed; Collapsed = !Collapsed;
Tree.Collapse(_anchor, Collapsed); if ( AddToTOC )
Tree.Collapse(_anchor, Collapsed);
StateHasChanged(); StateHasChanged();
} }
} }

View File

@ -1,38 +1,42 @@
@if (Value != null) @if (Value != null)
{ {
<div class="header"> <TocMember Title="Header">
<h1 class="h-title">@Value.Title <span class="h-version">@Value.Version</span></h1>
<div class="h-contact"> <div class="header">
@if (!string.IsNullOrWhiteSpace(Value.Contact?.Name)) <h1 class="h-title">@Value.Title <span class="h-version">@Value.Version</span></h1>
<div class="h-contact">
@if (!string.IsNullOrWhiteSpace(Value.Contact?.Name))
{
<div class="h-name">
@Value.Contact.Name
</div>
}
@if (Value.Contact?.Url != null)
{
<div class="h-url">
@Value.Contact.Url
</div>
}
@if (Value.Contact?.Email != null)
{
<div class="h-email">
@Value.Contact.Email
</div>
}
</div>
@if (!string.IsNullOrWhiteSpace(DownloadUrl))
{ {
<div class="h-name"> <a class="h-source-url" href="@DownloadUrl">@DownloadUrl</a>
@Value.Contact.Name
</div>
}
@if (Value.Contact?.Url != null)
{
<div class="h-url">
@Value.Contact.Url
</div>
}
@if (Value.Contact?.Email != null)
{
<div class="h-email">
@Value.Contact.Email
</div>
} }
</div> </div>
@if (!string.IsNullOrWhiteSpace(DownloadUrl))
{
<a class="h-source-url" href="@DownloadUrl">@DownloadUrl</a>
}
</div>
@if (!string.IsNullOrWhiteSpace(Value.Description)) @if (!string.IsNullOrWhiteSpace(Value.Description))
{ {
<h2>Description</h2> <h2>Description</h2>
<MarkdownControl Value="@Value.Description"/> <MarkdownControl Value="@Value.Description"/>
} }
</TocMember>
} }
@code { @code {

View File

@ -1,69 +1,71 @@
@if (Value != null) <TocMember Title="@TocTitle" Anchor="@_anchor">
{ @if (Value != null)
<div class="op-header @OperationClass" @onclick="Expand"> {
<div class="op-type">@Operation</div> <div id="@_anchor" class="op-header @OperationClass" @onclick="Expand">
<div class="spacer op-header-text">-</div> <div class="op-type">@Operation</div>
<div class="op-header-text" id="@Value.OperationId">@Endpoint</div> <div class="spacer op-header-text">-</div>
<div class="spacer op-header-text">-</div> <div class="op-header-text" id="@Value.OperationId">@Endpoint</div>
<div class="op-summary op-header-text">@Value.Summary</div> <div class="spacer op-header-text">-</div>
@if (Value.Tags.Count > 0) <div class="op-summary op-header-text">@Value.Summary</div>
@if (Value.Tags.Count > 0)
{
<div class="op-tags">
@foreach (var tag in Value.Tags)
{
<Tooltip class="op-tag" TooltipText="@tag.Description">
<Text Class="op-tag-name" Value="@tag.Name" />
</Tooltip>
}
</div>
}
@if (!Collapsed)
{
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 15L12 9L6 15" stroke="var(--oa-fg-lighter)" stroke-width="2" />
</svg>
}
else
{
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 9L12 15L6 9" stroke="var(--oa-fg-lighter)" stroke-width="2" />
</svg>
}
</div>
@if (!Collapsed)
{ {
<div class="op-tags"> <div class="operation">
@foreach (var tag in Value.Tags)
@if (Value != null)
{ {
<Tooltip class="op-tag" TooltipText="@tag.Description"> <MarkdownControl Value="@Value.Description" />
<Text Class="op-tag-name" Value="@tag.Name" />
</Tooltip> if (Value.Servers?.Count > 0)
{
<ServersControl Value="@Value.Servers" />
}
<ParametersControl Value="@Value.Parameters" />
<RequestBodyControl Value="@Value.RequestBody" />
<ResponsesControl Value="@Value.Responses" />
@if (true)
{
var example = GenerateExampleData();
if (!string.IsNullOrWhiteSpace(example))
{
<div class="example-data">
<h3>Example Data</h3>
<pre>@GenerateExampleData()</pre>
</div>
}
}
} }
</div> </div>
} }
@if (_expanded)
{
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 15L12 9L6 15" stroke="var(--oa-fg-lighter)" stroke-width="2" />
</svg>
}
else
{
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 9L12 15L6 9" stroke="var(--oa-fg-lighter)" stroke-width="2" />
</svg>
}
</div>
@if ( _expanded )
{
<div class="operation">
@if (Value != null)
{
<MarkdownControl Value="@Value.Description" />
if (Value.Servers?.Count > 0)
{
<ServersControl Value="@Value.Servers" />
}
<ParametersControl Value="@Value.Parameters"/>
<RequestBodyControl Value="@Value.RequestBody"/>
<ResponsesControl Value="@Value.Responses" />
@if (true)
{
var example = GenerateExampleData();
if ( !string.IsNullOrWhiteSpace(example) )
{
<div class="example-data">
<h3>Example Data</h3>
<pre>@GenerateExampleData()</pre>
</div>
}
}
}
</div>
} }
} </TocMember>
@code { @code {
[Parameter] [Parameter]
@ -73,13 +75,20 @@
[Parameter] [Parameter]
public string Endpoint { get; set; } = ""; public string Endpoint { get; set; } = "";
[CascadingParameter(Name = "OpenAPIUI_TOC")] public ITableOfContentsTree Tree { get; set; } = null!;
[CascadingParameter(Name = "OpenAPIUI_TOC_Parent")] public string Parent { get; set; } = "";
private string _anchor = $"op_anc{Random.Shared.Next():X8}";
private string TocTitle => $"{Endpoint} - [{Operation.ToString().ToUpper()}] {Value?.Summary}";
private string OperationClass => $"op-{Operation.ToString().ToLower()}"; private string OperationClass => $"op-{Operation.ToString().ToLower()}";
private bool _expanded = true; private bool Collapsed => Tree.IsCollapsed(_anchor);
private void Expand() private void Expand()
{ {
_expanded = !_expanded; var collapsed = !Tree.IsCollapsed(_anchor);
Tree.Collapse(_anchor, collapsed);
StateHasChanged(); StateHasChanged();
} }

View File

@ -1,35 +1,37 @@
@if (!string.IsNullOrWhiteSpace(Key) || Value != null) @if (!string.IsNullOrWhiteSpace(Key) || Value != null)
{ {
@if (!string.IsNullOrWhiteSpace(@Value?.Summary) || !string.IsNullOrWhiteSpace(Value?.Description)) <TocMember Title="@Key">
{ @if (!string.IsNullOrWhiteSpace(@Value?.Summary) || !string.IsNullOrWhiteSpace(Value?.Description))
<div class="pa-header">
<h3>@Key</h3>
<div class="spacer">-</div>
<div class="pa-summary">@Value?.Summary</div>
</div>
}
<div class="path">
@if (Value != null)
{ {
<MarkdownControl Value="@Value.Description" /> <div class="pa-header">
<h3>@Key</h3>
if (Value.Servers?.Count > 0) <div class="spacer">-</div>
{ <div class="pa-summary">@Value?.Summary</div>
<ServersControl Value="@Value.Servers" /> </div>
}
@if (Value.Operations?.Count > 0)
{
<div class="operations">
@foreach (var (key, op) in @Value.Operations.Where(x => x.Value != null))
{
<OperationControl Endpoint="@Key" Operation="@key" Value="@op" />
}
</div>
}
} }
</div>
<div class="path">
@if (Value != null)
{
<MarkdownControl Value="@Value.Description" />
if (Value.Servers?.Count > 0)
{
<ServersControl Value="@Value.Servers" />
}
@if (Value.Operations?.Count > 0)
{
<div class="operations">
@foreach (var (key, op) in @Value.Operations.Where(x => x.Value != null))
{
<OperationControl Endpoint="@Key" Operation="@key" Value="@op" />
}
</div>
}
}
</div>
</TocMember>
} }
@code { @code {

View File

@ -1,62 +1,64 @@
@if (Value != null && Value.Count > 0) @if (Value != null && Value.Count > 0)
{ {
<h2>Servers</h2> <TocMember Title="Servers">
<table class="servers"> <h2>Servers</h2>
@foreach (var server in Value) <table class="servers">
{ @foreach (var server in Value)
<tr>
<td><MarkdownControl Value="@server.Description" /></td>
<td><a class="sr-url" href="@server.Url">@server.Url</a></td>
</tr>
@if (server.Variables?.Count > 0)
{ {
<tr> <tr>
<td colspan="2"> <td><MarkdownControl Value="@server.Description" /></td>
<div class="sr-vars"> <td><a class="sr-url" href="@server.Url">@server.Url</a></td>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>
@foreach (var (key, val) in server.Variables.OrderBy(x => x.Key))
{
<tr>
<td class="sr-var-name">@key</td>
<MarkdownControl Value="@val.Description" />
<td class="sr-var-val">
<Text Class="sr-default" Value="@val.Default" />
@if (val.Enum?.Count > 0)
{
<ul class="sr-enum">
@foreach (var v in val.Enum)
{
if (string.IsNullOrWhiteSpace(v))
{
<li>&lt; empty string &gt;</li>
}
else
{
<li>@v</li>
}
}
</ul>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</td>
</tr> </tr>
@if (server.Variables?.Count > 0)
{
<tr>
<td colspan="2">
<div class="sr-vars">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Value</th>
</tr>
</thead>
<tbody>
@foreach (var (key, val) in server.Variables.OrderBy(x => x.Key))
{
<tr>
<td class="sr-var-name">@key</td>
<MarkdownControl Value="@val.Description" />
<td class="sr-var-val">
<Text Class="sr-default" Value="@val.Default" />
@if (val.Enum?.Count > 0)
{
<ul class="sr-enum">
@foreach (var v in val.Enum)
{
if (string.IsNullOrWhiteSpace(v))
{
<li>&lt; empty string &gt;</li>
}
else
{
<li>@v</li>
}
}
</ul>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</td>
</tr>
}
} }
} </table>
</table> </TocMember>
} }
@code { @code {

View File

@ -0,0 +1,48 @@
@inject IJSRuntime _js;
@implements IDisposable
<div class="@Class">
<a @onclick="(async() => await ScrolltoAnchor())">@Title</a>
@foreach (var child in Tree.GetChildren(Anchor))
{
<TableOfContents Title="@child.Name" Anchor="@child.Anchor" class="toc-level" Tree="@Tree"/>
}
</div>
@code{
[Parameter] public string Class { get; set; } = "toc-level";
[Parameter] public string Anchor { get; set; } = "";
[Parameter] public string Title { get; set; } = "";
[Parameter] public ITableOfContentsTree Tree { get; set; } = null!;
private string AdaptedTitle => string.IsNullOrWhiteSpace(Title) ? "Root" : Title;
private bool _initialized;
protected override void OnParametersSet()
{
base.OnParametersSet();
if (Tree != null && !_initialized)
{
Tree.Changed += OnTocChanged;
_initialized = true;
}
}
public void Dispose()
{
if (Tree != null)
{
Tree.Changed -= OnTocChanged;
}
}
private void OnTocChanged(object? sender, EventArgs e)
{
StateHasChanged();
}
private async Task ScrolltoAnchor()
{
await _js.InvokeVoidAsync("ScrollTo", Anchor);
}
}

View File

@ -0,0 +1,45 @@
@if (string.IsNullOrWhiteSpace(Anchor))
{
<CascadingValue Name="OpenAPIUI_TOC_Parent" Value="@_anchor">
<div id="@_anchor">
@if (ChildContent != null)
{
@ChildContent
}
</div>
</CascadingValue>
}
else
{
<CascadingValue Name="OpenAPIUI_TOC_Parent" Value="@Anchor">
<div>
@if (ChildContent != null)
{
@ChildContent
}
</div>
</CascadingValue>
}
@code{
[Parameter] public string Title { get; set; } = "";
[Parameter] public string Anchor { get; set; } = "";
[Parameter] public bool Collapsed { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[CascadingParameter(Name = "OpenAPIUI_TOC")] public ITableOfContentsTree Tree { get; set; } = null!;
[CascadingParameter(Name = "OpenAPIUI_TOC_Parent")] public string Parent { get; set; } = "";
private string _anchor = $"anc{Random.Shared.Next():X8}";
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
return;
Tree.Add(Title, string.IsNullOrWhiteSpace(Anchor) ? _anchor : Anchor, Parent, Collapsed);
}
}

View File

@ -6,21 +6,25 @@ using System.Threading.Tasks;
namespace BlazorOpenApi; namespace BlazorOpenApi;
public class ExpandoTreeNode public class TocTreeNode
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Anchor { get; set; } = string.Empty; public string Anchor { get; set; } = string.Empty;
public string ParentAnchor { get; set; } = string.Empty; public string ParentAnchor { get; set; } = string.Empty;
public bool Collapsed { get; set; } public bool Collapsed { get; set; }
public override string ToString() => $"Name: {Name}, Anchor: {Anchor}, ParentAnchor: {ParentAnchor}, Collapsed: {Collapsed}";
} }
public interface IExpandoTree public interface ITableOfContentsTree
{ {
public event EventHandler Changed;
void Add(string name, string anchor, string parentAnchor, bool collapsed); void Add(string name, string anchor, string parentAnchor, bool collapsed);
void Collapse(string anchor, bool collapsed); void Collapse(string anchor, bool collapsed);
bool IsCollapsed(string anchor); bool IsCollapsed(string anchor);
bool Exists(string anchor); bool Exists(string anchor);
ExpandoTreeNode[] GetChildren(string anchor); TocTreeNode[] GetChildren(string anchor);
} }
//implement IExpandoTree //implement IExpandoTree

View File

@ -2,6 +2,7 @@
@using Microsoft.OpenApi.Models @using Microsoft.OpenApi.Models
@using Microsoft.OpenApi.Readers @using Microsoft.OpenApi.Readers
<script src="_content/BlazorOpenApi/js/BlazorOpenAPI.js"></script>
@if (Palette != null ) @if (Palette != null )
{ {
@ -10,44 +11,58 @@
<div class="openapi-ui"> <div class="openapi-ui">
<CascadingValue Value="@_api" IsFixed="true"> <CascadingValue Value="@_api" IsFixed="true">
<CascadingValue Name="OpenAPIUI_ExpandoTree" Value="_tree" IsFixed="true"> <CascadingValue Name="OpenAPIUI_TOC" Value="@_tree" IsFixed="true">
<TableOfContents Tree="@_tree" />
<HeaderControl Value="@_api.Info" DownloadUrl="@Url" /> <HeaderControl Value="@_api.Info" DownloadUrl="@Url" />
<ServersControl Value="@_api.Servers" /> <ServersControl Value="@_api.Servers" />
@if (_api.Paths?.Count > 0) @if (_api.Paths?.Count > 0)
{ {
<h2>Endpoints</h2> <TocMember Title="Endpoints">
foreach (var path in _api.Paths) <h2>Endpoints</h2>
{ @foreach (var path in _api.Paths)
if (!string.IsNullOrWhiteSpace(path.Key) || path.Value != null)
{ {
<PathControl Key="@path.Key" Value="@path.Value" /> if (!string.IsNullOrWhiteSpace(path.Key) || path.Value != null)
{
<PathControl Key="@path.Key" Value="@path.Value" />
}
} }
} </TocMember>
} }
@if (_api.Components != null) @if (_api.Components != null)
{ {
<h2>Components</h2> <TocMember Title="Components">
<ComponentsControl Value="@_api.Components" /> <h2>Components</h2>
<ComponentsControl Value="@_api.Components" />
</TocMember>
} }
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
</div> </div>
@code { @code {
[Parameter] public string Url { get; set; } = ""; [Parameter] public string Url { get; set; } = "";
[Parameter] public string Json { get; set; } = ""; [Parameter] public string Json { get; set; } = "";
[Parameter] public OpenApiUiPalette? Palette { get; set; } [Parameter] public OpenApiUiPalette? Palette { get; set; }
private string _loadedFor = ""; private string _loadedFor = "";
private OpenApiDocument _api = new(); private OpenApiDocument _api = new();
private IExpandoTree _tree = new ExpandoTree(); private ITableOfContentsTree _tree = new TableOfContentsTree();
private MarkupString PaletteStr => Palette?.AsMarkupString ?? new(); private MarkupString PaletteStr => Palette?.AsMarkupString ?? new();
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
return;
StateHasChanged();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
var loadedFromUrl = _loadedFor == Url && !string.IsNullOrWhiteSpace(Url); var loadedFromUrl = _loadedFor == Url && !string.IsNullOrWhiteSpace(Url);

View File

@ -4,19 +4,26 @@ using System.Linq;
namespace BlazorOpenApi; namespace BlazorOpenApi;
internal class ExpandoTree : IExpandoTree internal class TableOfContentsTree : ITableOfContentsTree
{ {
private readonly Dictionary<string, ExpandoTreeNode> _nodes = new(); private readonly Dictionary<string, TocTreeNode> _nodes = new();
private readonly List<string> _order = new(); private readonly List<string> _order = new();
public event EventHandler Changed;
public void Add(string name, string anchor, string parentAnchor, bool collapsed) public void Add(string name, string anchor, string parentAnchor, bool collapsed)
{ {
if (_nodes.ContainsKey(anchor)) if (_nodes.ContainsKey(anchor))
{ {
throw new InvalidOperationException($"A node with anchor '{anchor}' already exists."); throw new InvalidOperationException($"A node with anchor '{anchor}' already exists for {name}.");
} }
var node = new ExpandoTreeNode if ( parentAnchor != "" && !_nodes.ContainsKey(parentAnchor))
{
throw new InvalidOperationException($"A parent anchor '{parentAnchor}' is not registered for {name}.");
}
var node = new TocTreeNode
{ {
Name = name, Name = name,
Anchor = anchor, Anchor = anchor,
@ -26,6 +33,11 @@ internal class ExpandoTree : IExpandoTree
_nodes[anchor] = node; _nodes[anchor] = node;
_order.Add(anchor); _order.Add(anchor);
if ( Changed != null )
{
Changed(this, EventArgs.Empty);
}
} }
public void Collapse(string anchor, bool collapsed) public void Collapse(string anchor, bool collapsed)
@ -47,7 +59,7 @@ internal class ExpandoTree : IExpandoTree
public bool Exists(string anchor) => _nodes.ContainsKey(anchor); public bool Exists(string anchor) => _nodes.ContainsKey(anchor);
public ExpandoTreeNode[] GetChildren(string anchor) => _order public TocTreeNode[] GetChildren(string anchor) => _order
.Where(x => _nodes[x].ParentAnchor == anchor) .Where(x => _nodes[x].ParentAnchor == anchor)
.Select(x => _nodes[x]) .Select(x => _nodes[x])
.ToArray(); .ToArray();

View File

@ -1,4 +1,9 @@
.descriminator {} .toc-level {
margin-left: 20px;
}
.descriminator {
}
.example {} .example {}
.e-item {} .e-item {}

View File

@ -0,0 +1,10 @@
function ScrollTo(elementId) {
var element = document.getElementById(elementId);
if (element != null) {
element.scrollIntoView({
block: "start"
// behavior: 'smooth'
});
window.scrollBy(0, -64); // Adjust scrolling with a negative value here
}
}

View File

@ -20,13 +20,13 @@
<MudSwitch @bind-Value="@IsDarkMode" Color="MudBlazor.Color.Primary" Class="ma-4" T="bool" Label="Dark Mode" /> <MudSwitch @bind-Value="@IsDarkMode" Color="MudBlazor.Color.Primary" Class="ma-4" T="bool" Label="Dark Mode" />
</div> </div>
</MudDrawer> </MudDrawer>
<MudMainContent> <CascadingValue Value="@IsDarkMode">
<CascadingValue Value="@IsDarkMode"> <MudMainContent>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="pt-2"> <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
@Body @Body
</MudContainer> </MudContainer>
</CascadingValue> </MudMainContent>
</MudMainContent> </CascadingValue>
</MudLayout> </MudLayout>
@code { @code {