diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bee416..49084af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` showing the previous page's preview after navigating within a site. + +### Fixed + +- **`` 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. diff --git a/Directory.Build.props b/Directory.Build.props index 29e69b7..112348b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,7 +17,7 @@ - 0.1.15-alpha + 0.1.16-alpha ShellUI ShellUI Copyright © 2026 ShellUI diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5bb69b0..b9587c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 `` | | 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` | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7aba29e..9816644 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 `
` 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.
@@ -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, ``, XAML highlighting; one-line component bodies stay inline.
 - `fix/not-found-and-sidebar-collapse` (0.1.14) — `` 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 `` 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`
diff --git a/src/ShellDocs.Components/wwwroot/shelldocs.js b/src/ShellDocs.Components/wwwroot/shelldocs.js
index 9409544..78ec4d3 100644
--- a/src/ShellDocs.Components/wwwroot/shelldocs.js
+++ b/src/ShellDocs.Components/wwwroot/shelldocs.js
@@ -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) {
diff --git a/tests/ShellDocs.Tests/PreviewCodeVariantsTests.cs b/tests/ShellDocs.Tests/PreviewCodeVariantsTests.cs
index c682765..5631e8f 100644
--- a/tests/ShellDocs.Tests/PreviewCodeVariantsTests.cs
+++ b/tests/ShellDocs.Tests/PreviewCodeVariantsTests.cs
@@ -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)!;