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 = ` +
+ +
+`; + } else { + // Main storybook page: navigate based on ?story= URL parameter for + // backward-compatibility with old links. + iframeHtml = `
`; + } // Replace the article content with the iframe // Find the article tag and replace its content diff --git a/Source/preprocess-storybooks.ts b/Source/preprocess-storybooks.ts index 6370989..0a6ebf3 100644 --- a/Source/preprocess-storybooks.ts +++ b/Source/preprocess-storybooks.ts @@ -10,6 +10,7 @@ const __dirname = dirname(__filename); interface StorybookConfig { path: string; + story?: string; } interface FrontMatter { @@ -58,7 +59,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) { @@ -70,7 +71,7 @@ function findFirstStoryInHierarchy(item: TocItem): string | null { return null; } -function buildTocFromStorybook(storybookIndex: StorybookIndex, storybookPageHref: string): TocItem[] { +function buildTocFromStorybook(storybookIndex: StorybookIndex): TocItem[] { const stories = Object.values(storybookIndex.entries).filter(entry => entry.type === 'story'); // Build a hierarchical structure from story titles @@ -101,13 +102,13 @@ function buildTocFromStorybook(storybookIndex: StorybookIndex, storybookPageHref // If this is the last part, add story links as children if (i === parts.length - 1) { - // Set href to first story + // Set href to first story's individual page 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` })); } @@ -202,7 +203,15 @@ function updateTocWithStorybook(tocPath: string, storybookPageName: string, stor return; } - // Add the storybook items as children + // Update the storybook root item to point to first story and add children + let firstStoryHref: string | null = null; + for (const item of storybookItems) { + firstStoryHref = findFirstStoryInHierarchy(item); + if (firstStoryHref) break; + } + if (firstStoryHref) { + toc[storybookIndex].href = firstStoryHref; + } toc[storybookIndex].items = storybookItems; // Write back the TOC @@ -278,6 +287,42 @@ async function main() { console.log('Pre-processing complete!'); } +function generateStoryFiles(storybookIndex: StorybookIndex, mdFilePath: string, storybookConfig: StorybookConfig): void { + const mdDir = path.dirname(mdFilePath); + const storiesDir = path.join(mdDir, 'stories'); + + // Clean up previously generated story files + if (fs.existsSync(storiesDir)) { + const existingFiles = fs.readdirSync(storiesDir).filter(f => f.endsWith('.md')); + existingFiles.forEach(f => fs.unlinkSync(path.join(storiesDir, f))); + } else { + fs.mkdirSync(storiesDir, { recursive: true }); + } + + const stories = Object.values(storybookIndex.entries).filter(e => e.type === 'story'); + for (const story of stories) { + const frontMatterData = { + title: `${story.title} - ${story.name}`, + storybook: { + path: storybookConfig.path, + story: story.id + } + }; + const content = `---\n${yaml.dump(frontMatterData, { lineWidth: -1 })}---\n`; + fs.writeFileSync(path.join(storiesDir, `${story.id}.md`), content, 'utf-8'); + } + + // Write a .gitignore so the generated story files are not tracked by git. + // The stories/ directory is exclusively for generated stubs — all .md files here + // are created and cleaned up by this script on every build. + const gitignorePath = path.join(storiesDir, '.gitignore'); + if (!fs.existsSync(gitignorePath)) { + fs.writeFileSync(gitignorePath, '*.md\n', 'utf-8'); + } + + console.log(`Generated ${stories.length} story file(s) in ${storiesDir}`); +} + async function processMarkdownFile(mdFilePath: string) { const content = fs.readFileSync(mdFilePath, 'utf-8'); @@ -298,6 +343,12 @@ async function processMarkdownFile(mdFilePath: string) { return; } + // Skip individual story pages — these are generated by this script and should + // not trigger recursive story-file generation. + if (frontMatter.storybook.story) { + return; + } + console.log(`Found Storybook page: ${mdFilePath}`); // Calculate the relative path from the HTML file to the storybook build @@ -311,9 +362,11 @@ async function processMarkdownFile(mdFilePath: string) { const mdDir = path.dirname(mdFilePath); const tocPath = path.join(mdDir, 'toc.yml'); - // Generate story hierarchy for TOC - const storybookPageHref = path.basename(mdFilePath); - const storybookItems = buildTocFromStorybook(storybookIndex, storybookPageHref); + // Generate individual story stub files + generateStoryFiles(storybookIndex, mdFilePath, frontMatter.storybook); + + // Generate story hierarchy for TOC (using individual story pages) + const storybookItems = buildTocFromStorybook(storybookIndex); // Update the TOC with story hierarchy updateTocWithStorybook(tocPath, 'Storybook', storybookItems);