From 646c3df65c3737b05925b212008594b89cc3b773 Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Tue, 10 Feb 2026 11:12:13 +0800 Subject: [PATCH 1/3] . --- app/components/Storybook/FileTree.vue | 129 ++++++ app/components/Storybook/MobileTreeDrawer.vue | 82 ++++ app/components/VersionSelector.vue | 10 +- app/composables/useStoryTreeState.ts | 36 ++ app/pages/package-stories/[...path].vue | 428 ++++++++++++++++++ app/pages/package/[[org]]/[name].vue | 23 +- app/utils/storybook-tree.ts | 144 ++++++ i18n/locales/en.json | 19 + i18n/schema.json | 57 +++ lunaria/files/en-GB.json | 19 + lunaria/files/en-US.json | 19 + shared/types/index.ts | 1 + shared/types/storybook.ts | 81 ++++ 13 files changed, 1046 insertions(+), 2 deletions(-) create mode 100644 app/components/Storybook/FileTree.vue create mode 100644 app/components/Storybook/MobileTreeDrawer.vue create mode 100644 app/composables/useStoryTreeState.ts create mode 100644 app/pages/package-stories/[...path].vue create mode 100644 app/utils/storybook-tree.ts create mode 100644 shared/types/storybook.ts diff --git a/app/components/Storybook/FileTree.vue b/app/components/Storybook/FileTree.vue new file mode 100644 index 000000000..8e4c34fc6 --- /dev/null +++ b/app/components/Storybook/FileTree.vue @@ -0,0 +1,129 @@ + + + diff --git a/app/components/Storybook/MobileTreeDrawer.vue b/app/components/Storybook/MobileTreeDrawer.vue new file mode 100644 index 000000000..89097439f --- /dev/null +++ b/app/components/Storybook/MobileTreeDrawer.vue @@ -0,0 +1,82 @@ + + + diff --git a/app/components/VersionSelector.vue b/app/components/VersionSelector.vue index af9d61659..346843f57 100644 --- a/app/components/VersionSelector.vue +++ b/app/components/VersionSelector.vue @@ -74,7 +74,15 @@ const versionToTags = computed(() => buildVersionToTagsMap(props.distTags)) /** Get URL for a specific version */ function getVersionUrl(version: string): string { - return props.urlPattern.replace('{version}', version) + let url = props.urlPattern.replace('{version}', version) + // Replace storyid placeholder if it exists + if (url.includes('{storyid}')) { + // Get current storyid from route query + const route = useRoute() + const currentStoryId = route.query.storyid as string + url = url.replace('{storyid}', currentStoryId || '') + } + return url } /** Safe semver comparison with fallback */ diff --git a/app/composables/useStoryTreeState.ts b/app/composables/useStoryTreeState.ts new file mode 100644 index 000000000..0b9f52186 --- /dev/null +++ b/app/composables/useStoryTreeState.ts @@ -0,0 +1,36 @@ +import { computed } from 'vue' +import { useState } from '#app' + +export function useStoryTreeState(baseUrl: string) { + const stateKey = computed(() => `npmx-story-tree${baseUrl}`) + + const expanded = useState>(stateKey.value, () => new Set()) + + function toggleDir(path: string) { + if (expanded.value.has(path)) { + expanded.value.delete(path) + } else { + expanded.value.add(path) + } + } + + function isExpanded(path: string) { + return expanded.value.has(path) + } + + function autoExpandAncestors(path: string) { + if (!path) return + const parts = path.split('/').filter(Boolean) + let prefix = '' + for (const part of parts) { + prefix = prefix ? `${prefix}/${part}` : part + expanded.value.add(prefix) + } + } + + return { + toggleDir, + isExpanded, + autoExpandAncestors, + } +} diff --git a/app/pages/package-stories/[...path].vue b/app/pages/package-stories/[...path].vue new file mode 100644 index 000000000..401093739 --- /dev/null +++ b/app/pages/package-stories/[...path].vue @@ -0,0 +1,428 @@ + + + diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 913c54af3..76f52bfbb 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -54,6 +54,12 @@ if (import.meta.server) { assertValidPackageName(packageName.value) } +const { data: packageJson } = useLazyFetch<{ storybook?: { title: string; url: string } }>(() => { + const version = requestedVersion.value ?? 'latest' + const url = `https://cdn.jsdelivr.net/npm/${packageName.value}@${version}/package.json` + return url +}) + // Fetch README for specific version if requested, otherwise latest const { data: readmeData } = useLazyFetch( () => { @@ -104,7 +110,9 @@ const { immediate: false, }, ) -onMounted(() => fetchInstallSize()) +onMounted(() => { + fetchInstallSize() +}) const { data: skillsData } = useLazyFetch( () => { @@ -1157,6 +1165,19 @@ onKeyStroke( :links="readmeData.playgroundLinks" /> + + + storybook + sb internal + + diff --git a/app/utils/storybook-tree.ts b/app/utils/storybook-tree.ts new file mode 100644 index 000000000..fb0ee7636 --- /dev/null +++ b/app/utils/storybook-tree.ts @@ -0,0 +1,144 @@ +import type { StorybookEntry, StorybookFileTree } from '#shared/types' + +/** + * Transform flat Storybook entries into hierarchical tree structure + */ +export function transformStorybookEntries( + entries: Record, +): StorybookFileTree[] { + const tree: StorybookFileTree[] = [] + const dirMap = new Map() + + // Sort entries by title for consistent ordering + const sortedEntries = Object.values(entries).sort((a, b) => + (a.title || '').localeCompare(b.title || ''), + ) + + for (const entry of sortedEntries) { + // Parse title into path parts + // "Example/Button/Primary" -> ["Example", "Button", "Primary"] + if (!entry.title) continue + const parts = entry.title.split('/') + const storyName = parts.pop()! // Last part is the story name + const storyPath = parts.join('/') || '' || '' + + // Create directories as needed + let currentPath = '' + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (!part) continue + currentPath = currentPath ? `${currentPath}/${part}` : part + + if (!dirMap.has(currentPath)) { + const dirNode: StorybookFileTree = { + name: part, + path: currentPath, + type: 'directory', + children: [], + } + dirMap.set(currentPath, dirNode) + + // Add to appropriate parent + if (i === 0) { + tree.push(dirNode) + } else { + const parentPath = parts.slice(0, i).join('/') || '' + const parent = dirMap.get(parentPath) + if (parent) { + parent.children!.push(dirNode) + } + } + } + } + + // Create story node + const storyNode: StorybookFileTree = { + name: storyName, + path: entry.title, + type: 'story', + storyId: entry.id, + story: entry, + } + + // Add story to its directory or root + if (storyPath) { + const parentDir = dirMap.get(storyPath) + if (parentDir) { + parentDir.children!.push(storyNode) + } + } else { + // Root level story + tree.push(storyNode) + } + } + + return tree +} + +/** + * Find a story by its ID in the tree + */ +export function findStoryById( + tree: StorybookFileTree[], + storyId: string, +): StorybookFileTree | null { + for (const node of tree) { + if (node.type === 'story' && node.storyId === storyId) { + return node + } + if (node.type === 'directory' && node.children) { + const found = findStoryById(node.children, storyId) + if (found) return found + } + } + return null +} + +/** + * Get the first story from the tree (for default selection) + */ +export function getFirstStory(tree: StorybookFileTree[]): StorybookFileTree | null { + for (const node of tree) { + if (node.type === 'story') { + return node + } + if (node.type === 'directory' && node.children) { + const found = getFirstStory(node.children) + if (found) return found + } + } + return null +} + +/** + * Get the first story from a specific directory + */ +export function getFirstStoryInDirectory(directory: StorybookFileTree): StorybookFileTree | null { + if (directory.type !== 'directory' || !directory.children) { + return null + } + return getFirstStory(directory.children) +} + +/** + * Build breadcrumb path segments for a story + */ +export function getStoryBreadcrumbs( + story: StorybookFileTree, +): { name: string; path: string; storyId?: string }[] { + const parts = story.path.split('/') + const result: { name: string; path: string; storyId?: string }[] = [] + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + if (!part) continue + const path = parts.slice(0, i + 1).join('/') + result.push({ + name: part, + path, + storyId: i === parts.length - 1 ? story.storyId || undefined : undefined, + }) + } + + return result +} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index f0f28b1c2..72f7e4955 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -631,6 +631,25 @@ }, "file_path": "File path" }, + "stories": { + "stories_label": "Stories", + "story_path": "Story path", + "root": "Stories", + "version_required": "Version is required to browse stories", + "go_to_package": "Go to package", + "loading_stories": "Loading Storybook stories...", + "no_storybook_found": "No Storybook found", + "check_package_json": "Check if this package has Storybook configuration in package.json", + "back_to_package": "Back to package", + "failed_to_load_stories": "Failed to load Storybook stories", + "storybook_unavailable": "The Storybook instance may be unavailable or misconfigured", + "open_storybook": "Open Storybook", + "story": "Story", + "open_in_storybook": "Open in Storybook", + "select_story": "Select a story from the tree to view it here", + "toggle_tree": "Toggle story tree", + "close_tree": "Close story tree" + }, "badges": { "provenance": { "verified": "verified", diff --git a/i18n/schema.json b/i18n/schema.json index 1b2ffaa8f..493412250 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -1897,6 +1897,63 @@ }, "additionalProperties": false }, + "stories": { + "type": "object", + "properties": { + "stories_label": { + "type": "string" + }, + "story_path": { + "type": "string" + }, + "root": { + "type": "string" + }, + "version_required": { + "type": "string" + }, + "go_to_package": { + "type": "string" + }, + "loading_stories": { + "type": "string" + }, + "no_storybook_found": { + "type": "string" + }, + "check_package_json": { + "type": "string" + }, + "back_to_package": { + "type": "string" + }, + "failed_to_load_stories": { + "type": "string" + }, + "storybook_unavailable": { + "type": "string" + }, + "open_storybook": { + "type": "string" + }, + "story": { + "type": "string" + }, + "open_in_storybook": { + "type": "string" + }, + "select_story": { + "type": "string" + }, + "toggle_tree": { + "type": "string" + }, + "close_tree": { + "type": "string" + } + }, + "additionalProperties": false + }, "badges": { "type": "object", "properties": { diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index 0655549a9..25695dada 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -630,6 +630,25 @@ }, "file_path": "File path" }, + "stories": { + "stories_label": "Stories", + "story_path": "Story path", + "root": "Stories", + "version_required": "Version is required to browse stories", + "go_to_package": "Go to package", + "loading_stories": "Loading Storybook stories...", + "no_storybook_found": "No Storybook found", + "check_package_json": "Check if this package has Storybook configuration in package.json", + "back_to_package": "Back to package", + "failed_to_load_stories": "Failed to load Storybook stories", + "storybook_unavailable": "The Storybook instance may be unavailable or misconfigured", + "open_storybook": "Open Storybook", + "story": "Story", + "open_in_storybook": "Open in Storybook", + "select_story": "Select a story from the tree to view it here", + "toggle_tree": "Toggle story tree", + "close_tree": "Close story tree" + }, "badges": { "provenance": { "verified": "verified", diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index 7fb0060ef..d9282873d 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -630,6 +630,25 @@ }, "file_path": "File path" }, + "stories": { + "stories_label": "Stories", + "story_path": "Story path", + "root": "Stories", + "version_required": "Version is required to browse stories", + "go_to_package": "Go to package", + "loading_stories": "Loading Storybook stories...", + "no_storybook_found": "No Storybook found", + "check_package_json": "Check if this package has Storybook configuration in package.json", + "back_to_package": "Back to package", + "failed_to_load_stories": "Failed to load Storybook stories", + "storybook_unavailable": "The Storybook instance may be unavailable or misconfigured", + "open_storybook": "Open Storybook", + "story": "Story", + "open_in_storybook": "Open in Storybook", + "select_story": "Select a story from the tree to view it here", + "toggle_tree": "Toggle story tree", + "close_tree": "Close story tree" + }, "badges": { "provenance": { "verified": "verified", diff --git a/shared/types/index.ts b/shared/types/index.ts index 96f55a32b..c97553f28 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -8,3 +8,4 @@ export * from './deno-doc' export * from './i18n-status' export * from './comparison' export * from './skills' +export * from './storybook' diff --git a/shared/types/storybook.ts b/shared/types/storybook.ts new file mode 100644 index 000000000..c026def04 --- /dev/null +++ b/shared/types/storybook.ts @@ -0,0 +1,81 @@ +/** + * Storybook API Types + * Types for Storybook index.json responses and story navigation + */ + +/** + * Individual story entry from Storybook's index.json + */ +export interface StorybookEntry { + /** Unique identifier for the story (e.g., "example-button--primary") */ + id: string + /** Display name of the story */ + name: string + /** Full title/path (e.g., "Example/Button/Primary") */ + title: string + /** Import path for the story file */ + importPath?: string + /** Story tags (e.g., ["autodocs", "play-fn"]) */ + tags?: string[] + /** Component kind/group */ + kind?: string + /** Story name (alternative to 'name') */ + story?: string + /** Story parameters and configuration */ + parameters?: Record + /** Story metadata */ + type?: 'story' | 'docs' | 'component' +} + +/** + * Storybook index.json response structure + */ +export interface StorybookIndexResponse { + /** Storybook version */ + v?: string + /** All story entries keyed by their ID */ + entries: Record + /** Global metadata about the Storybook instance */ + metadata?: { + /** Storybook version info */ + storybook?: { + version?: string + configDir?: string + } + /** Package information */ + packageJson?: { + name?: string + version?: string + dependencies?: Record + } + } +} + +/** + * Tree node for Storybook story navigation + * Similar structure to PackageFileTree but for stories + */ +export interface StorybookFileTree { + /** Story or group name */ + name: string + /** Full path from root */ + path: string + /** Node type */ + type: 'story' | 'directory' + /** Story ID (only for stories) */ + storyId?: string + /** Story entry data (only for stories) */ + story?: StorybookEntry + /** Child nodes (only for directories) */ + children?: StorybookFileTree[] +} + +/** + * Response for Storybook tree API + */ +export interface StorybookTreeResponse { + package: string + version: string + storybookUrl: string + tree: StorybookFileTree[] +} From 97a17d3d0255df4c19d594cfbdf00351fb0f9c8c Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Tue, 10 Feb 2026 20:04:34 +0800 Subject: [PATCH 2/3] . --- app/components/Package/Playgrounds.vue | 2 ++ app/components/VersionSelector.vue | 10 +----- app/pages/package-stories/[...path].vue | 6 ++-- app/pages/package/[[org]]/[name].vue | 45 ++++++++++++++----------- app/utils/storybook-tree.ts | 10 ++---- lunaria/files/en-GB.json | 19 ----------- uno.config.ts | 1 + 7 files changed, 35 insertions(+), 58 deletions(-) diff --git a/app/components/Package/Playgrounds.vue b/app/components/Package/Playgrounds.vue index 83a6cdefb..3a6e4d18b 100644 --- a/app/components/Package/Playgrounds.vue +++ b/app/components/Package/Playgrounds.vue @@ -17,6 +17,7 @@ const providerIcons: Record = { 'nuxt-new': 'i-simple-icons:nuxtdotjs', 'vite-new': 'i-simple-icons:vite', 'jsfiddle': 'i-carbon:code', + 'storybook': 'i-simple-icons:storybook', } // Map provider id to color class @@ -30,6 +31,7 @@ const providerColors: Record = { 'nuxt-new': 'text-provider-nuxt', 'vite-new': 'text-provider-vite', 'jsfiddle': 'text-provider-jsfiddle', + 'storybook': 'text-provider-storybook', } function getIcon(provider: string): string { diff --git a/app/components/VersionSelector.vue b/app/components/VersionSelector.vue index 346843f57..af9d61659 100644 --- a/app/components/VersionSelector.vue +++ b/app/components/VersionSelector.vue @@ -74,15 +74,7 @@ const versionToTags = computed(() => buildVersionToTagsMap(props.distTags)) /** Get URL for a specific version */ function getVersionUrl(version: string): string { - let url = props.urlPattern.replace('{version}', version) - // Replace storyid placeholder if it exists - if (url.includes('{storyid}')) { - // Get current storyid from route query - const route = useRoute() - const currentStoryId = route.query.storyid as string - url = url.replace('{storyid}', currentStoryId || '') - } - return url + return props.urlPattern.replace('{version}', version) } /** Safe semver comparison with fallback */ diff --git a/app/pages/package-stories/[...path].vue b/app/pages/package-stories/[...path].vue index 401093739..442dfb33d 100644 --- a/app/pages/package-stories/[...path].vue +++ b/app/pages/package-stories/[...path].vue @@ -52,8 +52,8 @@ const { data: pkg } = usePackage(packageName) // URL pattern for version selector - maintain current story if available const versionUrlPattern = computed(() => { const base = `/package-stories/${packageName.value}/v/{version}` - // Use placeholder for storyid that will be handled by version switch watcher - return currentStoryId.value ? `${base}?storyid={storyid}` : base + // Directly include the current storyid if available + return currentStoryId.value ? `${base}?storyid=${currentStoryId.value}` : base }) // Fetch package.json to get Storybook URL @@ -383,7 +383,7 @@ defineOgImageComponent('Default', {
( { default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) }, ) +const playgroundLinks = computed(() => [ + ...readmeData.value.playgroundLinks, + ...(packageJson.value?.storybook + ? [ + { + url: packageJson.value.storybook.url, + provider: 'storybook', + providerName: 'Storybook', + label: 'Storybook', + }, + ] + : []), +]) + //copy README file as Markdown const { copied: copiedReadme, copy: copyReadme } = useClipboard({ source: () => readmeData.value?.md ?? '', @@ -110,9 +124,7 @@ const { immediate: false, }, ) -onMounted(() => { - fetchInstallSize() -}) +onMounted(() => fetchInstallSize()) const { data: skillsData } = useLazyFetch( () => { @@ -639,6 +651,15 @@ onKeyStroke( > {{ $t('package.links.code') }} + + stories + - - - - - storybook - sb internal - + diff --git a/app/utils/storybook-tree.ts b/app/utils/storybook-tree.ts index fb0ee7636..2643f397f 100644 --- a/app/utils/storybook-tree.ts +++ b/app/utils/storybook-tree.ts @@ -9,17 +9,13 @@ export function transformStorybookEntries( const tree: StorybookFileTree[] = [] const dirMap = new Map() - // Sort entries by title for consistent ordering - const sortedEntries = Object.values(entries).sort((a, b) => - (a.title || '').localeCompare(b.title || ''), - ) - - for (const entry of sortedEntries) { + // Use entries in original order to preserve object key ordering + for (const [_id, entry] of Object.entries(entries)) { // Parse title into path parts // "Example/Button/Primary" -> ["Example", "Button", "Primary"] if (!entry.title) continue const parts = entry.title.split('/') - const storyName = parts.pop()! // Last part is the story name + const storyName = entry.name const storyPath = parts.join('/') || '' || '' // Create directories as needed diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index 25695dada..0655549a9 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -630,25 +630,6 @@ }, "file_path": "File path" }, - "stories": { - "stories_label": "Stories", - "story_path": "Story path", - "root": "Stories", - "version_required": "Version is required to browse stories", - "go_to_package": "Go to package", - "loading_stories": "Loading Storybook stories...", - "no_storybook_found": "No Storybook found", - "check_package_json": "Check if this package has Storybook configuration in package.json", - "back_to_package": "Back to package", - "failed_to_load_stories": "Failed to load Storybook stories", - "storybook_unavailable": "The Storybook instance may be unavailable or misconfigured", - "open_storybook": "Open Storybook", - "story": "Story", - "open_in_storybook": "Open in Storybook", - "select_story": "Select a story from the tree to view it here", - "toggle_tree": "Toggle story tree", - "close_tree": "Close story tree" - }, "badges": { "provenance": { "verified": "verified", diff --git a/uno.config.ts b/uno.config.ts index b75446763..26bf8db80 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -98,6 +98,7 @@ export default defineConfig({ nuxt: '#00DC82', vite: '#646CFF', jsfiddle: '#0084FF', + storybook: '#FF4785', }, }, animation: { From 9490155814a981a7d34f08d6e09efaf9005951c7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:33:39 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- lunaria/files/en-GB.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index 0655549a9..25695dada 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -630,6 +630,25 @@ }, "file_path": "File path" }, + "stories": { + "stories_label": "Stories", + "story_path": "Story path", + "root": "Stories", + "version_required": "Version is required to browse stories", + "go_to_package": "Go to package", + "loading_stories": "Loading Storybook stories...", + "no_storybook_found": "No Storybook found", + "check_package_json": "Check if this package has Storybook configuration in package.json", + "back_to_package": "Back to package", + "failed_to_load_stories": "Failed to load Storybook stories", + "storybook_unavailable": "The Storybook instance may be unavailable or misconfigured", + "open_storybook": "Open Storybook", + "story": "Story", + "open_in_storybook": "Open in Storybook", + "select_story": "Select a story from the tree to view it here", + "toggle_tree": "Toggle story tree", + "close_tree": "Close story tree" + }, "badges": { "provenance": { "verified": "verified",