Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht

## [Unreleased]

## [0.1.16-alpha] — 2026-10-14

Fixes `<IframePreview>` showing the previous page's preview after navigating within a site.

### Fixed

- **`<IframePreview>` after in-site navigation.** When the next page has an iframe preview in the same place, Blazor reuses the element and only changes its `data-src`. `shelldocs.js` kept treating it as loaded, so the frame went on showing the previous page's preview and the Run button stayed hidden. It now watches `data-src` and `data-lazy`, unloads a host whose values change, and sets it up again: the Run button comes back for `Lazy="click"`, and `visible` / `none` previews load the new page. Sites that worked around it with their own `MutationObserver` can drop it.

## [0.1.15-alpha] — 2026-10-11

Search now works in static exports, the last piece of chrome that needed a running Blazor app.
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<!-- Package metadata (applies to any project with IsPackable=true) -->
<PropertyGroup>
<Version>0.1.15-alpha</Version>
<Version>0.1.16-alpha</Version>
<Authors>ShellUI</Authors>
<Company>ShellUI</Company>
<Copyright>Copyright © 2026 ShellUI</Copyright>
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Chrome that must work without a Blazor runtime follows one contract:
| Package / version selectors, preview ⋯ menu | `.pkg` / `.ver` / `.preview-menu` `[data-open]` | open on trigger; close on outside click, Escape or option |
| Preview tabs | `.preview-frame[data-preview-tab]` | Preview / Code; arrow keys; copy reads the source |
| Tabs, CodeGroup, preview code tabs, page switch | `[data-tabs]`, `[data-tab-target]`, `[data-tab-panel]` | switch on click / arrows; `[data-tabs-sync]` groups switch together and persist in `localStorage`; a `[data-tabs-missing]` group without the chosen value shows its `[data-tab-missing]` note; with nothing saved yet, groups follow a `[data-tabs-switch]` for their key |
| Iframe previews | `[data-iframe-preview][data-src][data-lazy]` | sets the iframe's `src` (plus `?theme=`) when visible (IntersectionObserver), on `[data-iframe-run]` or at once; posts `{ type: "shelldocs-theme", theme }` to loaded frames on theme change |
| Iframe previews | `[data-iframe-preview][data-src][data-lazy]` | sets the iframe's `src` (plus `?theme=`) when visible (IntersectionObserver), on `[data-iframe-run]` or at once; posts `{ type: "shelldocs-theme", theme }` to loaded frames on theme change; a host Blazor reuses for another page (`data-src` or `data-lazy` changed) is unloaded and set up again |
| Mobile nav | page shell `[data-mobile-open]` | hamburger toggles; backdrop, Escape, link or navigation closes; page scroll locked on `<html>` |
| Sidebar collapse | page shell `[data-sidebar-collapsed]`, `[data-sidebar-collapse-toggle]` | toggles collapse; remembered in `localStorage` and restored as soon as `shelldocs.js` runs, so the sidebar doesn't flash open |
| TOC | `[data-toc-list][data-toc-ids]` | IntersectionObserver scroll-spy, re-attached on `enhancedload` |
Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Ships to `ShellDocs.Components` + `ShellDocs.Templates` + `ShellDocs.CLI` + `She
**Authoring fix (`ShellDocs.Markdown`)**
- `SlotExtractor.ReplaceComponentTags` no longer `.Trim()`s the raw child content of inline component tags. The Trim was stripping the first line's indent and defeating `SlotRenderer.Dedent` — Markdig then interpreted the remaining 4-space-indented lines as an indented code block. Symptom was the same "literal `<pre>` around placeholder divs" bug that had already been fixed for `razor:preview` fences; the inline-tag code path was still hitting it.

### ✅ Hardening releases (`0.1.3`–`0.1.15-alpha`) — shipped
### ✅ Hardening releases (`0.1.3`–`0.1.16-alpha`) — shipped
Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHANGELOG):

- `feat/build-static-prerender` (0.1.5) — `shelldocs build` prerenders every URL into a static site.
Expand All @@ -222,6 +222,7 @@ Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHA
- `feat/preview-code-variants` (0.1.13) — code tabs on previews with sync and a missing-tab note, page switch (`AddSyncSwitch`), captions, `<IframePreview>`, XAML highlighting; one-line component bodies stay inline.
- `fix/not-found-and-sidebar-collapse` (0.1.14) — `<DocsNotFound>` answers 404, `build` fails on not-found pages and writes `404.html`, desktop sidebar collapse in `shelldocs.js`.
- `feat/static-search` (0.1.15) — search runs in `shelldocs.js` against `/_shelldocs/search.json`, which `build` copies into the export, so static sites have search.
- `fix/iframe-preview-navigation` (0.1.16) — an `<IframePreview>` reused for the next page after in-site navigation is unloaded and set up again.
- `fix/preview-fidelity`, `feat/generics-redirects-page-title` (0.1.10) — `not-prose` previews, Razor child content, stretch layout, generic components in markdown, redirects (middleware + static pages), `RenderPageTitle`, theme sync and a static-host `ThemeToggle`.

### `feat/animation-polish`
Expand Down
25 changes: 24 additions & 1 deletion src/ShellDocs.Components/wwwroot/shelldocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,36 @@ window.shelldocsIframes = (function () {
if (scope.matches && scope.matches('[data-iframe-preview]')) hosts.unshift(scope);
hosts.forEach(function (host) {
if (host.__shelldocsIframe) return;
host.__shelldocsIframe = true;
host.__shelldocsIframe = { src: host.getAttribute('data-src'), lazy: host.getAttribute('data-lazy') };
var lazy = host.getAttribute('data-lazy');
if (lazy === 'none') load(host);
else if (lazy !== 'click') watch(host);
});
}

/* Navigating within the site can reuse a host for the next page's preview and
change only its data-src (or data-lazy), which would leave the previous page's
frame loaded and its Run button hidden. Unload it and set the host up again. */
function reset(host) {
var state = host.__shelldocsIframe;
if (!state) return;
if (state.src === host.getAttribute('data-src') && state.lazy === host.getAttribute('data-lazy')) return;
if (observer) observer.unobserve(host);
host.removeAttribute('data-loaded');
var frame = host.querySelector('iframe');
if (frame && frame.hasAttribute('src')) frame.src = 'about:blank';
host.__shelldocsIframe = null;
init(host);
}

if (window.MutationObserver) {
new MutationObserver(function (records) {
records.forEach(function (r) {
if (r.target.matches && r.target.matches('[data-iframe-preview]')) reset(r.target);
});
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-src', 'data-lazy'], subtree: true });
}

function postTheme(dark) {
var message = { type: 'shelldocs-theme', theme: dark ? 'dark' : 'light' };
document.querySelectorAll('[data-iframe-preview][data-loaded="true"] iframe').forEach(function (frame) {
Expand Down
10 changes: 10 additions & 0 deletions tests/ShellDocs.Tests/PreviewCodeVariantsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ public void ShelldocsJs_ShowsTheMissingNote_AndLoadsIframesLazily()
Assert.Matches(new Regex("IntersectionObserver"), js);
}

[Fact]
public void ShelldocsJs_ResetsAnIframeHostReusedForAnotherPage()
{
var js = ReadAsset("shelldocs.js");

Assert.Contains("attributeFilter: ['data-src', 'data-lazy']", js);
Assert.Contains("host.removeAttribute('data-loaded')", js);
Assert.Contains("frame.src = 'about:blank'", js);
}

private static string ReadAsset(string name)
{
var testDir = Path.GetDirectoryName(typeof(PreviewCodeVariantsTests).Assembly.Location)!;
Expand Down
Loading