From 7f75c8b63ac8f8ded539791019afe865c6477c40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:36:01 +0000 Subject: [PATCH 1/2] Initial plan From 0ab82ca64e93437a619399e1a22c872241f8aa13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:51:35 +0000 Subject: [PATCH 2/2] fix: generate individual story pages so DocFX can differentiate them in the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocFX was treating all story TOC items as the same page because they all pointed to storybook.html with different ?story= query params, which DocFX ignores for active-state matching. Fix: during pre-processing, generate a stub .md file for each story in a stories/ subdirectory. DocFX builds these into unique HTML pages so each story has its own URL. DocFX's native active-state detection then works correctly — only the current story is highlighted in the sidebar. Post-processing now injects a simplified iframe (no JS workarounds needed) for story-specific pages, and retains backward-compatible ?story= URL handling on the main storybook page. Co-authored-by: einari <134365+einari@users.noreply.github.com> --- Source/postprocess-storybooks.ts | 288 +++++++++---------------------- Source/preprocess-storybooks.ts | 71 +++++++- 2 files changed, 140 insertions(+), 219 deletions(-) diff --git a/Source/postprocess-storybooks.ts b/Source/postprocess-storybooks.ts index cc3357d..ea26e39 100644 --- a/Source/postprocess-storybooks.ts +++ b/Source/postprocess-storybooks.ts @@ -11,6 +11,7 @@ const __dirname = dirname(__filename); interface StorybookConfig { path: string; + story?: string; } interface FrontMatter { @@ -60,7 +61,7 @@ function parseStorybookIndex(storybookPath: string): StorybookIndex | null { } function findFirstStoryInHierarchy(item: TocItem): string | null { - if (item.href && item.href.includes('?story=')) { + if (item.href && item.href.startsWith('stories/')) { return item.href; } if (item.items) { @@ -105,11 +106,11 @@ function buildTocFromStorybook(storybookIndex: StorybookIndex, storybookPageHref if (i === parts.length - 1) { // Set href BEFORE items to ensure correct YAML property order if (titleStories.length > 0) { - item.href = `${storybookPageHref}?story=${encodeURIComponent(titleStories[0].id)}`; + item.href = `stories/${titleStories[0].id}.md`; } item.items = titleStories.map(story => ({ name: story.name, - href: `${storybookPageHref}?story=${encodeURIComponent(story.id)}` + href: `stories/${story.id}.md` })); } @@ -183,8 +184,8 @@ function buildTocFromStorybook(storybookIndex: StorybookIndex, storybookPageHref function findFirstStoryHref(items: TocItem[]): string | null { for (const item of items) { - // If this item has an href (it's a leaf/story), return it - if (item.href && item.href.includes('?story=')) { + // If this item has an href to a story page, return it + if (item.href && item.href.startsWith('stories/')) { return item.href; } // Otherwise, recursively search children @@ -379,8 +380,9 @@ async function processMarkdownFile(mdFilePath: string) { const storybookRelativePath = path.relative(htmlDir, storybookSitePath).replace(/\\/g, '/'); - // Inject the iframe into the HTML - injectStorybookIframe(htmlPath, storybookRelativePath); + // Inject the iframe into the HTML, passing the specific story ID if set + const storyId = frontMatter.storybook.story; + injectStorybookIframe(htmlPath, storybookRelativePath, storyId); } function getSubmoduleName(markdownFile: string): string | null { @@ -420,83 +422,106 @@ function resolveStorybookPath(storybookPath: string, markdownFile: string): stri } } -function injectStorybookIframe(htmlPath: string, storybookRelativePath: string) { +function injectStorybookIframe(htmlPath: string, storybookRelativePath: string, storyId?: string) { let html = fs.readFileSync(htmlPath, 'utf-8'); - // Use index.html with nav=false to get full Storybook UI (toolbar + addon panels) - // but without the sidebar navigation (which is handled by DocFX TOC instead) - const iframeSrc = `${storybookRelativePath}/index.html?nav=false&panel=right&addonPanel=storybook/docs`; + // For story-specific pages, navigate directly to the story. + // For the main storybook page, show the default view (navigation handled by URL params). + const iframeSrc = storyId + ? `${storybookRelativePath}/index.html?nav=false&panel=right&addonPanel=storybook/docs&path=/story/${encodeURIComponent(storyId)}` + : `${storybookRelativePath}/index.html?nav=false&panel=right&addonPanel=storybook/docs`; - // Create the iframe HTML with theme synchronization and story navigation script - const iframeHtml = ` + // Theme-sync script shared by all storybook pages + const themeSyncScript = ` +(function() { + const iframe = document.getElementById('storybook-iframe'); + + function syncTheme() { + const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark'; + const theme = isDark ? 'dark' : 'light'; + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage({ type: 'STORYBOOK_THEME_CHANGE', theme: theme }, '*'); + } + } + + iframe.addEventListener('load', function() { + setTimeout(syncTheme, 100); + setTimeout(function() { + try { + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (iframeDoc) { + const codeTab = iframeDoc.getElementById('tabbutton-storybook-docs'); + if (codeTab) { codeTab.click(); } + } + } catch (e) {} + }, 500); + }); + + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') { + syncTheme(); + } + }); + }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'] }); + syncTheme(); +})();`; + + let iframeHtml: string; + + if (storyId) { + // Story-specific page: iframe src is hardcoded to this story. + // DocFX generates correct TOC active state and breadcrumb because each story + // has its own unique URL — no JavaScript workarounds needed. + iframeHtml = ` +