From 8a3795b0e7172dab23d96a8922df4f37f2fc34bd Mon Sep 17 00:00:00 2001 From: Shewart Date: Fri, 2 Oct 2026 14:33:22 +0200 Subject: [PATCH 01/17] =?UTF-8?q?feat(preview):=20Preview=20|=20Code=20too?= =?UTF-8?q?lbar=20with=20copy=20and=20=E2=8B=AF=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PreviewFrame gets a header toolbar: Preview | Code tabs (WAI-ARIA, arrow/Home/End keys), a copy button and a ⋯ menu with "Open in new tab", "Report a bug" and "Suggest something". Replaces the "View Code" fade overlay. - Issue links are pre-filled with the example name and page URL; ShellDocsOptions.IssueTrackerUrl overrides the GitHubRepo default, and the items hide when neither is set. - Stable example anchors: preview-N (page order), demo-{component}, example-{component}-{hash}; DemoPreview takes an optional Id. - ComponentPreview renders through PreviewFrame; its own markup and CSS are removed. - Static-host friendly: both panels are server-rendered, shelldocs.js switches tabs via [data-preview-tab] and opens the menu through the shared dropdown contract. --- .../Content/ComponentPreview.razor | 47 ++--- .../Content/ComponentPreview.razor.css | 134 ------------ .../Content/DemoPreview.razor | 6 +- .../Content/MarkdownContent.razor | 4 +- .../Content/PreviewFrame.razor | 100 +++++++-- .../Content/PreviewFrame.razor.css | 193 +++++++++++------- .../Content/PreviewLinks.cs | 78 +++++++ src/ShellDocs.Components/ShellDocsOptions.cs | 4 + src/ShellDocs.Components/wwwroot/shelldocs.js | 66 ++++-- tests/ShellDocs.Tests/PreviewToolbarTests.cs | 119 +++++++++++ 10 files changed, 478 insertions(+), 273 deletions(-) delete mode 100644 src/ShellDocs.Components/Content/ComponentPreview.razor.css create mode 100644 src/ShellDocs.Components/Content/PreviewLinks.cs create mode 100644 tests/ShellDocs.Tests/PreviewToolbarTests.cs diff --git a/src/ShellDocs.Components/Content/ComponentPreview.razor b/src/ShellDocs.Components/Content/ComponentPreview.razor index 6cd4dae..f09d145 100644 --- a/src/ShellDocs.Components/Content/ComponentPreview.razor +++ b/src/ShellDocs.Components/Content/ComponentPreview.razor @@ -5,34 +5,12 @@ @inject TypeRegistry Registry @inject ILogger Logger -@* Expand/collapse and copy are handled by shelldocs.js (data-preview-*). *@ - +." : null)" + ErrorTitle="ComponentPreview error" + Id="@_id" + Name="@Component" /> @code { [Parameter, EditorRequired] public string? Component { get; set; } @@ -45,12 +23,23 @@ private Type? _target; private IDictionary? _targetParams; private string? _source; + private string? _id; protected override void OnParametersSet() { _target = Component is null ? null : Registry.Resolve(Component); _targetParams = _target is null ? null : BuildTargetParams(_target); - _source = _target is null ? null : BuildSource(); + _source = BuildSource(); + // Same component twice on a page gets distinct ids via the source hash. + _id = $"example-{PreviewLinks.Slug(Component)}-{PreviewLinks.ShortHash(_source)}"; + } + + private void RenderTarget(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddComponentParameter(1, "Type", _target); + builder.AddComponentParameter(2, "Parameters", _targetParams); + builder.CloseComponent(); } private IDictionary BuildTargetParams(Type target) diff --git a/src/ShellDocs.Components/Content/ComponentPreview.razor.css b/src/ShellDocs.Components/Content/ComponentPreview.razor.css deleted file mode 100644 index c3ea25a..0000000 --- a/src/ShellDocs.Components/Content/ComponentPreview.razor.css +++ /dev/null @@ -1,134 +0,0 @@ -.component-preview { - border: 1px solid var(--border); - border-radius: calc(var(--radius) + 2px); - background: var(--card); - overflow: hidden; - margin: 1.5rem 0; -} - -.component-preview-render { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; - min-height: 12rem; - padding: 2rem 1.5rem; - background: - repeating-linear-gradient(45deg, - color-mix(in oklch, var(--foreground) 2.5%, transparent) 0, - color-mix(in oklch, var(--foreground) 2.5%, transparent) 1px, - transparent 1px, transparent 8px); -} - -.component-preview-error { - color: var(--destructive, oklch(0.577 0.245 27.325)); - font-family: var(--font-mono); - font-size: 0.8125rem; -} -.component-preview-error code { - background: color-mix(in oklch, var(--destructive, oklch(0.577 0.245 27.325)) 12%, transparent); - padding: 0.1rem 0.4rem; - border-radius: calc(var(--radius) - 4px); -} - -.component-preview-source-wrap { - position: relative; - border-top: 1px solid var(--border); - background: color-mix(in oklch, var(--card) 55%, var(--background)); - overflow: hidden; - transition: max-height 300ms ease; -} -.component-preview.collapsed .component-preview-source-wrap { max-height: 6rem; } -.component-preview.expanded .component-preview-source-wrap { max-height: none; } - -.component-preview-source { - margin: 0; - padding: 1.15rem 1.25rem; - background: transparent; - font-family: var(--font-mono); - font-size: 0.8125rem; - line-height: 1.65; - color: var(--foreground); - overflow-x: auto; -} -.component-preview-source code { - background: transparent !important; - border: 0 !important; - padding: 0 !important; - font-family: var(--font-mono) !important; - font-size: inherit !important; - color: inherit !important; -} - -.component-preview-fade { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient( - to bottom, - transparent 0%, - color-mix(in oklch, var(--card) 40%, transparent) 35%, - var(--card) 75%); - pointer-events: none; -} - -.component-preview-expand { - pointer-events: auto; - background: var(--card); - border: 1px solid var(--border); - border-radius: calc(var(--radius) - 2px); - color: var(--foreground); - padding: 0.45rem 1rem; - font-family: inherit; - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; - box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 8%, transparent); - transition: background 150ms, border-color 150ms; -} -.component-preview-expand:hover { - background: var(--muted); - border-color: color-mix(in oklch, var(--border) 60%, var(--foreground)); -} - -.component-preview-actions { - position: absolute; - top: 0.55rem; - right: 0.6rem; - display: flex; - align-items: center; - gap: 0.35rem; - z-index: 1; -} - -.component-preview-copy, -.component-preview-hide { - display: inline-flex; - align-items: center; - gap: 0.3rem; - padding: 0.3rem 0.55rem; - background: color-mix(in oklch, var(--card) 92%, var(--foreground)); - border: 1px solid var(--border); - border-radius: calc(var(--radius) - 3px); - color: var(--muted-foreground); - font-family: inherit; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: color 150ms, background 150ms, border-color 150ms; -} -.component-preview-copy:hover, -.component-preview-hide:hover { - color: var(--foreground); - background: var(--muted); - border-color: color-mix(in oklch, var(--border) 60%, var(--foreground)); -} -.component-preview-copy.copied { color: var(--success, oklch(0.723 0.219 149.579)); } -.component-preview-copy ::deep svg { width: 0.8125rem; height: 0.8125rem; transition: transform 150ms cubic-bezier(0.34, 1.56, 0.64, 1); } -.component-preview-copy.copied ::deep svg { transform: scale(1.15); } -.component-preview-copy ::deep .icon-check { display: none; } -.component-preview-copy.copied ::deep .icon-copy { display: none; } -.component-preview-copy.copied ::deep .icon-check { display: block; } diff --git a/src/ShellDocs.Components/Content/DemoPreview.razor b/src/ShellDocs.Components/Content/DemoPreview.razor index d203c00..3b56a08 100644 --- a/src/ShellDocs.Components/Content/DemoPreview.razor +++ b/src/ShellDocs.Components/Content/DemoPreview.razor @@ -8,11 +8,15 @@ Code="@(_source ?? $"")" Title="@Title" Error="@_error" - ErrorTitle="DemoPreview error" /> + ErrorTitle="DemoPreview error" + Id="@(Id ?? $"demo-{PreviewLinks.Slug(Component)}")" + Name="@Component" /> @code { [Parameter, EditorRequired] public string? Component { get; set; } [Parameter] public string? Title { get; set; } + // Anchor id; defaults to demo-{component-slug}. Set it when a page shows the same demo twice. + [Parameter] public string? Id { get; set; } private RenderFragment? _content; private string? _source; diff --git a/src/ShellDocs.Components/Content/MarkdownContent.razor b/src/ShellDocs.Components/Content/MarkdownContent.razor index 3d9e19d..12bf0ad 100644 --- a/src/ShellDocs.Components/Content/MarkdownContent.razor +++ b/src/ShellDocs.Components/Content/MarkdownContent.razor @@ -11,6 +11,8 @@ } else { + @* Ordinal ids keep anchors and saved tab state stable across renders. *@ + var previewIndex = 0; @foreach (var part in _parts) { @if (part is HtmlPart html) @@ -25,7 +27,7 @@ } else if (slot.Slot is PreviewSlot preview) { - + } } } diff --git a/src/ShellDocs.Components/Content/PreviewFrame.razor b/src/ShellDocs.Components/Content/PreviewFrame.razor index 2f889b0..d80501f 100644 --- a/src/ShellDocs.Components/Content/PreviewFrame.razor +++ b/src/ShellDocs.Components/Content/PreviewFrame.razor @@ -1,17 +1,65 @@ -@using ShellIcons.Icons @using ShellDocs.Markdown +@using ShellIcons.Icons @attribute [ShellDocsIgnore] @inject MarkdownRenderer Renderer +@inject ShellDocsOptions Options +@inject NavigationManager Nav @inject ILogger Logger -@* Expand/collapse and copy are handled by shelldocs.js (data-preview-*). - Fed either a PreviewSlot (razor:preview fence) or Content + Code (DemoPreview). *@ -