From 0fbd54afdc0c0e660fd2ba432029e7ba9c48c27b Mon Sep 17 00:00:00 2001 From: Shewart Date: Fri, 2 Oct 2026 21:46:40 +0200 Subject: [PATCH 1/7] fix(preview): keep page prose styles out of live examples The .shelldocs-prose rules (paragraph margins, list padding, heading sizes, link underlines) also matched components inside preview frames and, being unlayered, beat Tailwind's layered utilities. Every prose rule now skips .not-prose subtrees through a zero-specificity :where(), and PreviewFrame carries the class. Consumers can add it to any other element. --- .../Content/PreviewFrame.razor | 3 +- .../wwwroot/shelldocs-theme.css | 46 ++++++++++--------- tests/ShellDocs.Tests/PreviewToolbarTests.cs | 2 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/ShellDocs.Components/Content/PreviewFrame.razor b/src/ShellDocs.Components/Content/PreviewFrame.razor index d80501f..6eb7476 100644 --- a/src/ShellDocs.Components/Content/PreviewFrame.razor +++ b/src/ShellDocs.Components/Content/PreviewFrame.razor @@ -10,7 +10,8 @@ so they work without a Blazor runtime. Both panels are always rendered; CSS shows the one named by [data-preview-tab]. Fed either a PreviewSlot (razor:preview fence) or Content + Code (DemoPreview, ComponentPreview). *@ -
+@* not-prose: page typography stays out of live examples. *@ +
@if (!string.IsNullOrEmpty(Title)) {
@Title
diff --git a/src/ShellDocs.Components/wwwroot/shelldocs-theme.css b/src/ShellDocs.Components/wwwroot/shelldocs-theme.css index 1baab8c..e0dd013 100644 --- a/src/ShellDocs.Components/wwwroot/shelldocs-theme.css +++ b/src/ShellDocs.Components/wwwroot/shelldocs-theme.css @@ -23,19 +23,21 @@ body { margin: 0; min-height: 100vh; } ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid var(--background); } ::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); } -/* Prose */ +/* Prose. Every rule skips .not-prose subtrees (preview frames, or any element a + consumer marks), so live components keep their own spacing and link styles. + :where() keeps the exclusion at zero specificity. */ .shelldocs-prose { color: var(--foreground); font-size: 0.95rem; line-height: 1.7; } .shelldocs-prose > *:first-child { margin-top: 0; } .shelldocs-prose > *:last-child { margin-bottom: 0; } -.shelldocs-prose h1 { font-size: 2.125rem; font-weight: 700; letter-spacing: -0.025em; margin: 0 0 0.75rem; line-height: 1.2; color: var(--foreground); } -.shelldocs-prose h2 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 2.5rem 0 0.75rem; line-height: 1.3; scroll-margin-top: calc(var(--header-height) + 2rem); } -.shelldocs-prose h3 { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.015em; margin: 1.75rem 0 0.5rem; line-height: 1.4; scroll-margin-top: calc(var(--header-height) + 2rem); } -.shelldocs-prose h4 { font-size: 1rem; font-weight: 600; margin: 1.25rem 0 0.5rem; } +.shelldocs-prose h1:where(:not(.not-prose, .not-prose *)) { font-size: 2.125rem; font-weight: 700; letter-spacing: -0.025em; margin: 0 0 0.75rem; line-height: 1.2; color: var(--foreground); } +.shelldocs-prose h2:where(:not(.not-prose, .not-prose *)) { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 2.5rem 0 0.75rem; line-height: 1.3; scroll-margin-top: calc(var(--header-height) + 2rem); } +.shelldocs-prose h3:where(:not(.not-prose, .not-prose *)) { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.015em; margin: 1.75rem 0 0.5rem; line-height: 1.4; scroll-margin-top: calc(var(--header-height) + 2rem); } +.shelldocs-prose h4:where(:not(.not-prose, .not-prose *)) { font-size: 1rem; font-weight: 600; margin: 1.25rem 0 0.5rem; } -.shelldocs-prose p { margin: 1rem 0; color: var(--foreground); } +.shelldocs-prose p:where(:not(.not-prose, .not-prose *)) { margin: 1rem 0; color: var(--foreground); } -.shelldocs-prose a { +.shelldocs-prose a:where(:not(.not-prose, .not-prose *)) { color: var(--foreground); text-decoration: underline; text-underline-offset: 3px; @@ -44,11 +46,11 @@ body { margin: 0; min-height: 100vh; } transition: text-decoration-color 150ms; font-weight: 500; } -.shelldocs-prose a:hover { text-decoration-color: var(--foreground); } +.shelldocs-prose a:hover:where(:not(.not-prose, .not-prose *)) { text-decoration-color: var(--foreground); } -.shelldocs-prose strong { font-weight: 600; color: var(--foreground); } +.shelldocs-prose strong:where(:not(.not-prose, .not-prose *)) { font-weight: 600; color: var(--foreground); } -.shelldocs-prose code { +.shelldocs-prose code:where(:not(.not-prose, .not-prose *)) { font-family: var(--font-mono); font-size: 0.85em; background: var(--muted); @@ -59,11 +61,11 @@ body { margin: 0; min-height: 100vh; } font-feature-settings: 'ss01'; } -.shelldocs-prose ul, .shelldocs-prose ol { padding-left: 1.5rem; margin: 1rem 0; } -.shelldocs-prose li { margin: 0.35rem 0; } -.shelldocs-prose li::marker { color: var(--muted-foreground); } +.shelldocs-prose ul:where(:not(.not-prose, .not-prose *)), .shelldocs-prose ol:where(:not(.not-prose, .not-prose *)) { padding-left: 1.5rem; margin: 1rem 0; } +.shelldocs-prose li:where(:not(.not-prose, .not-prose *)) { margin: 0.35rem 0; } +.shelldocs-prose li:where(:not(.not-prose, .not-prose *))::marker { color: var(--muted-foreground); } -.shelldocs-prose blockquote { +.shelldocs-prose blockquote:where(:not(.not-prose, .not-prose *)) { border-left: 3px solid var(--border-strong); padding: 0.75rem 0 0.75rem 1rem; margin: 1.25rem 0; @@ -71,9 +73,9 @@ body { margin: 0; min-height: 100vh; } background: color-mix(in oklch, var(--muted) 60%, transparent); border-radius: 0 var(--radius) var(--radius) 0; } -.shelldocs-prose blockquote p { margin: 0; } +.shelldocs-prose blockquote p:where(:not(.not-prose, .not-prose *)) { margin: 0; } -.shelldocs-prose table { +.shelldocs-prose table:where(:not(.not-prose, .not-prose *)) { border-collapse: collapse; width: 100%; margin: 1.25rem 0; @@ -82,13 +84,13 @@ body { margin: 0; min-height: 100vh; } border-radius: var(--radius); overflow: hidden; } -.shelldocs-prose th, .shelldocs-prose td { padding: 0.625rem 0.875rem; text-align: left; border-bottom: 1px solid var(--border); } -.shelldocs-prose th { font-weight: 600; color: var(--foreground); background: var(--muted); font-size: 0.8125rem; } -.shelldocs-prose tr:last-child td { border-bottom: 0; } -.shelldocs-prose tr:hover { background: color-mix(in oklch, var(--muted) 40%, transparent); } +.shelldocs-prose th:where(:not(.not-prose, .not-prose *)), .shelldocs-prose td:where(:not(.not-prose, .not-prose *)) { padding: 0.625rem 0.875rem; text-align: left; border-bottom: 1px solid var(--border); } +.shelldocs-prose th:where(:not(.not-prose, .not-prose *)) { font-weight: 600; color: var(--foreground); background: var(--muted); font-size: 0.8125rem; } +.shelldocs-prose tr:last-child td:where(:not(.not-prose, .not-prose *)) { border-bottom: 0; } +.shelldocs-prose tr:hover:where(:not(.not-prose, .not-prose *)) { background: color-mix(in oklch, var(--muted) 40%, transparent); } -.shelldocs-prose hr { border: 0; border-top: 1px solid var(--border); margin: 2.5rem 0; } -.shelldocs-prose img { border-radius: var(--radius); border: 1px solid var(--border); max-width: 100%; height: auto; } +.shelldocs-prose hr:where(:not(.not-prose, .not-prose *)) { border: 0; border-top: 1px solid var(--border); margin: 2.5rem 0; } +.shelldocs-prose img:where(:not(.not-prose, .not-prose *)) { border-radius: var(--radius); border: 1px solid var(--border); max-width: 100%; height: auto; } /* Code blocks — wrapped by CodeBlockEnhancer */ .shelldocs-codeblock { diff --git a/tests/ShellDocs.Tests/PreviewToolbarTests.cs b/tests/ShellDocs.Tests/PreviewToolbarTests.cs index 44d26c2..4848570 100644 --- a/tests/ShellDocs.Tests/PreviewToolbarTests.cs +++ b/tests/ShellDocs.Tests/PreviewToolbarTests.cs @@ -89,7 +89,7 @@ public async Task ComponentPreview_RendersThroughPreviewFrame() ["ExtraProps"] = new Dictionary { ["Variant"] = "Outline" } }); - Assert.Contains("class=\"preview-frame\"", html); + Assert.Contains("class=\"preview-frame not-prose\"", html); Assert.Contains("id=\"example-button-", html); Assert.Contains("data-variant=\"Outline\"", html); Assert.Contains("<Button Variant="Outline" />", html); From d75e78593fd20ca9e874eb0075877839a43a5aba Mon Sep 17 00:00:00 2001 From: Shewart Date: Fri, 2 Oct 2026 21:46:42 +0200 Subject: [PATCH 2/7] fix(preview): parse component child content inside previews as Razor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Child content went through markdown and SlotSplitter, so closed the div before the nested component and wrapped loose text in

. Inside a razor:preview, and in ComponentPreview bodies, children are now parsed with PreviewParser (MarkdownRenderer.ParseRazor) and emitted as real elements, recursively. Inline component tags in prose keep markdown bodies. --- .../Content/ComponentPreview.razor | 14 ++++++++++- .../Content/PreviewFrame.razor | 2 +- .../Content/SlotRenderer.cs | 24 +++++++++++++++---- src/ShellDocs.Markdown/MarkdownRenderer.cs | 10 ++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/ShellDocs.Components/Content/ComponentPreview.razor b/src/ShellDocs.Components/Content/ComponentPreview.razor index f09d145..0f91644 100644 --- a/src/ShellDocs.Components/Content/ComponentPreview.razor +++ b/src/ShellDocs.Components/Content/ComponentPreview.razor @@ -3,6 +3,7 @@ @using System.Text @using ShellDocs.Markdown @inject TypeRegistry Registry +@inject MarkdownRenderer Renderer @inject ILogger Logger (); + foreach (var (k, v) in SlotRenderer.BuildParameters(Renderer, target, empty, ChildContentSource, Logger, razorChildren: true)) + dict[k] = v; + } + else if (ChildContent is not null) + { + dict["ChildContent"] = ChildContent; + } return dict; } diff --git a/src/ShellDocs.Components/Content/PreviewFrame.razor b/src/ShellDocs.Components/Content/PreviewFrame.razor index 6eb7476..ace0ce1 100644 --- a/src/ShellDocs.Components/Content/PreviewFrame.razor +++ b/src/ShellDocs.Components/Content/PreviewFrame.razor @@ -135,5 +135,5 @@ // Only reached when ErrorMessage is null, i.e. ComponentType is non-null. private IDictionary BuildParameters(IReadOnlyDictionary attrs, string? childContentRaw) => - SlotRenderer.BuildParameters(Renderer, Preview!.ComponentType!, attrs, childContentRaw, Logger); + SlotRenderer.BuildParameters(Renderer, Preview!.ComponentType!, attrs, childContentRaw, Logger, razorChildren: true); } diff --git a/src/ShellDocs.Components/Content/SlotRenderer.cs b/src/ShellDocs.Components/Content/SlotRenderer.cs index 57341c8..9ce258b 100644 --- a/src/ShellDocs.Components/Content/SlotRenderer.cs +++ b/src/ShellDocs.Components/Content/SlotRenderer.cs @@ -33,6 +33,14 @@ authored at the outer tag's indent would render as

. */
         }
     };
 
+    // Child content inside a razor:preview is Razor, not markdown.
+    public static RenderFragment FromRazor(MarkdownRenderer renderer, string raw, ILogger? logger = null)
+    {
+        var (nodes, warnings) = renderer.ParseRazor(raw);
+        foreach (var warning in warnings) logger?.LogWarning("ShellDocs: {Warning}", warning);
+        return RenderNodes(renderer, nodes, logger);
+    }
+
     private static void Emit(RenderTreeBuilder builder, ref int seq, MarkdownRenderer renderer, ComponentSlot slot, ILogger? logger)
     {
         builder.OpenComponent(seq++);
@@ -61,7 +69,7 @@ private static void EmitNodes(RenderTreeBuilder builder, MarkdownRenderer render
                 case PreviewComponentNode comp:
                     builder.OpenComponent(0);
                     builder.AddAttribute(1, "Type", comp.ComponentType);
-                    builder.AddAttribute(2, "Parameters", BuildParameters(renderer, comp.ComponentType, comp.Parameters, comp.ChildContentRaw, logger));
+                    builder.AddAttribute(2, "Parameters", BuildParameters(renderer, comp.ComponentType, comp.Parameters, comp.ChildContentRaw, logger, razorChildren: true));
                     builder.CloseComponent();
                     break;
 
@@ -114,14 +122,20 @@ private static string Dedent(string raw)
     /* Never throws. Anything static markup can't set — directive attributes,
        EventCallback/delegate params, unsupported types, unparseable values, unknown
        attributes without a CaptureUnmatchedValues catch-all — is skipped with a
-       warning and the component still renders. */
+       warning and the component still renders. With razorChildren (razor:preview)
+       child content is parsed as Razor; otherwise it's markdown, as in prose. */
     public static IDictionary BuildParameters(
         MarkdownRenderer renderer,
         Type componentType,
         IReadOnlyDictionary attrs,
         string? childContentRaw,
-        ILogger? logger = null)
+        ILogger? logger = null,
+        bool razorChildren = false)
     {
+        RenderFragment Fragment(string raw) => razorChildren
+            ? FromRazor(renderer, raw, logger)
+            : FromMarkup(renderer, raw, logger);
+
         var dict = new Dictionary(StringComparer.Ordinal);
         var props = GetParameterProps(componentType);
         var catchAll = HasCatchAll(componentType);
@@ -167,7 +181,7 @@ public static IDictionary BuildParameters(
                     var extracted = ExtractNamedSlot(remaining, slotName);
                     if (extracted.Content is not null)
                     {
-                        dict[slotName] = FromMarkup(renderer, extracted.Content, logger);
+                        dict[slotName] = Fragment(extracted.Content);
                         remaining = extracted.Remaining;
                     }
                 }
@@ -178,7 +192,7 @@ public static IDictionary BuildParameters(
                 // Only a plain RenderFragment ChildContent can take markup; a
                 // missing or templated (RenderFragment) one would throw.
                 if (props.TryGetValue("ChildContent", out var cc) && cc.PropertyType == typeof(RenderFragment))
-                    dict["ChildContent"] = FromMarkup(renderer, remaining, logger);
+                    dict["ChildContent"] = Fragment(remaining);
                 else
                     Skip(logger, componentType, "ChildContent", "component has no RenderFragment ChildContent parameter");
             }
diff --git a/src/ShellDocs.Markdown/MarkdownRenderer.cs b/src/ShellDocs.Markdown/MarkdownRenderer.cs
index beb68f6..e06094f 100644
--- a/src/ShellDocs.Markdown/MarkdownRenderer.cs
+++ b/src/ShellDocs.Markdown/MarkdownRenderer.cs
@@ -34,4 +34,14 @@ public RenderedDocument Render(string markdown)
     }
 
     public RenderedDocument RenderFile(string path) => Render(File.ReadAllText(path));
+
+    // Razor markup (component child content inside a razor:preview) parsed into
+    // nodes the way the fence itself is: no markdown pass, so nothing gets wrapped
+    // in 

and elements around nested components stay intact. + public (IReadOnlyList Nodes, IReadOnlyList Warnings) ParseRazor(string markup) + { + var warnings = new List(); + var nodes = new PreviewParser(_registry, warnings).Parse(markup ?? ""); + return (nodes, warnings); + } } From bd3912e59e31b50fc522f1ddf98b31ff15546d1c Mon Sep 17 00:00:00 2001 From: Shewart Date: Fri, 2 Oct 2026 21:46:43 +0200 Subject: [PATCH 3/7] feat(preview): stretch layout for block-level examples Preview content is centred in a flex row, so charts, inputs and selects shrank to their text. razor:preview stretch (fence info string) or Layout="stretch" on PreviewFrame, DemoPreview and ComponentPreview lays the example out in one column at the frame's width. Center stays the default. --- .../Content/ComponentPreview.razor | 5 ++++- src/ShellDocs.Components/Content/DemoPreview.razor | 5 ++++- src/ShellDocs.Components/Content/PreviewFrame.razor | 7 ++++++- .../Content/PreviewFrame.razor.css | 7 +++++++ src/ShellDocs.Markdown/RenderedDocument.cs | 4 +++- src/ShellDocs.Markdown/SlotExtractor.cs | 13 ++++++++++--- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/ShellDocs.Components/Content/ComponentPreview.razor b/src/ShellDocs.Components/Content/ComponentPreview.razor index 0f91644..cb17061 100644 --- a/src/ShellDocs.Components/Content/ComponentPreview.razor +++ b/src/ShellDocs.Components/Content/ComponentPreview.razor @@ -11,13 +11,16 @@ Error="@(_target is null ? $"Unknown component <{Component}>." : null)" ErrorTitle="ComponentPreview error" Id="@_id" - Name="@Component" /> + Name="@Component" + Layout="@Layout" /> @code { [Parameter, EditorRequired] public string? Component { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; } // Raw child markup from SlotRenderer, used to rebuild the source view. [Parameter] public string? ChildContentSource { get; set; } + // "center" (default) or "stretch"; see PreviewFrame. + [Parameter] public string? Layout { get; set; } [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? ExtraProps { get; set; } diff --git a/src/ShellDocs.Components/Content/DemoPreview.razor b/src/ShellDocs.Components/Content/DemoPreview.razor index 3b56a08..cad06ad 100644 --- a/src/ShellDocs.Components/Content/DemoPreview.razor +++ b/src/ShellDocs.Components/Content/DemoPreview.razor @@ -10,13 +10,16 @@ Error="@_error" ErrorTitle="DemoPreview error" Id="@(Id ?? $"demo-{PreviewLinks.Slug(Component)}")" - Name="@Component" /> + Name="@Component" + Layout="@Layout" /> @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; } + // "center" (default) or "stretch"; see PreviewFrame. + [Parameter] public string? Layout { get; set; } private RenderFragment? _content; private string? _source; diff --git a/src/ShellDocs.Components/Content/PreviewFrame.razor b/src/ShellDocs.Components/Content/PreviewFrame.razor index ace0ce1..5bf4f12 100644 --- a/src/ShellDocs.Components/Content/PreviewFrame.razor +++ b/src/ShellDocs.Components/Content/PreviewFrame.razor @@ -60,7 +60,7 @@

-
+
@if (ErrorMessage is not null) { + + ``` + """); + + var nav = Regex.Match(html, "", RegexOptions.Singleline).Groups[1].Value; + Assert.DoesNotContain("

", nav); + Assert.Contains("MyApp", nav); + Assert.Matches("

Docs]*>Go
", nav); + } + + [Fact] + public async Task RazorPreview_NestedComponents_AreRazorAllTheWayDown() + { + var html = await Render(""" + ```razor:preview + +
+
+ ``` + """); + + Assert.Matches("
", html); + Assert.DoesNotContain("

", Regex.Match(html, "

", RegexOptions.Singleline).Value); + } + + [Fact] + public async Task InlineComponent_InProse_StillTakesMarkdownChildren() + { + var html = await Render("Some text.\n\n**bold** body\n"); + + Assert.Contains("bold", html); + } + + [Fact] + public async Task ComponentPreview_ChildContent_IsParsedAsRazor() + { + var html = await Render("\n
\n
\n"); + + // HtmlRenderer encodes the newline text nodes around the div as . + Assert.Matches("", html); + } + + [Fact] + public async Task PreviewLayout_DefaultsToCenter_StretchFromFenceOrParameter() + { + var centered = await Render("```razor:preview\n\n```"); + Assert.Contains("data-layout=\"center\"", centered); + + var stretched = await Render("```razor:preview stretch\n\n```"); + Assert.Contains("data-layout=\"stretch\"", stretched); + + var component = await Harness().RenderAsync(new() { ["Component"] = "Button", ["Layout"] = "stretch" }); + Assert.Contains("data-layout=\"stretch\"", component); + } + + [Fact] + public async Task PreviewFrame_OptsOutOfProse() + { + var html = await Render("```razor:preview\n\n```"); + Assert.Contains("class=\"preview-frame not-prose\"", html); + } + + [Fact] + public void ProseRules_AllSkipNotProseSubtrees() + { + var css = ReadThemeCss(); + var selectors = Regex.Matches(css, @"^(\.shelldocs-prose [^{]+)\{", RegexOptions.Multiline) + .SelectMany(m => Regex.Split(m.Groups[1].Value, @",(?![^(]*\))")) + .Select(s => s.Trim()) + .Where(s => s.StartsWith(".shelldocs-prose ") && !s.StartsWith(".shelldocs-prose >") && !s.Contains("pre.shiki")) + .ToList(); + + Assert.NotEmpty(selectors); + Assert.All(selectors, s => Assert.Contains(":where(:not(.not-prose, .not-prose *))", s)); + } + + [Fact] + public async Task ThemeToggle_IsStaticHostFriendly() + { + var html = await Harness().RenderAsync(); + + Assert.Contains("data-theme-toggle", html); + Assert.Contains("theme-icon-light", html); + Assert.Contains("theme-icon-dark", html); + Assert.DoesNotContain("blazor:onclick", html); + } + + private static string ReadThemeCss() + { + var testDir = Path.GetDirectoryName(typeof(PreviewFidelityTests).Assembly.Location)!; + var candidates = new[] + { + Path.Combine(testDir, "wwwroot", "_content", "ShellDocs.Components", "shelldocs-theme.css"), + Path.Combine(testDir, "..", "..", "..", "..", "..", "src", "ShellDocs.Components", "wwwroot", "shelldocs-theme.css") + }; + foreach (var c in candidates) + { + var full = Path.GetFullPath(c); + if (File.Exists(full)) return File.ReadAllText(full); + } + throw new FileNotFoundException("shelldocs-theme.css not found"); + } +} From 1e995fa6d949ac68e1895e5ca2e0e82ca943be2e Mon Sep 17 00:00:00 2001 From: Shewart Date: Fri, 2 Oct 2026 21:46:49 +0200 Subject: [PATCH 7/7] docs: Razor child content, preview layout, not-prose and the static theme toggle CHANGELOG Unreleased entries; README preview bullets and static-export list; ARCHITECTURE on Razor child content, PreviewFrame layout and not-prose, the theme row and enhancedload; example pages for markdown syntax, ComponentPreview, theming and installation. --- CHANGELOG.md | 17 ++++++++++++++ README.md | 5 ++++- docs/ARCHITECTURE.md | 12 +++++----- .../docs/components/component-preview.md | 3 ++- .../content/docs/installation.md | 2 +- .../content/docs/markdown-syntax.md | 22 +++++++++++++++++++ .../ShellDocs.Preview/content/docs/theming.md | 2 +- 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ffc2c..336cfa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht ## [Unreleased] +### Added + +- **Preview layout.** `razor:preview stretch` (fence info string), or `Layout="stretch"` on `` / `` / `PreviewFrame`, lets block-level components (charts, inputs, tables) fill the frame instead of shrinking to their content. `center` stays the default. +- **`not-prose` class.** Every `.shelldocs-prose` rule skips `.not-prose` subtrees. Preview frames carry it; add it to any element that should keep page typography out. + +### Changed + +- **`razor:preview` child content is parsed as Razor, not markdown.** Component bodies inside a fence (and `` bodies) go through the same node parser as the fence itself: elements wrap nested components exactly as written and nothing is wrapped in `

`. Inline component tags in prose still take markdown bodies. +- **`ThemeToggle` works without a Blazor runtime.** Clicks are handled by `shelldocs.js` and both icons render, with CSS picking one from ``. + +### Fixed + +- **Prose styles leaked into previews.** Paragraph margins, list padding, heading sizes and link underlines from `.shelldocs-prose` hit live components and beat Tailwind's layered utilities (gaps inside cards and menus, underlined nav links). +- **Nested markup inside a preview component was mangled.** `

` closed the `
` before the nested component and wrapped loose text in `

`. +- **Theme state only followed ShellDocs' own toggle.** A component library flipping `` left the stored theme and `ThemeState` stale. `shelldocs.js` now watches the class, saves it under `shelldocs-theme` and pushes it into `ThemeState`. +- **`enhancedload` handlers never ran.** Blazor raises the event through `Blazor.addEventListener`, not as a DOM event, so re-applying the theme, re-attaching the TOC and closing the mobile nav after enhanced navigation (static SSR apps) silently did nothing. + ## [0.1.9-alpha] — 2026-10-02 A Preview | Code toolbar for every example, fixes for chrome that reset or ignored early clicks, consumer components no longer losing to built-ins with the same name, and code spans that stay code. diff --git a/README.md b/README.md index 37c5845..627b93d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds ``` Your components win name collisions with ShellDocs' built-ins, which stay available as ``, ``, ``, and so on. - **Content primitives.** `Callout`, `Card` / `CardGrid` / `LinkCard`, `Steps`, `FileTree`, `Tabs`, `CodeGroup`, `TypeTable` / `AutoTypeTable`, `ComponentPreview`, `DemoPreview`. -- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `` (`--base-href`), add a SPA `404.html` (`--spa-fallback`), and write sitemap / robots / `og:` meta (`--site-url`). Navigation, sidebar sections, the mobile menu, selectors, tabs, preview toolbars, the TOC and code copy work there through `shelldocs.js`. Search, the theme-toggle button, desktop sidebar collapse and stateful demos need a running Blazor app. +- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `` (`--base-href`), add a SPA `404.html` (`--spa-fallback`), and write sitemap / robots / `og:` meta (`--site-url`). Navigation, sidebar sections, the mobile menu, selectors, tabs, preview toolbars, the TOC and code copy work there through `shelldocs.js`. So does the theme toggle. Search, desktop sidebar collapse and stateful demos need a running Blazor app. ## Versioned docs @@ -69,6 +69,9 @@ It's all server-rendered links plus `shelldocs.js`, so it works on static hosts. - **Preview | Code toolbar on every example**, with copy and a ⋯ menu: *Open in new tab*, *Report a bug*, *Suggest something*. The issue links go to `https://github.com/{GitHubRepo}/issues/new`, pre-filled with the example and page URL; point them elsewhere with `o.IssueTrackerUrl`. - **`razor:preview` fences render everything.** Several sibling components, HTML wrappers (`

…
`) and text render in order inside one frame, and the Code tab shows the whole fence. +- **Child content is Razor.** Component bodies inside a fence are parsed like the fence itself, so `
…
` keeps its structure and nothing is wrapped in `

`. +- **Page typography stays out.** Preview frames are `not-prose`, so prose margins, list padding and link underlines don't reach your components. +- **Centred or stretched.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `` / ``) lets charts, inputs and tables fill the frame. - **Razor-shaped attribute values work:** `Variant="ButtonVariant.Destructive"`, `@ButtonVariant.Destructive`, `@true`, `@42`, `[Flags]` values as `Bold | Italic`. - **Attributes a static preview can't evaluate are skipped, not fatal:** `OnClick="HandleClick"`, `@onclick`, `@bind-*`, `@ref`, non-primitive parameter types, unparseable values. Each logs a warning and the component still renders. - **Inline code stays code.** `` `