Skip to content

Commit 67df229

Browse files
authored
Merge pull request #29 from shellui-dev/fix/preview-fidelity
fix: preview fidelity : Razor child content, prose isolation, stretch layout, theme sync
2 parents 91255b8 + 1e995fa commit 67df229

21 files changed

Lines changed: 417 additions & 81 deletions

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- **Preview layout.** `razor:preview stretch` (fence info string), or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>` / `PreviewFrame`, lets block-level components (charts, inputs, tables) fill the frame instead of shrinking to their content. `center` stays the default.
10+
- **`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.
11+
12+
### Changed
13+
14+
- **`razor:preview` child content is parsed as Razor, not markdown.** Component bodies inside a fence (and `<ComponentPreview>` bodies) go through the same node parser as the fence itself: elements wrap nested components exactly as written and nothing is wrapped in `<p>`. Inline component tags in prose still take markdown bodies.
15+
- **`ThemeToggle` works without a Blazor runtime.** Clicks are handled by `shelldocs.js` and both icons render, with CSS picking one from `<html class="dark">`.
16+
17+
### Fixed
18+
19+
- **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).
20+
- **Nested markup inside a preview component was mangled.** `<Navbar><div><a/>…<ThemeToggle/></div></Navbar>` closed the `<div>` before the nested component and wrapped loose text in `<p>`.
21+
- **Theme state only followed ShellDocs' own toggle.** A component library flipping `<html class="dark">` left the stored theme and `ThemeState` stale. `shelldocs.js` now watches the class, saves it under `shelldocs-theme` and pushes it into `ThemeState`.
22+
- **`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.
23+
724
## [0.1.9-alpha] — 2026-10-02
825

926
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.

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds
3939
```
4040
Your components win name collisions with ShellDocs' built-ins, which stay available as `<DocsCard>`, `<DocsCallout>`, `<DocsTabs>`, and so on.
4141
- **Content primitives.** `Callout`, `Card` / `CardGrid` / `LinkCard`, `Steps`, `FileTree`, `Tabs`, `CodeGroup`, `TypeTable` / `AutoTypeTable`, `ComponentPreview`, `DemoPreview`.
42-
- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `<base href>` (`--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.
42+
- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `<base href>` (`--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.
4343

4444
## Versioned docs
4545

@@ -69,6 +69,9 @@ It's all server-rendered links plus `shelldocs.js`, so it works on static hosts.
6969

7070
- **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`.
7171
- **`razor:preview` fences render everything.** Several sibling components, HTML wrappers (`<div class="flex gap-2">…</div>`) and text render in order inside one frame, and the Code tab shows the whole fence.
72+
- **Child content is Razor.** Component bodies inside a fence are parsed like the fence itself, so `<Navbar><div>…<ThemeToggle /></div></Navbar>` keeps its structure and nothing is wrapped in `<p>`.
73+
- **Page typography stays out.** Preview frames are `not-prose`, so prose margins, list padding and link underlines don't reach your components.
74+
- **Centred or stretched.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>`) lets charts, inputs and tables fill the frame.
7275
- **Razor-shaped attribute values work:** `Variant="ButtonVariant.Destructive"`, `@ButtonVariant.Destructive`, `@true`, `@42`, `[Flags]` values as `Bold | Italic`.
7376
- **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.
7477
- **Inline code stays code.** `` `<Button>` `` in prose renders as literal code, not a component.

‎docs/ARCHITECTURE.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Placeholders plus a slot list keep everything at render time: no generated Razor
112112

113113
### `razor:preview` fences
114114

115-
`PreviewParser` turns the fence into ordered nodes: component nodes (registered capitalised tags; their children kept raw), element nodes (HTML, including unregistered capitalised tags) and text. `@code { }` / `@functions { }` blocks, `@* *@` comments and directive lines (`@using`, `@inject`, …) are skipped for rendering but stay in the Code tab. If no registered component is found the slot carries an `Error`, rendered as a red panel instead of a silent code block.
115+
`PreviewParser` turns the fence into ordered nodes: component nodes (registered capitalised tags; their children kept raw and parsed the same way, as Razor, when the component renders), element nodes (HTML, including unregistered capitalised tags) and text. `@code { }` / `@functions { }` blocks, `@* *@` comments and directive lines (`@using`, `@inject`, …) are skipped for rendering but stay in the Code tab. If no registered component is found the slot carries an `Error`, rendered as a red panel instead of a silent code block. `razor:preview stretch` in the info string sets `PreviewSlot.Layout`.
116116

117117
---
118118

@@ -124,11 +124,11 @@ Placeholders plus a slot list keep everything at render time: no generated Razor
124124
- `ComponentSlot` → `<DynamicComponent>` with parameters from `SlotRenderer.BuildParameters`.
125125
- `PreviewSlot` → `<PreviewFrame Id="preview-N">`, where `SlotRenderer.RenderNodes` emits elements and components as real render-tree nodes, so wrappers keep their children under interactive re-renders.
126126

127-
Component child markup is rendered recursively by `SlotRenderer.FromMarkup` (dedented first, since Markdig treats 4-space indentation as a code block). Child tags named after a `RenderFragment` parameter (`<Icon>` → `Alert.Icon`) are routed into that slot.
127+
Component child markup is rendered recursively. Inline tags in prose take markdown bodies (`SlotRenderer.FromMarkup`, dedented first, since Markdig treats 4-space indentation as a code block). Inside a `razor:preview` and in `<ComponentPreview>` the body is Razor: `SlotRenderer.FromRazor` parses it with `PreviewParser` and emits real elements, so wrappers around nested components stay intact. Child tags named after a `RenderFragment` parameter (`<Icon>` → `Alert.Icon`) are routed into that slot.
128128

129129
**Parameter coercion.** Attribute values are strings. `BuildParameters` matches them to `[Parameter]` properties case-insensitively and coerces string, bool, char, numeric primitives and enums, accepting Razor forms: a leading `@`, `@( … )`, `Type.Member` enum values, `A | B` flags, numeric suffixes, `@null`. It never throws. Anything static markup can't set is skipped with a logged warning: directive attributes, delegate/`EventCallback` parameters, unsupported types, unparseable values, unknown attributes on components without a `CaptureUnmatchedValues` catch-all, and child content on components without a plain `RenderFragment ChildContent`.
130130

131-
**`PreviewFrame`** is the frame for every example: a toolbar with Preview | Code tabs, copy and a ⋯ menu (*Open in new tab* → the example's anchor; *Report a bug* / *Suggest something* → new-issue links built by `PreviewLinks` from `IssueTrackerUrl` or `GitHubRepo`). `ComponentPreview` and `DemoPreview` feed it a ready-made `Content` fragment and `Code` string instead of a `PreviewSlot`. `DemoPreview` reads its source from the first `{DemoSourceRoot}/**/X.razor` (shallowest path wins, cached, re-read when the file changes).
131+
**`PreviewFrame`** is the frame for every example. It carries `not-prose` (every `.shelldocs-prose` rule skips `.not-prose` subtrees) and a `Layout` of `center` or `stretch`. It has a toolbar with Preview | Code tabs, copy and a ⋯ menu (*Open in new tab* → the example's anchor; *Report a bug* / *Suggest something* → new-issue links built by `PreviewLinks` from `IssueTrackerUrl` or `GitHubRepo`). `ComponentPreview` and `DemoPreview` feed it a ready-made `Content` fragment and `Code` string instead of a `PreviewSlot`. `DemoPreview` reads its source from the first `{DemoSourceRoot}/**/X.razor` (shallowest path wins, cached, re-read when the file changes).
132132

133133
**`DocsPageState`** (scoped) is fed by `MarkdownContent` and recomputes current node, prev/next and breadcrumbs on navigation, so the layout chrome needs no per-page wiring.
134134

@@ -195,9 +195,11 @@ Chrome that must work without a Blazor runtime follows one contract:
195195
| Tabs, CodeGroup | `[data-tabs]`, `[data-tab-target]`, `[data-tab-panel]` | switch on click / arrows; `[data-tabs-sync]` groups switch together and persist in `localStorage` |
196196
| Mobile nav | page shell `[data-mobile-open]` | hamburger toggles; backdrop, Escape, link or navigation closes; page scroll locked on `<html>` |
197197
| TOC | `[data-toc-list][data-toc-ids]` | IntersectionObserver scroll-spy, re-attached on `enhancedload` |
198-
| Theme | `<html class="dark">` | inline head script applies the saved / system theme; re-applied after enhanced navigation |
198+
| Theme | `<html class="dark">`, `[data-theme-toggle]` | inline head script applies the saved / system theme; the toggle flips the class and CSS picks its icon; a `MutationObserver` saves any change (including another library's toggle) and pushes it to `ThemeState`; re-applied after enhanced navigation |
199199

200-
Still Blazor-only: search, the `ThemeToggle` button, the desktop sidebar-collapse button.
200+
Still Blazor-only: search, the desktop sidebar-collapse button.
201+
202+
`enhancedload` is a Blazor event (`Blazor.addEventListener`), not a DOM event; `shelldocsOnEnhancedLoad(fn)` attaches once `blazor.web.js` has run.
201203

202204
**Syntax highlighting.** Shiki (loaded by the app as `window.__shiki`) never replaces Blazor-owned nodes: the source `<pre>` gets `data-shiki="source"` (hidden by CSS) and the highlighted output goes into a JS-owned `[data-shiki-output]` sibling, re-rendered when the source text changes and removed when its source is gone.
203205

‎examples/ShellDocs.Preview/content/docs/components/component-preview.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Body content that becomes the Callout's ChildContent.
2323

2424
- `Component` — required. The registered tag name (`"Callout"`, `"Card"`, `"LinkCard"`, …). Resolved through the same registry as `razor:preview`, so anything `AddShellDocs` or your `RegisterComponent*` calls register works.
2525
- Any other attribute — forwarded to the target. Values are strings in markdown and are coerced to each parameter's type (`bool`, numbers, enums including `Type.Member` and `A | B` flags). Attributes that can't be set this way are skipped with a logged warning.
26-
- The tag body becomes the target's `ChildContent`.
26+
- `Layout` — `"center"` (default) or `"stretch"`, which lets block-level components fill the frame's width.
27+
- The tag body is parsed as Razor (HTML and registered components, no markdown) and becomes the target's `ChildContent`. Child tags named after one of the target's `RenderFragment` parameters fill that slot.
2728

2829
## Notes
2930

‎examples/ShellDocs.Preview/content/docs/installation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ shelldocs build
5555
- `--spa-fallback` copies `index.html` to `404.html`.
5656
- `--site-url https://docs.example.com` emits `sitemap.xml`, `robots.txt` and `og:` meta tags.
5757

58-
In the static output, navigation, sidebar sections, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents and code copy all work through `shelldocs.js`. Search, the theme toggle, the desktop sidebar-collapse button and stateful demo components still need a running Blazor app (Server or WebAssembly).
58+
In the static output, navigation, sidebar sections, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents, the theme toggle and code copy all work through `shelldocs.js`. Search, the desktop sidebar-collapse button and stateful demo components still need a running Blazor app (Server or WebAssembly).

‎examples/ShellDocs.Preview/content/docs/markdown-syntax.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,25 @@ A code fence with the info string `razor:preview` renders live in a preview fram
5050
```
5151

5252
Attribute values accept Razor forms such as `Variant="ButtonVariant.Destructive"`, `@true` and `[Flags]` values like `Bold | Italic`. Attributes a static preview can't evaluate (`OnClick="Handler"`, `@bind-*`, `@ref`) are skipped with a logged warning. For demos that need `@code`, see `<DemoPreview>` in the README.
53+
54+
### Child content is Razor
55+
56+
Inside a fence, component bodies are Razor markup, not markdown. Elements wrap nested components exactly as written, and nothing is wrapped in `<p>`:
57+
58+
```razor:preview
59+
<Card Title="Nested markup">
60+
<div style="display: flex; gap: 0.75rem; align-items: center;"><span>Inside a div:</span><Callout Variant="tip" Text="a nested component" /></div>
61+
</Card>
62+
```
63+
64+
Inline component tags in prose (outside a fence) still take markdown bodies.
65+
66+
### Layout
67+
68+
Examples are centred. Add `stretch` to the info string (`razor:preview stretch`) so block-level components such as charts, inputs and tables fill the frame's width. `<DemoPreview>` and `<ComponentPreview>` take `Layout="stretch"`.
69+
70+
```razor:preview stretch
71+
<Callout Variant="info" Text="This callout fills the frame instead of shrinking to its text." />
72+
```
73+
74+
Preview frames are marked `not-prose`, so the page's typography (paragraph margins, list padding, link underlines) never reaches the components inside. Add the class to any other element that should opt out.

‎examples/ShellDocs.Preview/content/docs/theming.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ Override any of these in a stylesheet loaded after `tokens.css` and every compon
2626

2727
## Dark mode
2828

29-
Dark mode is the `dark` class on `<html>`. The `ThemeToggle` in the header / sidebar footer saves the choice in `localStorage` and falls back to `prefers-color-scheme` on the first visit; an inline script in `App.razor` applies it before first paint. The toggle button itself needs a running Blazor app; on a static build the saved or system preference still applies.
29+
Dark mode is the `dark` class on `<html>`. The `ThemeToggle` in the header / sidebar footer saves the choice in `localStorage` and falls back to `prefers-color-scheme` on the first visit; an inline script in `App.razor` applies it before first paint. The toggle is handled by `shelldocs.js`, so it works on static builds too. Whatever flips the class, including a component library's own theme toggle, is saved the same way, so the choice sticks across pages.
Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,48 @@
11
@using ShellIcons.Icons
22
@inject IJSRuntime JS
33
@inject ThemeState Theme
4-
@implements IDisposable
4+
@implements IAsyncDisposable
55

6-
<button type="button" class="theme-toggle" @onclick="OnClick" aria-label="Toggle theme">
7-
@if (Theme.IsDark)
8-
{
9-
<Moon />
10-
}
11-
else
12-
{
13-
<Sun />
14-
}
6+
@* Clicks are handled by shelldocs.js and CSS picks the icon from <html class="dark">,
7+
so the toggle works without a Blazor runtime. *@
8+
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Toggle theme">
9+
<Sun Class="theme-icon-light" />
10+
<Moon Class="theme-icon-dark" />
1511
</button>
1612

1713
@code {
18-
protected override void OnInitialized() => Theme.OnChange += StateHasChanged;
14+
private DotNetObjectReference<ThemeToggle>? _ref;
15+
private IJSObjectReference? _subscription;
1916

17+
// Keeps ThemeState in step with the dark class, whoever flips it.
2018
protected override async Task OnAfterRenderAsync(bool firstRender)
2119
{
22-
if (!firstRender || Theme.IsInitialized) return;
20+
if (!firstRender) return;
2321
try
2422
{
25-
var current = await JS.InvokeAsync<string>("eval",
26-
"document.documentElement.classList.contains('dark') ? 'dark' : 'light'");
27-
Theme.Init(current == "dark");
23+
_ref = DotNetObjectReference.Create(this);
24+
_subscription = await JS.InvokeAsync<IJSObjectReference>("shelldocsTheme.subscribe", _ref);
25+
var dark = await JS.InvokeAsync<bool>("shelldocsTheme.isDark");
26+
if (Theme.IsInitialized) Theme.Set(dark);
27+
else Theme.Init(dark);
2828
}
2929
catch { }
3030
}
3131

32-
private async Task OnClick()
32+
[JSInvokable]
33+
public void ThemeChanged(bool isDark) => Theme.Set(isDark);
34+
35+
public async ValueTask DisposeAsync()
3336
{
34-
Theme.Toggle();
3537
try
3638
{
37-
await JS.InvokeVoidAsync("eval", Theme.IsDark
38-
? "document.documentElement.classList.add('dark'); try { localStorage.setItem('shelldocs-theme','dark'); } catch{}"
39-
: "document.documentElement.classList.remove('dark'); try { localStorage.setItem('shelldocs-theme','light'); } catch{}");
39+
if (_subscription is not null)
40+
{
41+
await _subscription.InvokeVoidAsync("dispose");
42+
await _subscription.DisposeAsync();
43+
}
4044
}
4145
catch { }
46+
_ref?.Dispose();
4247
}
43-
44-
public void Dispose() => Theme.OnChange -= StateHasChanged;
4548
}

‎src/ShellDocs.Components/Chrome/ThemeToggle.razor.css‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@
1515
.theme-toggle:hover { color: var(--foreground); background: var(--muted); }
1616
.theme-toggle:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
1717
.theme-toggle ::deep svg { width: 1rem; height: 1rem; }
18+
19+
/* Both icons render; the dark class picks one, so a theme flipped by any script shows here. */
20+
.theme-toggle ::deep .theme-icon-dark { display: none; }
21+
:root.dark .theme-toggle ::deep .theme-icon-light { display: none; }
22+
:root.dark .theme-toggle ::deep .theme-icon-dark { display: inline; }

0 commit comments

Comments
 (0)