>(props: T) {
+ return ;
+}
+
+const MyComponent = (props: P) => ;
+`;
+
+ const result = await plugin.transform(code, '/some-project/src/utils/factory.ts');
+
+ // Should inject into JSX elements
+ if (result) {
+ expect(result.code).toContain('data-sb-component-path="./src/utils/factory.ts"');
+ expect(result.code).toContain('');
+ expect(code).toContain('');
+ }
+
+ // Should NOT inject into type parameters
+ if (result) {
+ expect(result.code).toContain('T extends ComponentProps<');
+ expect(result.code).not.toContain('T extends ComponentProps< data-sb-component-path=');
+ expect(result.code).toContain('MyComponent =
');
+ } else {
+ // If no result, verify original code remains unchanged
+ expect(code).toContain('T extends ComponentProps<');
+ expect(code).toContain('MyComponent =
');
+ }
+ });
+
+ it('should handle paths correctly by making them relative to code directory', async () => {
+ const plugin = componentPathInjectorPlugin();
+ const code = `export const Story = () => `;
+
+ const result1 = await plugin.transform(code, '/some-project/src/Button.tsx');
+ const result2 = await plugin.transform(
+ code,
+ '/different/path/to/storybook/code/addons/test/Component.tsx'
+ );
+ const result3 = await plugin.transform(code, '/no/code/directory/Component.tsx');
+
+ expect(result1?.code).toContain('data-sb-component-path="./src/Button.tsx"');
+ expect(result2?.code).toContain('data-sb-component-path="./addons/test/Component.tsx"');
+ // Should fall back to the original path if no /code/ directory is found (but still add ./ prefix)
+ expect(result3?.code).toContain('data-sb-component-path="./no/code/directory/Component.tsx"');
+ });
+});
diff --git a/code/addons/story-inspector/src/utils/story-matcher.ts b/code/addons/story-inspector/src/utils/story-matcher.ts
new file mode 100644
index 000000000000..e894bb48806a
--- /dev/null
+++ b/code/addons/story-inspector/src/utils/story-matcher.ts
@@ -0,0 +1,104 @@
+import type { IndexEntry, StoryIndex } from 'storybook/internal/types';
+
+import { COMPONENT_PATH_ATTRIBUTE } from '../constants';
+
+export interface ComponentInfo {
+ element: Element;
+ componentPath: string;
+ hasStory: boolean;
+ storyEntry?: IndexEntry;
+ storyId?: string;
+}
+
+/** Find all components in the DOM that have component path metadata */
+export function findComponentsInDOM(): ComponentInfo[] {
+ // Find the preview iframe - this is where the actual stories are rendered
+ const previewIframe = document.getElementById('storybook-preview-iframe') as HTMLIFrameElement;
+
+ if (!previewIframe) {
+ console.warn('Story Inspector: Preview iframe not found');
+ return [];
+ }
+
+ // Check if iframe is loaded and accessible
+ let previewDocument: Document;
+ try {
+ previewDocument = previewIframe.contentDocument || previewIframe.contentWindow?.document;
+ if (!previewDocument) {
+ console.warn('Story Inspector: Could not access preview iframe document');
+ return [];
+ }
+ } catch (error) {
+ // Handle cross-origin restrictions
+ console.warn('Story Inspector: Cannot access iframe document (possibly cross-origin):', error);
+ return [];
+ }
+
+ // Query within the iframe's document, not the manager's document
+ const elements = previewDocument.querySelectorAll(`[${COMPONENT_PATH_ATTRIBUTE}]`);
+ const componentMap = new Map();
+
+ elements.forEach((element) => {
+ const componentPath = element.getAttribute(COMPONENT_PATH_ATTRIBUTE);
+ if (componentPath) {
+ // Deduplicate: only keep one entry per component path
+ // Use the first element found for each unique component path
+ if (!componentMap.has(componentPath)) {
+ componentMap.set(componentPath, {
+ element,
+ componentPath,
+ hasStory: false, // Will be determined by checkComponentsAgainstIndex
+ });
+ }
+ }
+ });
+
+ return Array.from(componentMap.values());
+}
+
+/** Check components against the story index to determine which have stories */
+export function checkComponentsAgainstIndex(
+ components: ComponentInfo[],
+ storyIndex: StoryIndex['entries']
+): ComponentInfo[] {
+ const entries = storyIndex || {};
+
+ return components.map((component) => {
+ // Find matching story entries by comparing componentPath
+ const matchingEntry = Object.values(entries).find((entry) => {
+ // Normalize paths for comparison
+ const entryPath = (entry as any).componentPath?.replace(/\\/g, '/');
+ const componentPath = component.componentPath.replace(/\\/g, '/');
+ return entryPath === componentPath;
+ });
+
+ if (matchingEntry) {
+ return {
+ ...component,
+ hasStory: true,
+ storyEntry: matchingEntry,
+ storyId: matchingEntry.id,
+ };
+ }
+
+ return component;
+ });
+}
+
+/** Group components by their story status */
+export function groupComponentsByStoryStatus(components: ComponentInfo[]) {
+ const withStories = components.filter((c) => c.hasStory);
+ const withoutStories = components.filter((c) => !c.hasStory);
+
+ return { withStories, withoutStories };
+}
+
+/** Generate CSS selectors for highlighting components */
+export function generateSelectorsForComponents(components: ComponentInfo[]): string[] {
+ return components.map((component) => {
+ // Generate a unique selector for the element
+ const tagName = component.element.tagName.toLowerCase();
+ const componentPath = component.componentPath;
+ return `${tagName}[${COMPONENT_PATH_ATTRIBUTE}="${componentPath}"]`;
+ });
+}
diff --git a/code/addons/story-inspector/src/utils/vite-plugin.ts b/code/addons/story-inspector/src/utils/vite-plugin.ts
new file mode 100644
index 000000000000..d4fb42fd99b1
--- /dev/null
+++ b/code/addons/story-inspector/src/utils/vite-plugin.ts
@@ -0,0 +1,181 @@
+import { babelParse, generate, traverse } from 'storybook/internal/babel';
+import type { Options } from 'storybook/internal/types';
+
+import { COMPONENT_PATH_ATTRIBUTE } from '../constants';
+
+/**
+ * This Vite plugin injects component file path metadata into JSX/TSX elements so they can be
+ * identified by the story inspector
+ */
+export function componentPathInjectorPlugin(options?: Options): any {
+ // Simple filter function instead of using vite's createFilter
+ const filter = (id: string) => {
+ // Include React/Vue/Svelte component files
+ const includeExtensions = ['.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte'];
+ const hasIncludedExt = includeExtensions.some((ext) => id.endsWith(ext));
+
+ // Exclude certain patterns
+ const excludePatterns = ['node_modules', '.stories.', '.spec.', '.test.', 'dist', 'build'];
+ const isExcluded = excludePatterns.some((pattern) => id.includes(pattern));
+
+ return hasIncludedExt && !isExcluded;
+ };
+
+ return {
+ name: 'storybook:story-inspector-component-path-injector',
+ enforce: 'pre', // Run before other transforms
+
+ async transform(code: string, id: string) {
+ if (!filter(id)) {
+ return undefined;
+ }
+
+ // Skip if this doesn't look like a component file
+ if (!isComponentFile(code)) {
+ return undefined;
+ }
+
+ // Find JSX elements and add the component path attribute using AST parsing
+ const transformedCode = injectComponentPath(code, id);
+
+ if (transformedCode === code) {
+ return undefined; // No changes made
+ }
+
+ return {
+ code: transformedCode,
+ // For AST-based transformations, we don't generate source maps
+ // as Babel handles this internally and it's complex to maintain
+ map: null as any,
+ };
+ },
+ };
+}
+
+/** Check if the file likely contains React components */
+function isComponentFile(code: string): boolean {
+ // Look for JSX elements or React imports
+ return (
+ code.includes('') &&
+ (code.includes('jsx') ||
+ code.includes('React') ||
+ /export\s+(default\s+)?function\s+[A-Z]/.test(code) ||
+ /export\s+(default\s+)?const\s+[A-Z]/.test(code) ||
+ /export\s+{\s*[A-Z]/.test(code))
+ );
+}
+
+/** Inject component path attribute into JSX elements using AST parsing */
+function injectComponentPath(code: string, filePath: string): string {
+ // Make file path relative with ./ prefix - similar to how story index calculates paths
+ const root = process.cwd();
+ let normalizedPath = filePath.replace(root, '');
+ normalizedPath = normalizedPath.startsWith('./') ? normalizedPath : `.${normalizedPath}`;
+ try {
+ // Parse the code into an AST
+ const ast = babelParse(code);
+
+ let hasChanges = false;
+
+ // Traverse the AST to find JSX opening elements
+ traverse(ast, {
+ JSXOpeningElement(path) {
+ const { node } = path;
+
+ // Check if this is a React component (starts with uppercase)
+ if (node.name.type === 'JSXIdentifier') {
+ const componentName = node.name.name;
+
+ // Skip HTML elements (lowercase first letter)
+ if (componentName[0].toLowerCase() === componentName[0]) {
+ return;
+ }
+
+ // Check if we already have this attribute
+ const hasAttribute = node.attributes.some((attr) => {
+ return (
+ attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === COMPONENT_PATH_ATTRIBUTE
+ );
+ });
+
+ if (!hasAttribute) {
+ // Add the component path attribute
+ node.attributes.push({
+ type: 'JSXAttribute',
+ name: {
+ type: 'JSXIdentifier',
+ name: COMPONENT_PATH_ATTRIBUTE,
+ },
+ value: {
+ type: 'StringLiteral',
+ value: normalizedPath,
+ },
+ } as any);
+ hasChanges = true;
+ }
+ } else if (node.name.type === 'JSXMemberExpression') {
+ // Handle member expressions like Namespace.Component
+ const getComponentName = (expr: any): string => {
+ if (expr.type === 'JSXIdentifier') {
+ return expr.name;
+ }
+ if (expr.type === 'JSXMemberExpression') {
+ return getComponentName(expr.object) + '.' + getComponentName(expr.property);
+ }
+ return '';
+ };
+
+ const componentName = getComponentName(node.name);
+
+ // Skip if first part is lowercase (like html.div)
+ const firstPart = componentName.split('.')[0];
+ if (firstPart[0]?.toLowerCase() === firstPart[0]) {
+ return;
+ }
+
+ // Check if we already have this attribute
+ const hasAttribute = node.attributes.some((attr) => {
+ return (
+ attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === COMPONENT_PATH_ATTRIBUTE
+ );
+ });
+
+ if (!hasAttribute) {
+ // Add the component path attribute
+ node.attributes.push({
+ type: 'JSXAttribute',
+ name: {
+ type: 'JSXIdentifier',
+ name: COMPONENT_PATH_ATTRIBUTE,
+ },
+ value: {
+ type: 'StringLiteral',
+ value: normalizedPath,
+ },
+ } as any);
+ hasChanges = true;
+ }
+ }
+ },
+ });
+
+ if (hasChanges) {
+ // Generate the modified code
+ const result = generate(ast, {
+ retainLines: true,
+ compact: false,
+ });
+ return result.code;
+ }
+
+ return code;
+ } catch (error) {
+ // If AST parsing fails, return original code
+ console.warn('Story Inspector: Failed to parse code with AST, skipping transformation:', error);
+ return code;
+ }
+}
diff --git a/code/addons/story-inspector/src/vite-plugin.ts b/code/addons/story-inspector/src/vite-plugin.ts
new file mode 100644
index 000000000000..ea9c15f893ef
--- /dev/null
+++ b/code/addons/story-inspector/src/vite-plugin.ts
@@ -0,0 +1 @@
+export { componentPathInjectorPlugin as storyInspector } from './utils/vite-plugin';
diff --git a/code/addons/story-inspector/tsconfig.json b/code/addons/story-inspector/tsconfig.json
new file mode 100644
index 000000000000..b219c710a3f3
--- /dev/null
+++ b/code/addons/story-inspector/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "strict": false
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**/*.test.*", "src/**/test.*", "src/**/*.stories.*"]
+}
diff --git a/code/addons/story-inspector/vitest.config.ts b/code/addons/story-inspector/vitest.config.ts
new file mode 100644
index 000000000000..8e730d5055c9
--- /dev/null
+++ b/code/addons/story-inspector/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ },
+});
diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts
index 1b7575dfaf1d..f3fe7e228df4 100644
--- a/code/core/src/common/versions.ts
+++ b/code/core/src/common/versions.ts
@@ -6,6 +6,7 @@ export default {
'@storybook/addon-links': '10.0.0-beta.6',
'@storybook/addon-onboarding': '10.0.0-beta.6',
'storybook-addon-pseudo-states': '10.0.0-beta.6',
+ '@storybook/addon-story-inspector': '10.0.0-beta.6',
'@storybook/addon-themes': '10.0.0-beta.6',
'@storybook/addon-vitest': '10.0.0-beta.6',
'@storybook/builder-vite': '10.0.0-beta.6',
diff --git a/code/core/src/highlight/icons.ts b/code/core/src/highlight/icons.ts
index 00fe876fa32c..49f754701bbe 100644
--- a/code/core/src/highlight/icons.ts
+++ b/code/core/src/highlight/icons.ts
@@ -1,4 +1,8 @@
export const iconPaths = {
+ plus: ['M7.5.5a.5.5 0 00-1 0v6h-6a.5.5 0 000 1h6v6a.5.5 0 001 0v-6h6a.5.5 0 000-1h-6v-6z'],
+ edit: [
+ 'M13.5 1.004a.5.5 0 01.5.5v11l-.01.1a.501.501 0 01-.39.39l-.1.01H.5l-.1-.01a.501.501 0 01-.39-.39l-.01-.1v-11a.5.5 0 01.5-.5h13zm-12.5 11h12v-8H1v8zm.5-10a.5.5 0 100 1 .5.5 0 000-1zm2 0a.5.5 0 100 1 .5.5 0 000-1zm2 0a.5.5 0 100 1 .5.5 0 000-1z',
+ ],
chevronLeft: [
'M9.10355 10.1464C9.29882 10.3417 9.29882 10.6583 9.10355 10.8536C8.90829 11.0488 8.59171 11.0488 8.39645 10.8536L4.89645 7.35355C4.70118 7.15829 4.70118 6.84171 4.89645 6.64645L8.39645 3.14645C8.59171 2.95118 8.90829 2.95118 9.10355 3.14645C9.29882 3.34171 9.29882 3.65829 9.10355 3.85355L5.95711 7L9.10355 10.1464Z',
],
diff --git a/code/core/src/manager/components/sidebar/CreateNewStoryFileModal.tsx b/code/core/src/manager/components/sidebar/CreateNewStoryFileModal.tsx
index 725ba5b59890..a73198e99398 100644
--- a/code/core/src/manager/components/sidebar/CreateNewStoryFileModal.tsx
+++ b/code/core/src/manager/components/sidebar/CreateNewStoryFileModal.tsx
@@ -98,6 +98,7 @@ export const CreateNewStoryFileModal = ({ open, onOpenChange }: CreateNewStoryFi
const set = (data: ResponseData) => {
const isLatestRequest = data.id === fileSearchQueryDeferred;
+ console.log({ data });
if (isLatestRequest) {
if (data.success) {
setSearchResults(data.payload.files);
diff --git a/code/package.json b/code/package.json
index 10a96039113c..ce34735003f5 100644
--- a/code/package.json
+++ b/code/package.json
@@ -115,6 +115,7 @@
"@storybook/addon-jest": "workspace:*",
"@storybook/addon-links": "workspace:*",
"@storybook/addon-onboarding": "workspace:*",
+ "@storybook/addon-story-inspector": "workspace:*",
"@storybook/addon-themes": "workspace:*",
"@storybook/addon-vitest": "workspace:*",
"@storybook/angular": "workspace:*",
diff --git a/code/yarn.lock b/code/yarn.lock
index 882fc3cde4de..daacaa67742e 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -6199,6 +6199,21 @@ __metadata:
languageName: unknown
linkType: soft
+"@storybook/addon-story-inspector@workspace:*, @storybook/addon-story-inspector@workspace:addons/story-inspector":
+ version: 0.0.0-use.local
+ resolution: "@storybook/addon-story-inspector@workspace:addons/story-inspector"
+ dependencies:
+ "@storybook/icons": "npm:^1.6.0"
+ magic-string: "npm:^0.30.5"
+ react: "npm:^18.2.0"
+ react-dom: "npm:^18.2.0"
+ ts-dedent: "npm:^2.0.0"
+ typescript: "npm:^5.8.3"
+ peerDependencies:
+ storybook: "workspace:^"
+ languageName: unknown
+ linkType: soft
+
"@storybook/addon-themes@workspace:*, @storybook/addon-themes@workspace:addons/themes":
version: 0.0.0-use.local
resolution: "@storybook/addon-themes@workspace:addons/themes"
@@ -6874,6 +6889,7 @@ __metadata:
"@storybook/addon-jest": "workspace:*"
"@storybook/addon-links": "workspace:*"
"@storybook/addon-onboarding": "workspace:*"
+ "@storybook/addon-story-inspector": "workspace:*"
"@storybook/addon-themes": "workspace:*"
"@storybook/addon-vitest": "workspace:*"
"@storybook/angular": "workspace:*"
diff --git a/scripts/build/entry-configs.ts b/scripts/build/entry-configs.ts
index f8e551623900..72c9e308a83b 100644
--- a/scripts/build/entry-configs.ts
+++ b/scripts/build/entry-configs.ts
@@ -11,6 +11,8 @@ import onboardingConfig from '../../../code/addons/onboarding/build-config';
// @ts-ignore
import pseudoStatesConfig from '../../../code/addons/pseudo-states/build-config';
// @ts-ignore
+import storyInspectorConfig from '../../../code/addons/story-inspector/build-config';
+// @ts-ignore
import themesConfig from '../../../code/addons/themes/build-config';
// @ts-ignore
import vitestConfig from '../../../code/addons/vitest/build-config';
@@ -93,6 +95,7 @@ export const buildEntries = {
'@storybook/addon-links': linksConfig,
'@storybook/addon-onboarding': onboardingConfig,
'storybook-addon-pseudo-states': pseudoStatesConfig,
+ '@storybook/addon-story-inspector': storyInspectorConfig,
'@storybook/addon-themes': themesConfig,
'@storybook/addon-vitest': vitestConfig,
'@storybook/addon-jest': jestConfig,