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);