diff --git a/examples/ShellDocs.Preview/Components/Pages/DocsPage.razor b/examples/ShellDocs.Preview/Components/Pages/DocsPage.razor index 4aebbee..441cf15 100644 --- a/examples/ShellDocs.Preview/Components/Pages/DocsPage.razor +++ b/examples/ShellDocs.Preview/Components/Pages/DocsPage.razor @@ -20,7 +20,7 @@ else

Page not found

The page @Path doesn't exist yet.

-

← Back to introduction

+

Back to introduction

} diff --git a/examples/ShellDocs.Preview/Components/Pages/Home.razor b/examples/ShellDocs.Preview/Components/Pages/Home.razor index bea88b1..e2a02e7 100644 --- a/examples/ShellDocs.Preview/Components/Pages/Home.razor +++ b/examples/ShellDocs.Preview/Components/Pages/Home.razor @@ -196,7 +196,10 @@ Works in Server, WASM, and static output.

Built by the ShellUI team.

Same taste, same craftsmanship, same install-and-go DX. Free forever, MIT-licensed, community-driven.

- Read the docs + + Read the docs + + Contribute on GitHub
@@ -328,16 +331,18 @@ Works in Server, WASM, and static output. .btn { display: inline-flex; align-items: center; - gap: 0.5rem; - padding: 0.65rem 1.25rem; + gap: 0.4rem; + padding: 0.6rem 1.15rem; border-radius: 9999px; border: 1px solid transparent; text-decoration: none; font-weight: 500; font-size: 0.9rem; letter-spacing: -0.005em; + white-space: nowrap; transition: background 150ms, border-color 150ms, color 150ms; } + .btn svg { width: 0.9rem; height: 0.9rem; flex-shrink: 0; } .btn.primary { background: var(--primary); color: var(--primary-foreground); diff --git a/examples/ShellDocs.Preview/Components/Pages/Showcase.razor b/examples/ShellDocs.Preview/Components/Pages/Showcase.razor index 35688c7..eb90ced 100644 --- a/examples/ShellDocs.Preview/Components/Pages/Showcase.razor +++ b/examples/ShellDocs.Preview/Components/Pages/Showcase.razor @@ -45,7 +45,10 @@

Ship your docs on ShellDocs?

Open a PR to get listed here. Include a screenshot, a one-line description, and your live URL.

- Submit on GitHub → + + Submit on GitHub + +
diff --git a/examples/ShellDocs.Preview/Program.cs b/examples/ShellDocs.Preview/Program.cs index 1074db8..203ba0c 100644 --- a/examples/ShellDocs.Preview/Program.cs +++ b/examples/ShellDocs.Preview/Program.cs @@ -14,6 +14,8 @@ o.SiteName = "ShellDocs"; o.SiteTagline = "the docs framework for .NET"; o.GitHubRepo = "shellui-dev/shelldocs"; + // Try the new floating-sidebar variant. Flip to TopNav for the classic look. + o.LayoutVariant = DocsLayoutVariant.Sidebar; o.AddNavMenu("Documentation", new NavMenuItem("Getting Started", "/docs/introduction", "Install, configure, and ship your first ShellDocs site.", diff --git a/src/ShellDocs.Components/Chrome/DocsHeader.razor b/src/ShellDocs.Components/Chrome/DocsHeader.razor index 6fa6616..893ec2f 100644 --- a/src/ShellDocs.Components/Chrome/DocsHeader.razor +++ b/src/ShellDocs.Components/Chrome/DocsHeader.razor @@ -1,5 +1,6 @@ @inject ShellDocsOptions Options @inject ShellDocs.Components.Chrome.MobileNavState MobileNav +@inject IJSRuntime JS
@@ -62,14 +63,17 @@
- + @if (ShowSearch) + { + + } @@ -86,9 +90,15 @@ @implements IDisposable @code { + /* Search chip toggle. Marketing pages (HomeLayout) pass false — visitors + there aren't looking to grep docs. Docs pages (DocsLayout TopNav variant) + leave the default true. */ + [Parameter] public bool ShowSearch { get; set; } = true; + private const string DefaultIcon = ""; protected override void OnInitialized() => MobileNav.OnChange += StateHasChanged; public void Dispose() => MobileNav.OnChange -= StateHasChanged; + private Task OpenSearch() => JS.InvokeVoidAsync("shelldocsSearch.open").AsTask(); } diff --git a/src/ShellDocs.Components/Chrome/DocsMobileBar.razor b/src/ShellDocs.Components/Chrome/DocsMobileBar.razor new file mode 100644 index 0000000..698c68a --- /dev/null +++ b/src/ShellDocs.Components/Chrome/DocsMobileBar.razor @@ -0,0 +1,30 @@ +@inject MobileNavState MobileNav +@inject IJSRuntime JS + + + +@code { + private Task OpenSearch() => JS.InvokeVoidAsync("shelldocsSearch.open").AsTask(); +} diff --git a/src/ShellDocs.Components/Chrome/DocsMobileBar.razor.css b/src/ShellDocs.Components/Chrome/DocsMobileBar.razor.css new file mode 100644 index 0000000..92befee --- /dev/null +++ b/src/ShellDocs.Components/Chrome/DocsMobileBar.razor.css @@ -0,0 +1,36 @@ +.docs-mobile-bar { + position: sticky; + top: 0; + z-index: 30; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + background: color-mix(in oklch, var(--background) 90%, transparent); + backdrop-filter: saturate(180%) blur(12px); + -webkit-backdrop-filter: saturate(180%) blur(12px); + border-bottom: 1px solid var(--border); +} +@media (min-width: 1024px) { + .docs-mobile-bar { display: none; } +} + +::deep .docs-mobile-tool { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + background: transparent; + border: 0; + border-radius: calc(var(--radius) - 3px); + color: var(--muted-foreground); + cursor: pointer; + transition: color 150ms, background 150ms; +} +::deep .docs-mobile-tool:hover { color: var(--foreground); background: var(--muted); } +::deep .docs-mobile-tool svg { width: 1.15rem; height: 1.15rem; } + +/* Push ThemeToggle to the right end */ +::deep .theme-toggle { margin-left: auto; } diff --git a/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor b/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor new file mode 100644 index 0000000..190e0f5 --- /dev/null +++ b/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor @@ -0,0 +1,32 @@ +@inject ShellDocsOptions Options +@inject IJSRuntime JS +@inject SidebarCollapseState Collapse + +
+ + + +
+ +@code { + private string ShortcutKey => OperatingSystem.IsMacOS() ? "⌘" : "Ctrl"; + private Task OpenSearch() => JS.InvokeVoidAsync("shelldocsSearch.open").AsTask(); +} diff --git a/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor.css b/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor.css new file mode 100644 index 0000000..3a74f76 --- /dev/null +++ b/src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor.css @@ -0,0 +1,93 @@ +.docs-sidebar-header { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.85rem 0.75rem 0.5rem 1rem; + flex-shrink: 0; +} + +.docs-sidebar-brand-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.docs-sidebar-brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: var(--foreground); + font-weight: 600; + font-size: 0.95rem; + letter-spacing: -0.01em; + padding: 0.15rem 0.2rem; + border-radius: calc(var(--radius) - 3px); + transition: opacity 150ms; + min-width: 0; + flex: 1; +} +.docs-sidebar-brand:hover { opacity: 0.8; } + +.docs-sidebar-collapse { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.85rem; + height: 1.85rem; + padding: 0; + background: transparent; + border: 0; + border-radius: calc(var(--radius) - 3px); + color: var(--muted-foreground); + cursor: pointer; + transition: color 150ms, background 150ms; + flex-shrink: 0; +} +.docs-sidebar-collapse:hover { color: var(--foreground); background: var(--muted); } +.docs-sidebar-collapse svg { width: 1rem; height: 1rem; } +.docs-sidebar-logo-dot { + width: 1.125rem; + height: 1.125rem; + border-radius: 4px; + background: var(--foreground); + flex-shrink: 0; +} + +.docs-sidebar-search { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.5rem 0.4rem 0.6rem; + background: var(--muted); + border: 1px solid transparent; + color: var(--muted-foreground); + border-radius: calc(var(--radius) - 2px); + cursor: pointer; + font-family: inherit; + font-size: 0.8125rem; + text-align: left; + width: 100%; + transition: background 150ms, color 150ms, border-color 150ms; +} +.docs-sidebar-search:hover { + color: var(--foreground); + border-color: var(--border); +} +.docs-sidebar-search svg { width: 0.875rem; height: 0.875rem; flex-shrink: 0; } +.docs-sidebar-search-label { flex: 1; } +.docs-sidebar-search-kbd { display: inline-flex; gap: 0.2rem; } +.docs-sidebar-search-kbd kbd { + font-family: var(--font-mono); + font-size: 0.62rem; + font-weight: 600; + background: var(--background); + border: 1px solid var(--border); + padding: 0.05rem 0.3rem; + border-radius: 4px; + color: var(--muted-foreground); + line-height: 1; + min-width: 1.1rem; + text-align: center; +} diff --git a/src/ShellDocs.Components/Chrome/PackageSelector.razor b/src/ShellDocs.Components/Chrome/PackageSelector.razor index 65cec79..1f2d29f 100644 --- a/src/ShellDocs.Components/Chrome/PackageSelector.razor +++ b/src/ShellDocs.Components/Chrome/PackageSelector.razor @@ -8,9 +8,8 @@ @Selected.Title - - - + + diff --git a/src/ShellDocs.Components/Chrome/PackageSelector.razor.css b/src/ShellDocs.Components/Chrome/PackageSelector.razor.css index b789d9a..e811883 100644 --- a/src/ShellDocs.Components/Chrome/PackageSelector.razor.css +++ b/src/ShellDocs.Components/Chrome/PackageSelector.razor.css @@ -46,14 +46,17 @@ white-space: nowrap; } -/* Stacked select-style chevrons — fumadocs pattern */ -.pkg-chevrons { +/* Single chevron-down that rotates 180° when the menu opens — matches + the collapsible section toggles in the sidebar. */ +.pkg-chevron { width: 0.9rem; height: 0.9rem; color: var(--muted-foreground); flex-shrink: 0; margin-left: 0.25rem; + transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1); } +.pkg-chevron.open { transform: rotate(180deg); } .pkg-menu { position: absolute; diff --git a/src/ShellDocs.Components/Chrome/SearchDialog.razor b/src/ShellDocs.Components/Chrome/SearchDialog.razor new file mode 100644 index 0000000..4d499fd --- /dev/null +++ b/src/ShellDocs.Components/Chrome/SearchDialog.razor @@ -0,0 +1,208 @@ +@inject SearchState State +@inject SearchIndex Index +@inject NavigationManager Nav +@inject IJSRuntime JS +@implements IDisposable + +@if (State.IsOpen) +{ +
+
+
+ + + Esc +
+ +
+ @if (_results.Count == 0 && !string.IsNullOrWhiteSpace(_query)) + { +
No results for “@_query”.
+ } + else + { + @for (var i = 0; i < _results.Count; i++) + { + var idx = i; + var r = _results[i]; + + } + } +
+ + +
+
+} + +@code { + private string _query = ""; + private int _highlightedIndex; + private List _results = new(); + private ElementReference _inputEl; + private bool _needsFocus; + private DotNetObjectReference? _selfRef; + private IJSObjectReference? _hotkeyDisposer; + + protected override void OnInitialized() + { + State.OnChange += OnStateChange; + Recalc(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _selfRef = DotNetObjectReference.Create(this); + try + { + _hotkeyDisposer = await JS.InvokeAsync("shelldocsSearch.init", _selfRef); + } + catch { } + } + if (_needsFocus) + { + _needsFocus = false; + try { await _inputEl.FocusAsync(); } catch { } + } + } + + [JSInvokable] public void OpenFromJs() => InvokeAsync(() => State.Open()); + + private void OnStateChange() + { + if (State.IsOpen) + { + _query = ""; + _highlightedIndex = 0; + Recalc(); + _needsFocus = true; + } + InvokeAsync(StateHasChanged); + } + + private void OnKey(KeyboardEventArgs e) + { + switch (e.Key) + { + case "Escape": State.Close(); break; + case "ArrowDown": + if (_results.Count > 0) + _highlightedIndex = (_highlightedIndex + 1) % _results.Count; + break; + case "ArrowUp": + if (_results.Count > 0) + _highlightedIndex = (_highlightedIndex - 1 + _results.Count) % _results.Count; + break; + case "Enter": + if (_highlightedIndex >= 0 && _highlightedIndex < _results.Count) + Jump(_results[_highlightedIndex].Entry); + break; + default: + // Any other key = user is typing; rerun search on next binding cycle. + InvokeAsync(Recalc); + break; + } + } + + private void Jump(SearchEntry entry) + { + State.Close(); + Nav.NavigateTo(entry.Url); + } + + private void Recalc() + { + _results = Rank(_query, Index.Entries).Take(30).ToList(); + if (_highlightedIndex >= _results.Count) _highlightedIndex = 0; + } + + /* Naive fuzzy score: substring hit boosts by big margin, then per-word + token overlap. Good enough for the ~100-entry indexes docs sites have. */ + private static IEnumerable Rank(string query, IReadOnlyList entries) + { + if (string.IsNullOrWhiteSpace(query)) + { + return entries.Take(20).Select(e => new Scored(e, 0)); + } + var q = query.Trim().ToLowerInvariant(); + var tokens = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return entries + .Select(e => new Scored(e, Score(e, q, tokens))) + .Where(s => s.Score > 0) + .OrderByDescending(s => s.Score); + } + + private static int Score(SearchEntry e, string q, string[] tokens) + { + var title = e.Title.ToLowerInvariant(); + var desc = (e.Description ?? "").ToLowerInvariant(); + var sec = (e.Section ?? "").ToLowerInvariant(); + var score = 0; + // Exact substring in title: massive boost. + if (title.Contains(q)) score += 100; + if (title.StartsWith(q)) score += 50; + if (sec.Contains(q)) score += 20; + if (desc.Contains(q)) score += 10; + // Per-token AND — every token must appear somewhere. + foreach (var t in tokens) + { + if (title.Contains(t)) score += 8; + else if (sec.Contains(t)) score += 4; + else if (desc.Contains(t)) score += 2; + else return 0; // hard reject: token missing + } + // Pages rank slightly higher than headings when otherwise-equal. + if (e.Kind == SearchEntryKind.Page) score += 2; + return score; + } + + private record Scored(SearchEntry Entry, int Score); + + public void Dispose() + { + State.OnChange -= OnStateChange; + _selfRef?.Dispose(); + } +} diff --git a/src/ShellDocs.Components/Chrome/SearchDialog.razor.css b/src/ShellDocs.Components/Chrome/SearchDialog.razor.css new file mode 100644 index 0000000..5832fa3 --- /dev/null +++ b/src/ShellDocs.Components/Chrome/SearchDialog.razor.css @@ -0,0 +1,167 @@ +.search-backdrop { + position: fixed; + inset: 0; + background: rgb(0 0 0 / 0.55); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 90; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 8vh; + animation: search-fade-in 140ms ease; +} +@keyframes search-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.search-panel { + width: min(38rem, calc(100vw - 2rem)); + max-height: 70vh; + background: var(--popover); + color: var(--popover-foreground); + border: 1px solid var(--border); + border-radius: calc(var(--radius) + 4px); + box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.35); + display: flex; + flex-direction: column; + overflow: hidden; + animation: search-scale-in 160ms cubic-bezier(0.16, 1, 0.3, 1); +} +@keyframes search-scale-in { + from { opacity: 0; transform: translateY(-8px) scale(0.98); } + to { opacity: 1; transform: none; } +} + +.search-input-row { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.75rem 0.85rem; + border-bottom: 1px solid var(--border); +} +.search-icon { width: 1rem; height: 1rem; color: var(--muted-foreground); flex-shrink: 0; } +.search-input { + flex: 1; + min-width: 0; + background: transparent; + border: 0; + outline: 0; + color: var(--foreground); + font-family: inherit; + font-size: 0.95rem; + padding: 0.15rem 0; +} +.search-input::placeholder { color: var(--muted-foreground); } +.search-esc { + font-family: var(--font-mono); + font-size: 0.7rem; + padding: 0.15rem 0.45rem; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--muted-foreground); +} + +.search-results { + overflow-y: auto; + padding: 0.35rem 0.35rem; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} +.search-empty { + padding: 2rem 1rem; + text-align: center; + color: var(--muted-foreground); + font-size: 0.875rem; +} + +.search-result { + display: flex; + align-items: flex-start; + gap: 0.65rem; + width: 100%; + padding: 0.55rem 0.65rem; + background: transparent; + border: 0; + border-radius: calc(var(--radius) - 3px); + color: var(--foreground); + text-align: left; + cursor: pointer; + font-family: inherit; + font-size: 0.875rem; + transition: background 100ms; +} +.search-result.highlighted { background: var(--accent); } + +.search-result-kind { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.65rem; + height: 1.65rem; + border-radius: 5px; + background: var(--muted); + color: var(--muted-foreground); + flex-shrink: 0; + margin-top: 1px; +} +.search-result.highlighted .search-result-kind { color: var(--foreground); } +.search-result-kind svg { width: 0.85rem; height: 0.85rem; } + +.search-result-body { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; + flex: 1; +} +.search-result-title { + font-weight: 500; + color: var(--foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.search-result-section { + font-size: 0.75rem; + color: var(--muted-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-footer { + display: flex; + gap: 1rem; + padding: 0.55rem 0.85rem; + border-top: 1px solid var(--border); + background: color-mix(in oklch, var(--muted) 40%, transparent); + font-size: 0.72rem; + color: var(--muted-foreground); +} +.search-hint { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} +.search-footer kbd { + font-family: var(--font-mono); + font-size: 0.68rem; + padding: 0.05rem 0.35rem; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--background); + color: var(--muted-foreground); + min-width: 1.05rem; + line-height: 1; + text-align: center; +} +.search-footer .kbd-icon { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.15rem; + min-width: 0; +} +.search-footer .kbd-icon svg { width: 0.6rem; height: 0.6rem; } diff --git a/src/ShellDocs.Components/Chrome/SearchState.cs b/src/ShellDocs.Components/Chrome/SearchState.cs new file mode 100644 index 0000000..9a5ae9b --- /dev/null +++ b/src/ShellDocs.Components/Chrome/SearchState.cs @@ -0,0 +1,14 @@ +namespace ShellDocs.Components.Chrome; + +/* Global open/close state for . Any component (header chip, + sidebar chip, or the Cmd+K JS shortcut) can call Open(); the dialog itself + subscribes to OnChange to re-render. */ +public class SearchState +{ + public bool IsOpen { get; private set; } + public event Action? OnChange; + + public void Open() { if (!IsOpen) { IsOpen = true; OnChange?.Invoke(); } } + public void Close() { if (IsOpen) { IsOpen = false; OnChange?.Invoke(); } } + public void Toggle() { IsOpen = !IsOpen; OnChange?.Invoke(); } +} diff --git a/src/ShellDocs.Components/Chrome/SidebarCollapseState.cs b/src/ShellDocs.Components/Chrome/SidebarCollapseState.cs new file mode 100644 index 0000000..bb8856e --- /dev/null +++ b/src/ShellDocs.Components/Chrome/SidebarCollapseState.cs @@ -0,0 +1,14 @@ +namespace ShellDocs.Components.Chrome; + +/* Desktop-only collapse state for the Sidebar-variant docs layout. Distinct + from MobileNavState — mobile drawer is temporary/overlay, this is a + persistent "hide the sidebar until I click reopen" toggle. */ +public class SidebarCollapseState +{ + public bool IsCollapsed { get; private set; } + public event Action? OnChange; + + public void Toggle() { IsCollapsed = !IsCollapsed; OnChange?.Invoke(); } + public void Collapse() { if (!IsCollapsed) { IsCollapsed = true; OnChange?.Invoke(); } } + public void Expand() { if (IsCollapsed) { IsCollapsed = false; OnChange?.Invoke(); } } +} diff --git a/src/ShellDocs.Components/Chrome/TableOfContents.razor.css b/src/ShellDocs.Components/Chrome/TableOfContents.razor.css index f2bb079..8e4c87c 100644 --- a/src/ShellDocs.Components/Chrome/TableOfContents.razor.css +++ b/src/ShellDocs.Components/Chrome/TableOfContents.razor.css @@ -9,16 +9,14 @@ font-size: 0.75rem; font-weight: 500; color: var(--muted-foreground); - padding: 2rem 0 0.75rem 0.75rem; - /* Sticks to the top of the scrolling TOC slot; solid background so links - scroll cleanly underneath. A subtle fade below hints at more content. */ + padding: 1rem 0 0.75rem 0.75rem; + /* Sticky at top of the scrolling TOC slot. Solid bg = content hides + cleanly under it (no bleed-through). Bottom border optional; kept + transparent to stay minimal. */ position: sticky; top: 0; z-index: 5; - background: linear-gradient(to bottom, - var(--background) 0%, - var(--background) calc(100% - 8px), - color-mix(in oklch, var(--background) 40%, transparent) 100%); + background: var(--background); } .toc-title svg { width: 0.9rem; height: 0.9rem; } diff --git a/src/ShellDocs.Components/Layouts/DocsLayout.razor b/src/ShellDocs.Components/Layouts/DocsLayout.razor index 94d679a..0ffb0b6 100644 --- a/src/ShellDocs.Components/Layouts/DocsLayout.razor +++ b/src/ShellDocs.Components/Layouts/DocsLayout.razor @@ -1,13 +1,41 @@ @inherits LayoutComponentBase @inject NavigationManager Nav @inject ShellDocs.Components.Chrome.MobileNavState MobileNav +@inject ShellDocs.Components.Chrome.SidebarCollapseState Collapse +@inject ShellDocsOptions Options +@inject IJSRuntime JS @implements IDisposable @using Microsoft.AspNetCore.Components.Sections -
- +
+ @if (Options.LayoutVariant == DocsLayoutVariant.TopNav) + { + + } + else + { + + } + + @* Floating reopen toolbar — only visible in Sidebar variant when collapsed *@ + @if (Options.LayoutVariant == DocsLayoutVariant.Sidebar && Collapse.IsCollapsed) + { +
+ + +
+ } +
@if (MobileNav.IsOpen) @@ -25,19 +53,31 @@
+ + @code { + private string Variant => Options.LayoutVariant switch + { + DocsLayoutVariant.Sidebar => "sidebar", + _ => "topnav" + }; + protected override void OnInitialized() { MobileNav.OnChange += StateHasChanged; + Collapse.OnChange += StateHasChanged; Nav.LocationChanged += OnRouteChanged; } private void OnRouteChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e) => MobileNav.Close(); + private Task OpenSearch() => JS.InvokeVoidAsync("shelldocsSearch.open").AsTask(); + public void Dispose() { MobileNav.OnChange -= StateHasChanged; + Collapse.OnChange -= StateHasChanged; Nav.LocationChanged -= OnRouteChanged; } } diff --git a/src/ShellDocs.Components/Layouts/DocsLayout.razor.css b/src/ShellDocs.Components/Layouts/DocsLayout.razor.css index 311b90f..4d48a68 100644 --- a/src/ShellDocs.Components/Layouts/DocsLayout.razor.css +++ b/src/ShellDocs.Components/Layouts/DocsLayout.razor.css @@ -118,3 +118,100 @@ @media (min-width: 1280px) { .docs-toc-slot { display: block; } } + +/* ─────────────────────────────────────────────────────────────────── + Sidebar variant — no top nav, sidebar is the ONLY floating card, + content sits flush against the page bg. shadcn sidebar-04 vibe. + ─────────────────────────────────────────────────────────────────── */ + +.docs-shell-sidebar { background: var(--background); } + +@media (min-width: 1024px) { + .docs-shell-sidebar .docs-body { + padding: 0.85rem 1rem 1rem 0.85rem; + gap: 1rem; + max-width: 1500px; + grid-template-columns: var(--sidebar-width) 1fr; + } + /* Sidebar = the floating card */ + .docs-shell-sidebar .docs-sidebar-slot { + display: flex; + flex-direction: column; + position: sticky; + top: 0.85rem; + height: calc(100vh - 1.85rem); + background: var(--card); + border: 1px solid var(--border); + border-radius: calc(var(--radius) + 2px); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.04); + margin-right: 0; + overflow: hidden; + } + /* DocsSidebar's own