diff --git a/.changeset/large-carrots-judge.md b/.changeset/large-carrots-judge.md new file mode 100644 index 00000000..fc549b14 --- /dev/null +++ b/.changeset/large-carrots-judge.md @@ -0,0 +1,5 @@ +--- +'@storybook/addon-mcp': patch +--- + +Add support for docs entries in manifests, sourced by MDX files. diff --git a/.changeset/stale-ducks-pay.md b/.changeset/stale-ducks-pay.md new file mode 100644 index 00000000..56625f5d --- /dev/null +++ b/.changeset/stale-ducks-pay.md @@ -0,0 +1,17 @@ +--- +'@storybook/mcp': minor +--- + +Add support for docs entries in manifests, sourced by MDX files. + +# Breaking Changes + +This change introduces a number of minor breaking changes to `@storybook/mcp`: + +1. The lower level tool adder functions have been renamed: +2. `addGetComponentDocumentationTool` -> `addGetDocumentationTool` +3. `addListAllComponentsTool` -> `addListAllDocumentationTool` +4. The optional tool hooks have been renamed: +5. `onListAllComponents` -> `onListAllDocumentation` +6. `onGetComponentDocumentation` -> `onGetDocumentation` +7. The exported `MANIFEST_PATH` constant have been removed in favor of two new constants, `COMPONENT_MANIFEST_PATH` and `DOCS_MANIFEST_PATH` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c72ea5ac..71d4129e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -34,7 +34,7 @@ The addon supports configuring which toolsets are enabled: options: { toolsets: { dev: true, // get-story-urls, get-ui-building-instructions - docs: true, // list-all-components, get-component-documentation + docs: true, // list-all-documentation, get-component-documentation }, experimentalFormat: 'markdown' // Output format: 'markdown' (default) or 'xml' } @@ -72,8 +72,8 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic: - Returns the manifest JSON as a string - **Optional handlers**: `StorybookContext` supports optional handlers that are called at various points, allowing consumers to track usage or collect telemetry: - `onSessionInitialize`: Called when an MCP session is initialized - - `onListAllComponents`: Called when the list-all-components tool is invoked - - `onGetComponentDocumentation`: Called when the get-component-documentation tool is invoked + - `onListAllDocumentation`: Called when the list-all-documentation tool is invoked + - `onGetDocumentation`: Called when the get-component-documentation tool is invoked - **Output Format**: The `format` property in context controls output format: - `'markdown'` (default): Token-efficient markdown with adaptive formatting - `'xml'`: Legacy XML format @@ -261,14 +261,14 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts'; - `AddonContext` extends `StorybookContext` to ensure type compatibility - Component manifest tools are conditionally registered based on feature flags: - Checks `features.experimentalComponentsManifest` flag - - Checks for `experimental_componentManifestGenerator` preset - - Only registers `addListAllComponentsTool` and `addGetComponentDocumentationTool` when enabled + - Checks for `experimental_manifests` preset + - Only registers `addListAllDocumentationTool` and `addGetDocumentationTool` when enabled - Context includes `request` (HTTP Request object) which tools use to determine manifest location - Default manifest URL is constructed from request origin, replacing `/mcp` with `/manifests/components.json` - **Optional handlers for tracking**: - `onSessionInitialize`: Called when an MCP session is initialized, receives context - - `onListAllComponents`: Called when list tool is invoked, receives context and manifest - - `onGetComponentDocumentation`: Called when get tool is invoked, receives context, input with componentId, and optional foundComponent + - `onListAllDocumentation`: Called when list tool is invoked, receives context and manifest + - `onGetDocumentation`: Called when get tool is invoked, receives context, input with id, and optional foundDocumentation - Addon-mcp uses these handlers to collect telemetry on tool usage **Storybook internals used:** @@ -278,7 +278,7 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts'; - `storybook/internal/node-logger` - Logging utilities - Framework detection via `options.presets.apply('framework')` - Feature flags via `options.presets.apply('features')` -- Component manifest generator via `options.presets.apply('experimental_componentManifestGenerator')` +- Component manifest generator via `options.presets.apply('experimental_manifests')` **Story URL generation:** diff --git a/.github/instructions/addon-mcp.instructions.md b/.github/instructions/addon-mcp.instructions.md index a1fa386d..8f0d2ac9 100644 --- a/.github/instructions/addon-mcp.instructions.md +++ b/.github/instructions/addon-mcp.instructions.md @@ -29,7 +29,7 @@ The addon supports two toolsets that can be enabled/disabled: - `get-ui-building-instructions`: Provide UI development guidelines 2. **`docs`** (default: true) - - `list-all-components`: List all available components from manifest + - `list-all-documentation`: List all available components from manifest - `get-component-documentation`: Get detailed component documentation - Requires experimental feature flag `features.experimentalComponentsManifest` @@ -430,32 +430,29 @@ The addon collects anonymous usage data: **For addon-specific tools**: Telemetry is collected directly in the tool implementation using `collectTelemetry()`. -**For reused tools from `@storybook/mcp`**: The addon uses optional handlers (`onListAllComponents`, `onGetComponentDocumentation`) provided by the `StorybookContext` to track usage. These handlers are passed in the context when calling `transport.respond()` in `mcp-handler.ts`: +**For reused tools from `@storybook/mcp`**: The addon uses optional handlers (`onListAllDocumentation`, `onGetDocumentation`) provided by the `StorybookContext` to track usage. These handlers are passed in the context when calling `transport.respond()` in `mcp-handler.ts`: ```typescript const addonContext: AddonContext = { // ... other context properties - onListAllComponents: async ({ manifest }) => { + onListAllDocumentation: async ({ manifests }) => { if (!disableTelemetry && server) { await collectTelemetry({ - event: 'tool:listAllComponents', + event: 'tool:listAllDocumentation', server, - componentCount: Object.keys(manifest.components).length, + componentCount: Object.keys(manifests.componentManifest.components) + .length, + docsCount: Object.keys(manifests.docsManifest.docs).length, }); } }, - onGetComponentDocumentation: async ({ - input, - foundComponents, - notFoundIds, - }) => { + onGetDocumentation: async ({ input, foundDocumentation }) => { if (!disableTelemetry && server) { await collectTelemetry({ - event: 'tool:getComponentDocumentation', + event: 'tool:getDocumentation', server, - inputComponentCount: input.componentIds.length, - foundCount: foundComponents.length, - notFoundCount: notFoundIds.length, + id: input.id, + found: !!foundDocumentation, }); } }, diff --git a/.github/instructions/eval.instructions.md b/.github/instructions/eval.instructions.md index 4721e384..da827380 100644 --- a/.github/instructions/eval.instructions.md +++ b/.github/instructions/eval.instructions.md @@ -681,9 +681,9 @@ When using `--context storybook-dev`, the framework: ### Storybook MCP - Docs Pattern (Component Manifest) -When using `--context components.json`, the framework: +When using `--context storybook-docs`, the framework: -1. Reads the manifest file from the eval directory +1. Reads the manifest files from the eval directory, components.json and optionally docs.json 2. Creates `.mcp.json` in project with stdio server config: ```json { @@ -691,7 +691,7 @@ When using `--context components.json`, the framework: "storybook-mcp": { "type": "stdio", "command": "node", - "args": ["../../packages/mcp/bin.ts", "--manifestPath", "/path/to/components.json"] + "args": ["../../packages/mcp/bin.ts", "--manifestsDir", "/path/to/manifests/dir/"] } } } diff --git a/.github/instructions/mcp.instructions.md b/.github/instructions/mcp.instructions.md index 4ca8918b..73924ae1 100644 --- a/.github/instructions/mcp.instructions.md +++ b/.github/instructions/mcp.instructions.md @@ -17,7 +17,7 @@ This is a Model Context Protocol (MCP) server for Storybook that serves knowledg - **Component Manifest**: Parses and formats component documentation including React prop information from react-docgen - **Schema Validation**: Uses Valibot for JSON schema validation via `@tmcp/adapter-valibot` - **HTTP Transport**: Provides HTTP-based MCP communication via `@tmcp/transport-http` -- **Context System**: `StorybookContext` allows passing optional handlers (`onSessionInitialize`, `onListAllComponents`, `onGetComponentDocumentation`) that are called at various points when provided. The `onGetComponentDocumentation` handler receives a single `componentId` input and an optional `foundComponent` result. +- **Context System**: `StorybookContext` allows passing optional handlers (`onSessionInitialize`, `onListAllDocumentation`, `onGetDocumentation`) that are called at various points when provided. The `onGetDocumentation` handler receives a single `id` input and an optional `foundDocumentation` result. ### File Structure @@ -26,7 +26,7 @@ src/ index.ts # Main entry point - exports createStorybookMcpHandler serve.ts # Development server setup tools/ - list-all-components.ts # List all components tool + list-all-documentation.ts # List all documentation tool get-component-documentation.ts # Get component documentation tool utils/ format-manifest.ts # Format component manifest to XML diff --git a/README.md b/README.md index 95893feb..5b9186f1 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ curl -X POST http://localhost:13316/mcp \ "id": 2, "method": "tools/call", "params": { - "name": "list-all-components", + "name": "list-all-documentation", "arguments": {} } }' diff --git a/apps/internal-storybook/.claude/settings.local.json b/apps/internal-storybook/.claude/settings.local.json index 13b72b65..01323a79 100644 --- a/apps/internal-storybook/.claude/settings.local.json +++ b/apps/internal-storybook/.claude/settings.local.json @@ -3,8 +3,8 @@ "allow": [ "mcp__storybook-addon-mcp__get-ui-building-instructions", "mcp__storybook-addon-mcp__get-story-urls", - "mcp__storybook-addon-mcp__list-all-components", - "mcp__storybook-addon-mcp__get-component-documentation" + "mcp__storybook-addon-mcp__list-all-documentation", + "mcp__storybook-addon-mcp__get-documentation" ] }, "enabledMcpjsonServers": ["storybook-addon-mcp", "storybook-mcp"] diff --git a/apps/internal-storybook/stories/components/Button.mdx b/apps/internal-storybook/stories/components/Button.mdx new file mode 100644 index 00000000..0ffce32f --- /dev/null +++ b/apps/internal-storybook/stories/components/Button.mdx @@ -0,0 +1,10 @@ +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import * as ButtonStories from './Button.stories'; + + + +It is critical when using the Button component, that the string passed to the `label` prop uses the 🍌-emoji instead of spaces. + +Here is the button: + + diff --git a/apps/internal-storybook/stories/getting-started.mdx b/apps/internal-storybook/stories/getting-started.mdx new file mode 100644 index 00000000..789df7be --- /dev/null +++ b/apps/internal-storybook/stories/getting-started.mdx @@ -0,0 +1,7 @@ +# Getting Started + +This is the getting started documentation of this design system. + +## Usage + +The most critical information to know, is that when using the Button component, you MUST put the 🍇-emoji before and after the component, otherwise it will break. diff --git a/apps/internal-storybook/tests/check-deps.e2e.test.ts b/apps/internal-storybook/tests/check-deps.e2e.test.ts index 7264e9fd..ae52b9f2 100644 --- a/apps/internal-storybook/tests/check-deps.e2e.test.ts +++ b/apps/internal-storybook/tests/check-deps.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, it } from 'vitest'; import { x } from 'tinyexec'; +import path from 'node:path'; const PACKAGES_TO_CHECK = [ '@storybook/addon-docs', @@ -15,17 +16,14 @@ describe('Storybook Dependencies', () => { for (const pkg of PACKAGES_TO_CHECK) { // Get local version const listResult = await x('pnpm', ['list', pkg, '--json'], { - nodeOptions: { cwd: process.cwd() }, + nodeOptions: { cwd: path.join(import.meta.dirname, '..') }, }); + const listData = JSON.parse(listResult.stdout); const currentVersion = listData[0]?.devDependencies?.[pkg]?.version || listData[0]?.dependencies?.[pkg]?.version; - if (!currentVersion) { - continue; - } - // Get registry version for @next tag const viewResult = await x('pnpm', ['view', `${pkg}@next`, 'version'], { nodeOptions: { cwd: process.cwd() }, diff --git a/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts index b307a26e..53741ad9 100644 --- a/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts +++ b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts @@ -213,30 +213,30 @@ describe('MCP Endpoint E2E Tests', () => { "title": "UI Component Building Instructions", }, { - "description": "List all available UI components from the component library", + "description": "List all available UI components and documentation entries from the Storybook", "inputSchema": { "properties": {}, "type": "object", }, - "name": "list-all-components", - "title": "List All Components", + "name": "list-all-documentation", + "title": "List All Documentation", }, { - "description": "Get detailed documentation for a specific UI component", + "description": "Get detailed documentation for a specific UI component or docs entry", "inputSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "componentId": { + "id": { "type": "string", }, }, "required": [ - "componentId", + "id", ], "type": "object", }, - "name": "get-component-documentation", - "title": "Get Documentation for Component", + "name": "get-documentation", + "title": "Get Documentation", }, ] `); @@ -310,10 +310,10 @@ describe('MCP Endpoint E2E Tests', () => { }); }); - describe('Tool: list-all-components', () => { - it('should list all components from manifest', async () => { + describe('Tool: list-all-documentation', () => { + it('should list all documentation from manifest', async () => { const response = await mcpRequest('tools/call', { - name: 'list-all-components', + name: 'list-all-documentation', arguments: {}, }); @@ -326,7 +326,11 @@ describe('MCP Endpoint E2E Tests', () => { - Button (example-button): A customizable button component for user interactions. - Header (header) - Page (page) - - Card (other-ui-card): Card component with title, image, content, and action button", + - Card (other-ui-card): Card component with title, image, content, and action button + + # Docs + + - getting-started (getting-started--docs): # Getting Started This is the getting started documentation of this design system. ## Usag...", "type": "text", }, ], @@ -335,11 +339,11 @@ describe('MCP Endpoint E2E Tests', () => { }); }); - describe('Tool: get-component-documentation', () => { + describe('Tool: get-documentation', () => { it('should return documentation for a specific component', async () => { // First, get the list to find a valid component ID const listResponse = await mcpRequest('tools/call', { - name: 'list-all-components', + name: 'list-all-documentation', arguments: {}, }); @@ -351,9 +355,9 @@ describe('MCP Endpoint E2E Tests', () => { // Now get documentation for that component const response = await mcpRequest('tools/call', { - name: 'get-component-documentation', + name: 'get-documentation', arguments: { - componentId, + id: componentId, }, }); @@ -374,7 +378,7 @@ describe('MCP Endpoint E2E Tests', () => { \`\`\` import { Button } from "@my-org/my-component-library"; - const Primary = () => ; + const Primary = () => ; + const Secondary = () => ; + const Large = () => ; + const Small = () => " + }, + { + "id": "button--secondary", + "name": "Secondary", + "description": "The secondary button variant is used for secondary actions that are still important but not the primary focus of the page.\n\nSecondary buttons have less visual weight than primary buttons and can be used multiple times on a page.", + "summary": "Secondary button for supporting actions", + "import": "import { Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Secondary Button" + } + ], + "snippet": "const Secondary = () => " + }, + { + "id": "button--with-sizes", + "name": "WithSizes", + "description": "Buttons are available in three sizes: small, medium (default), and large.\n\nChoose the appropriate size based on the context and hierarchy of actions. Larger buttons are more prominent and easier to tap on mobile devices.", + "summary": "Button size variations", + "import": "import { Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Button Sizes" + } + ], + "snippet": "const WithSizes = () => (\n <>\n \n \n \n \n)" + }, + { + "id": "button--loading", + "name": "Loading", + "description": "The loading state provides visual feedback when an async operation is in progress.\n\nWhen loading is true, the button displays a spinner and is automatically disabled to prevent multiple submissions. The button text remains visible to maintain layout stability.", + "summary": "Button in loading state during async operations", + "import": "import { Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Loading Button" + } + ], + "snippet": "const Loading = () => " + }, + { + "id": "button--danger", + "name": "Danger", + "description": "The danger variant is used for destructive actions that cannot be easily undone, such as deleting data or canceling subscriptions.\n\nUse this variant to draw attention to the serious nature of the action. Consider adding a confirmation dialog for critical operations.", + "summary": "Danger button for destructive actions", + "import": "import { Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Danger Button" + }, + { + "key": "warning", + "value": "Use with caution for destructive actions" + } + ], + "snippet": "const Danger = () => " + } + ], + "jsDocTag": [ + { + "key": "summary", + "value": "A versatile button component for user interactions" + }, + { + "key": "since", + "value": "1.0.0" + }, + { + "key": "component", + "value": "Button" + } + ] + }, + "card": { + "id": "card", + "path": "src/components/Card.tsx", + "name": "Card", + "description": "A flexible container component for grouping related content with optional header, footer, and action areas.\n\nThe Card component provides a consistent way to present information in a contained, elevated surface. It's commonly used for displaying articles, products, user profiles, or any grouped content that benefits from visual separation.\n\n## Design Principles\n\n- Cards should contain a single subject or action\n- Maintain consistent padding and spacing\n- Use elevation to indicate interactive vs static cards\n- Keep content hierarchy clear with proper use of typography", + "summary": "A flexible container component for grouping related content", + "import": "import { Card } from '@storybook/design-system';", + "reactDocgen": { + "props": { + "variant": { + "description": "The visual style variant of the card", + "required": false, + "tsType": { + "name": "union", + "raw": "\"elevated\" | \"outlined\" | \"flat\"", + "elements": [ + { "name": "literal", "value": "\"elevated\"" }, + { "name": "literal", "value": "\"outlined\"" }, + { "name": "literal", "value": "\"flat\"" } + ] + }, + "defaultValue": { "value": "\"elevated\"", "computed": false } + }, + "padding": { + "description": "The amount of internal padding", + "required": false, + "tsType": { + "name": "union", + "raw": "\"none\" | \"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"none\"" }, + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } + }, + "clickable": { + "description": "Whether the entire card is clickable/interactive", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "header": { + "description": "Content to display in the card header", + "required": false, + "tsType": { "name": "ReactNode" } + }, + "footer": { + "description": "Content to display in the card footer", + "required": false, + "tsType": { "name": "ReactNode" } + }, + "children": { + "description": "The main content of the card", + "required": true, + "tsType": { "name": "ReactNode" } + }, + "onClick": { + "description": "Callback function when the card is clicked (requires clickable=true)", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } + } + } + }, + "stories": [ + { + "id": "card--basic", + "name": "Basic", + "description": "A basic card with just content.\n\nThe default elevated variant provides subtle depth through shadow, making the card appear to float above the page. This is ideal for creating visual hierarchy and grouping related information.", + "summary": "Basic card with content only", + "import": "import { Card } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Basic Card" + } + ], + "snippet": "const Basic = () => (\n \n

Card Title

\n

This is some card content that provides information to the user.

\n
\n)" + }, + { + "id": "card--with-header-and-footer", + "name": "WithHeaderAndFooter", + "description": "A card with distinct header and footer sections.\n\nHeaders typically contain titles, subtitles, or avatars. Footers often contain actions like buttons or metadata like timestamps. The header and footer are visually separated from the main content area.", + "summary": "Card with header and footer sections", + "import": "import { Card } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Card with Header and Footer" + } + ], + "snippet": "const WithHeaderAndFooter = () => (\n Article Title}\n footer={}\n >\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

\n \n)" + }, + { + "id": "card--clickable", + "name": "Clickable", + "description": "An interactive card that responds to clicks.\n\nClickable cards add hover effects and cursor changes to indicate interactivity. This pattern is useful for navigation cards, product cards, or any scenario where the entire card acts as a single interactive element.\n\n## Accessibility\n\nClickable cards are rendered as buttons with proper keyboard support and ARIA attributes.", + "summary": "Interactive clickable card", + "import": "import { Card } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Clickable Card" + } + ], + "snippet": "const Clickable = () => (\n alert('Card clicked!')}>\n

Product Name

\n

Click anywhere on this card to view details.

\n
\n)" + }, + { + "id": "card--variants", + "name": "Variants", + "description": "Different visual variants of the card component.\n\n- **Elevated**: Default variant with shadow for depth\n- **Outlined**: Border-only variant without shadow\n- **Flat**: No border or shadow, minimal visual separation\n\nChoose variants based on your design system and the level of emphasis needed.", + "summary": "Card visual variants (elevated, outlined, flat)", + "import": "import { Card } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Card Variants" + } + ], + "snippet": "const Variants = () => (\n <>\n \n

Elevated card with shadow

\n
\n \n

Outlined card with border

\n
\n \n

Flat card without border or shadow

\n
\n \n)" + }, + { + "id": "card--user-profile", + "name": "UserProfile", + "description": "A real-world example of a user profile card.\n\nThis example demonstrates how to compose the Card component with other design system components to create a complete, functional UI element. It includes an avatar, user information, stats, and action buttons.", + "summary": "Complete user profile card example", + "import": "import { Card, Avatar, Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "User Profile Card" + }, + { + "key": "composition", + "value": "Uses Avatar and Button components" + } + ], + "snippet": "const UserProfile = () => (\n \n \n
\n

Jane Doe

\n

Senior Developer

\n
\n \n }\n footer={\n
\n \n \n
\n }\n >\n
\n
1.2K
Followers
\n
342
Following
\n
89
Posts
\n
\n \n)" + } + ], + "jsDocTag": [ + { + "key": "summary", + "value": "A flexible container component for grouping related content" + }, + { + "key": "since", + "value": "1.0.0" + }, + { + "key": "component", + "value": "Card" + }, + { + "key": "pattern", + "value": "Container" + } + ] + }, + "input": { + "id": "input", + "path": "src/components/Input.tsx", + "name": "Input", + "description": "A flexible text input component that supports various input types, validation states, and accessibility features.\n\nThe Input component is a foundational form element that wraps the native HTML input with consistent styling and behavior. It includes support for labels, error messages, helper text, and different visual states.\n\n## Accessibility\n\nThe Input component automatically manages ARIA attributes for labels, descriptions, and error messages to ensure screen reader compatibility.", + "summary": "A flexible text input component with validation support", + "import": "import { Input } from '@storybook/design-system';", + "reactDocgen": { + "props": { + "type": { + "description": "The type of input field", + "required": false, + "tsType": { + "name": "union", + "raw": "\"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\"", + "elements": [ + { "name": "literal", "value": "\"text\"" }, + { "name": "literal", "value": "\"email\"" }, + { "name": "literal", "value": "\"password\"" }, + { "name": "literal", "value": "\"number\"" }, + { "name": "literal", "value": "\"tel\"" }, + { "name": "literal", "value": "\"url\"" } + ] + }, + "defaultValue": { "value": "\"text\"", "computed": false } + }, + "label": { + "description": "The label text for the input", + "required": false, + "tsType": { "name": "string" } + }, + "placeholder": { + "description": "Placeholder text shown when the input is empty", + "required": false, + "tsType": { "name": "string" } + }, + "value": { + "description": "The controlled value of the input", + "required": false, + "tsType": { "name": "string" } + }, + "defaultValue": { + "description": "The initial value for an uncontrolled input", + "required": false, + "tsType": { "name": "string" } + }, + "disabled": { + "description": "Whether the input is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "required": { + "description": "Whether the input is required", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "error": { + "description": "Error message to display below the input", + "required": false, + "tsType": { "name": "string" } + }, + "helperText": { + "description": "Helper text to display below the input", + "required": false, + "tsType": { "name": "string" } + }, + "onChange": { + "description": "Callback function when the input value changes", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { + "name": "event", + "type": { + "name": "ChangeEvent", + "elements": [{ "name": "HTMLInputElement" }] + } + } + ], + "return": { "name": "void" } + } + } + } + } + }, + "stories": [ + { + "id": "input--basic", + "name": "Basic", + "description": "A basic text input with a label.\n\nThis is the most common use case for the Input component. Always include a label for accessibility, even if it's visually hidden in your design.", + "summary": "Basic text input with label", + "import": "import { Input } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Basic Input" + } + ], + "snippet": "const Basic = () => " + }, + { + "id": "input--with-error", + "name": "WithError", + "description": "An input displaying an error state with an error message.\n\nError messages should be clear, concise, and provide actionable guidance to help users fix the issue. The input border and message text are styled in red to indicate the error state.", + "summary": "Input with validation error", + "import": "import { Input } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Input with Error" + } + ], + "snippet": "const WithError = () => " + }, + { + "id": "input--with-helper-text", + "name": "WithHelperText", + "description": "An input with helper text providing additional context or instructions.\n\nHelper text appears below the input and provides guidance without being an error. Use it to clarify format expectations, character limits, or provide helpful hints.", + "summary": "Input with helper text for guidance", + "import": "import { Input } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Input with Helper Text" + } + ], + "snippet": "const WithHelperText = () => " + }, + { + "id": "input--types", + "name": "Types", + "description": "Different input types for various data formats.\n\nUsing the correct input type improves the user experience by showing appropriate mobile keyboards and enabling browser validation features.", + "summary": "Various input types (email, tel, url, number)", + "import": "import { Input } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Input Types" + } + ], + "snippet": "const Types = () => (\n <>\n \n \n \n \n \n)" + }, + { + "id": "input--disabled", + "name": "Disabled", + "description": "A disabled input that cannot be interacted with.\n\nDisabled inputs are useful for displaying non-editable data in forms or for inputs that become available only after certain conditions are met.", + "summary": "Disabled input state", + "import": "import { Input } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Disabled Input" + } + ], + "snippet": "const Disabled = () => " + } + ], + "jsDocTag": [ + { + "key": "summary", + "value": "A flexible text input component with validation support" + }, + { + "key": "since", + "value": "1.0.0" + }, + { + "key": "component", + "value": "Input" + }, + { + "key": "accessibility", + "value": "WCAG 2.1 Level AA compliant" + } + ] + } + } +} diff --git a/packages/mcp/fixtures/default/docs.json b/packages/mcp/fixtures/default/docs.json new file mode 100644 index 00000000..5242d31e --- /dev/null +++ b/packages/mcp/fixtures/default/docs.json @@ -0,0 +1,19 @@ +{ + "v": 1, + "docs": { + "getting-started": { + "id": "getting-started", + "name": "Getting Started", + "title": "Getting Started Guide", + "path": "docs/getting-started.mdx", + "content": "# Getting Started\n\nWelcome to the component library. This guide will help you get up and running.\n\n## Installation\n\n```bash\nnpm install my-component-library\n```\n\n## Usage\n\nImport components and use them in your application." + }, + "theming": { + "id": "theming", + "name": "Theming", + "title": "Theming and Customization", + "path": "docs/theming.mdx", + "content": "# Theming\n\nLearn how to customize the look and feel of components using our theming system.\n\n## Theme Provider\n\nWrap your app with the ThemeProvider." + } + } +} diff --git a/packages/mcp/fixtures/small-docs-manifest.fixture.json b/packages/mcp/fixtures/small-docs-manifest.fixture.json new file mode 100644 index 00000000..5242d31e --- /dev/null +++ b/packages/mcp/fixtures/small-docs-manifest.fixture.json @@ -0,0 +1,19 @@ +{ + "v": 1, + "docs": { + "getting-started": { + "id": "getting-started", + "name": "Getting Started", + "title": "Getting Started Guide", + "path": "docs/getting-started.mdx", + "content": "# Getting Started\n\nWelcome to the component library. This guide will help you get up and running.\n\n## Installation\n\n```bash\nnpm install my-component-library\n```\n\n## Usage\n\nImport components and use them in your application." + }, + "theming": { + "id": "theming", + "name": "Theming", + "title": "Theming and Customization", + "path": "docs/theming.mdx", + "content": "# Theming\n\nLearn how to customize the look and feel of components using our theming system.\n\n## Theme Provider\n\nWrap your app with the ThemeProvider." + } + } +} diff --git a/packages/mcp/serve.ts b/packages/mcp/serve.ts index d00bee37..2f6f793b 100644 --- a/packages/mcp/serve.ts +++ b/packages/mcp/serve.ts @@ -3,24 +3,25 @@ import { serve } from 'srvx'; import fs from 'node:fs/promises'; import { parseArgs } from 'node:util'; import type { OutputFormat } from './src/types.ts'; +import { basename } from 'node:path'; async function serveMcp( port: number, - manifestPath: string, + manifestsDir: string, format: OutputFormat, ) { const storybookMcpHandler = await createStorybookMcpHandler({ format, // Use the local fixture file via manifestProvider - manifestProvider: async () => { + manifestProvider: async (_request, path) => { if ( - manifestPath.startsWith('http://') || - manifestPath.startsWith('https://') + manifestsDir.startsWith('http://') || + manifestsDir.startsWith('https://') ) { - const res = await fetch(manifestPath); + const res = await fetch(`${manifestsDir}/${basename(path)}`); return await res.text(); } - return await fs.readFile(manifestPath, 'utf-8'); + return await fs.readFile(`${manifestsDir}/${basename(path)}`, 'utf-8'); }, }); @@ -46,9 +47,9 @@ if (import.meta.main) { type: 'string', default: '13316', }, - manifestPath: { + manifestsDir: { type: 'string', - default: './fixtures/full-manifest.fixture.json', + default: './fixtures/default', }, format: { type: 'string', @@ -58,7 +59,7 @@ if (import.meta.main) { }); await serveMcp( Number(args.values.port), - args.values.manifestPath, + args.values.manifestsDir, args.values.format as OutputFormat, ); } diff --git a/packages/mcp/src/index.test.ts b/packages/mcp/src/index.test.ts index c85b093d..b2d61463 100644 --- a/packages/mcp/src/index.test.ts +++ b/packages/mcp/src/index.test.ts @@ -3,6 +3,36 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { createStorybookMcpHandler } from './index.ts'; import smallManifestFixture from '../fixtures/small-manifest.fixture.json' with { type: 'json' }; +import smallDocsManifestFixture from '../fixtures/small-docs-manifest.fixture.json' with { type: 'json' }; + +/** + * Creates a manifestProvider mock that returns component manifest for components.json + * and throws an error for docs.json (simulating no docs manifest available) + */ +function createManifestProviderMock() { + return vi.fn().mockImplementation((_request: Request, path: string) => { + if (path.includes('components.json')) { + return Promise.resolve(JSON.stringify(smallManifestFixture)); + } + // Simulate docs.json not found + return Promise.reject(new Error('Not found')); + }); +} + +/** + * Creates a manifestProvider mock that returns both component and docs manifests + */ +function createManifestProviderMockWithDocs() { + return vi.fn().mockImplementation((_request: Request, path: string) => { + if (path.includes('components.json')) { + return Promise.resolve(JSON.stringify(smallManifestFixture)); + } + if (path.includes('docs.json')) { + return Promise.resolve(JSON.stringify(smallDocsManifestFixture)); + } + return Promise.reject(new Error('Not found')); + }); +} describe('createStorybookMcpHandler', () => { let client: Client; @@ -59,12 +89,12 @@ describe('createStorybookMcpHandler', () => { expect(tools.tools).toEqual( expect.arrayContaining([ expect.objectContaining({ - name: 'list-all-components', - title: 'List All Components', + name: 'list-all-documentation', + title: 'List All Documentation', }), expect.objectContaining({ - name: 'get-component-documentation', - title: 'Get Documentation for Component', + name: 'get-documentation', + title: 'Get Documentation', }), ]), ); @@ -89,10 +119,8 @@ describe('createStorybookMcpHandler', () => { ); }); - it('should use manifestProvider when calling list-all-components', async () => { - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(smallManifestFixture)); + it('should use manifestProvider when calling list-all-documentation', async () => { + const manifestProvider = createManifestProviderMock(); const handler = await createStorybookMcpHandler({ manifestProvider, @@ -100,7 +128,7 @@ describe('createStorybookMcpHandler', () => { await setupClient(handler); const result = await client.callTool({ - name: 'list-all-components', + name: 'list-all-documentation', arguments: {}, }); @@ -115,58 +143,54 @@ describe('createStorybookMcpHandler', () => { }); }); - it('should call onListAllComponents handler when tool is invoked', async () => { - const onListAllComponents = vi.fn(); - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(smallManifestFixture)); + it('should call onListAllDocumentation handler when tool is invoked', async () => { + const onListAllDocumentation = vi.fn(); + const manifestProvider = createManifestProviderMock(); const handler = await createStorybookMcpHandler({ manifestProvider, - onListAllComponents, + onListAllDocumentation, }); await setupClient(handler); await client.callTool({ - name: 'list-all-components', + name: 'list-all-documentation', arguments: {}, }); - expect(onListAllComponents).toHaveBeenCalledTimes(1); - expect(onListAllComponents).toHaveBeenCalledWith({ + expect(onListAllDocumentation).toHaveBeenCalledTimes(1); + expect(onListAllDocumentation).toHaveBeenCalledWith({ context: expect.objectContaining({ request: expect.any(Request), }), - manifest: smallManifestFixture, + manifests: { componentManifest: smallManifestFixture }, }); }); - it('should call onGetComponentDocumentation handler when tool is invoked', async () => { - const onGetComponentDocumentation = vi.fn(); - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(smallManifestFixture)); + it('should call onGetDocumentation handler when tool is invoked', async () => { + const onGetDocumentation = vi.fn(); + const manifestProvider = createManifestProviderMock(); const handler = await createStorybookMcpHandler({ manifestProvider, - onGetComponentDocumentation, + onGetDocumentation, }); await setupClient(handler); const result = await client.callTool({ - name: 'get-component-documentation', + name: 'get-documentation', arguments: { - componentId: 'button', + id: 'button', }, }); - expect(onGetComponentDocumentation).toHaveBeenCalledTimes(1); - expect(onGetComponentDocumentation).toHaveBeenCalledWith({ + expect(onGetDocumentation).toHaveBeenCalledTimes(1); + expect(onGetDocumentation).toHaveBeenCalledWith({ context: expect.objectContaining({ request: expect.any(Request), }), - input: { componentId: 'button' }, - foundComponent: expect.objectContaining({ + input: { id: 'button' }, + foundDocumentation: expect.objectContaining({ id: 'button', name: 'Button', }), @@ -184,7 +208,7 @@ describe('createStorybookMcpHandler', () => { await setupClient(handler); const result = await client.callTool({ - name: 'list-all-components', + name: 'list-all-documentation', arguments: {}, }); @@ -195,38 +219,130 @@ describe('createStorybookMcpHandler', () => { }); }); - it('should handle non-existent component ID in get-component-documentation', async () => { - const onGetComponentDocumentation = vi.fn(); - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(smallManifestFixture)); + it('should handle non-existent component ID in get-documentation', async () => { + const onGetDocumentation = vi.fn(); + const manifestProvider = createManifestProviderMock(); const handler = await createStorybookMcpHandler({ manifestProvider, - onGetComponentDocumentation, + onGetDocumentation, }); await setupClient(handler); const result = await client.callTool({ - name: 'get-component-documentation', + name: 'get-documentation', arguments: { - componentId: 'non-existent', + id: 'non-existent', }, }); // Should still call the handler - expect(onGetComponentDocumentation).toHaveBeenCalledTimes(1); - expect(onGetComponentDocumentation).toHaveBeenCalledWith({ + expect(onGetDocumentation).toHaveBeenCalledTimes(1); + expect(onGetDocumentation).toHaveBeenCalledWith({ context: expect.objectContaining({ request: expect.any(Request), }), - input: { componentId: 'non-existent' }, + input: { id: 'non-existent' }, }); expect(result.content).toHaveLength(1); expect((result.content as any)[0]).toMatchObject({ type: 'text', - text: expect.stringContaining('Component not found'), + text: expect.stringContaining('not found'), + }); + }); + + describe('with docs manifest', () => { + it('should return docs entries in list-all-documentation when docs manifest is available', async () => { + const manifestProvider = createManifestProviderMockWithDocs(); + + const handler = await createStorybookMcpHandler({ + manifestProvider, + }); + await setupClient(handler); + + const result = await client.callTool({ + name: 'list-all-documentation', + arguments: {}, + }); + + expect(manifestProvider).toHaveBeenCalledWith( + expect.any(Request), + './manifests/components.json', + ); + expect(manifestProvider).toHaveBeenCalledWith( + expect.any(Request), + './manifests/docs.json', + ); + + expect(result.content).toHaveLength(1); + const text = (result.content as any)[0].text; + expect(text).toContain('# Components'); + expect(text).toContain('# Docs'); + expect(text).toContain('Getting Started'); + }); + + it('should include docs manifest in onListAllDocumentation handler', async () => { + const onListAllDocumentation = vi.fn(); + const manifestProvider = createManifestProviderMockWithDocs(); + + const handler = await createStorybookMcpHandler({ + manifestProvider, + onListAllDocumentation, + }); + await setupClient(handler); + + await client.callTool({ + name: 'list-all-documentation', + arguments: {}, + }); + + expect(onListAllDocumentation).toHaveBeenCalledTimes(1); + expect(onListAllDocumentation).toHaveBeenCalledWith({ + context: expect.objectContaining({ + request: expect.any(Request), + }), + manifests: { + componentManifest: smallManifestFixture, + docsManifest: smallDocsManifestFixture, + }, + }); + }); + + it('should return documentation for a docs entry', async () => { + const onGetDocumentation = vi.fn(); + const manifestProvider = createManifestProviderMockWithDocs(); + + const handler = await createStorybookMcpHandler({ + manifestProvider, + onGetDocumentation, + }); + await setupClient(handler); + + const result = await client.callTool({ + name: 'get-documentation', + arguments: { + id: 'getting-started', + }, + }); + + expect(onGetDocumentation).toHaveBeenCalledTimes(1); + expect(onGetDocumentation).toHaveBeenCalledWith({ + context: expect.objectContaining({ + request: expect.any(Request), + }), + input: { id: 'getting-started' }, + foundDocumentation: expect.objectContaining({ + id: 'getting-started', + name: 'Getting Started', + }), + }); + + expect(result.content).toHaveLength(1); + expect((result.content as any)[0]).toMatchObject({ + type: 'text', + text: expect.stringContaining('Getting Started'), + }); }); }); }); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index c81a4280..786de842 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -2,22 +2,25 @@ import { McpServer } from 'tmcp'; import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot'; import { HttpTransport } from '@tmcp/transport-http'; import pkgJson from '../package.json' with { type: 'json' }; -import { addListAllComponentsTool } from './tools/list-all-components.ts'; -import { addGetComponentDocumentationTool } from './tools/get-component-documentation.ts'; +import { addListAllDocumentationTool } from './tools/list-all-documentation.ts'; +import { addGetDocumentationTool } from './tools/get-documentation.ts'; import type { StorybookContext } from './types.ts'; // Export tools for reuse by addon-mcp export { - addListAllComponentsTool, + addListAllDocumentationTool, LIST_TOOL_NAME, -} from './tools/list-all-components.ts'; +} from './tools/list-all-documentation.ts'; export { - addGetComponentDocumentationTool, + addGetDocumentationTool, GET_TOOL_NAME, -} from './tools/get-component-documentation.ts'; +} from './tools/get-documentation.ts'; // Export manifest constants -export { MANIFEST_PATH } from './utils/get-manifest.ts'; +export { + COMPONENT_MANIFEST_PATH, + DOCS_MANIFEST_PATH, +} from './utils/get-manifest.ts'; // Export types for reuse export type { StorybookContext } from './types.ts'; @@ -90,8 +93,8 @@ export const createStorybookMcpHandler = async ( server.on('initialize', options.onSessionInitialize); } - await addListAllComponentsTool(server); - await addGetComponentDocumentationTool(server); + await addListAllDocumentationTool(server); + await addGetDocumentationTool(server); const transport = new HttpTransport(server, { path: null }); @@ -100,11 +103,10 @@ export const createStorybookMcpHandler = async ( request: req, format: context?.format ?? options.format ?? 'markdown', manifestProvider: context?.manifestProvider ?? options.manifestProvider, - onListAllComponents: - context?.onListAllComponents ?? options.onListAllComponents, - onGetComponentDocumentation: - context?.onGetComponentDocumentation ?? - options.onGetComponentDocumentation, + onListAllDocumentation: + context?.onListAllDocumentation ?? options.onListAllDocumentation, + onGetDocumentation: + context?.onGetDocumentation ?? options.onGetDocumentation, }); }) as Handler; }; diff --git a/packages/mcp/src/tools/get-component-documentation.test.ts b/packages/mcp/src/tools/get-component-documentation.test.ts index c221393f..3e93e740 100644 --- a/packages/mcp/src/tools/get-component-documentation.test.ts +++ b/packages/mcp/src/tools/get-component-documentation.test.ts @@ -1,17 +1,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { McpServer } from 'tmcp'; import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot'; -import { - addGetComponentDocumentationTool, - GET_TOOL_NAME, -} from './get-component-documentation.ts'; +import { addGetDocumentationTool, GET_TOOL_NAME } from './get-documentation.ts'; import type { StorybookContext } from '../types.ts'; import smallManifestFixture from '../../fixtures/small-manifest.fixture.json' with { type: 'json' }; import * as getManifest from '../utils/get-manifest.ts'; -describe('getComponentDocumentationTool', () => { +describe('getDocumentationTool', () => { let server: McpServer; - let getManifestSpy: any; + let getManifestsSpy: any; beforeEach(async () => { const adapter = new ValibotJsonSchemaAdapter(); @@ -43,11 +40,13 @@ describe('getComponentDocumentationTool', () => { }, { sessionId: 'test-session' }, ); - await addGetComponentDocumentationTool(server); + await addGetDocumentationTool(server); - // Mock getManifest to return the fixture - getManifestSpy = vi.spyOn(getManifest, 'getManifest'); - getManifestSpy.mockResolvedValue(smallManifestFixture); + // Mock getManifests to return the fixture + getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + }); }); it('should return formatted documentation for a single component', async () => { @@ -58,7 +57,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'button', + id: 'button', }, }, }; @@ -100,7 +99,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'nonexistent', + id: 'nonexistent', }, }, }; @@ -114,7 +113,7 @@ describe('getComponentDocumentationTool', () => { { "content": [ { - "text": "Component not found: "nonexistent". Use the list-all-components tool to see available components.", + "text": "Component or Docs Entry not found: "nonexistent". Use the list-all-documentation tool to see available components and documentation entries.", "type": "text", }, ], @@ -124,7 +123,7 @@ describe('getComponentDocumentationTool', () => { }); it('should handle fetch errors gracefully', async () => { - getManifestSpy.mockRejectedValue( + getManifestsSpy.mockRejectedValue( new getManifest.ManifestGetError( 'Failed to fetch manifest: 404 Not Found', 'https://example.com/manifest.json', @@ -138,7 +137,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'button', + id: 'button', }, }, }; @@ -161,7 +160,7 @@ describe('getComponentDocumentationTool', () => { `); }); - it('should call onGetComponentDocumentation handler when provided', async () => { + it('should call onGetDocumentation handler when provided', async () => { const handler = vi.fn(); const request = { @@ -171,7 +170,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'button', + id: 'button', }, }, }; @@ -181,7 +180,7 @@ describe('getComponentDocumentationTool', () => { await server.receive(request, { custom: { request: mockHttpRequest, - onGetComponentDocumentation: handler, + onGetDocumentation: handler, }, }); @@ -189,10 +188,13 @@ describe('getComponentDocumentationTool', () => { expect(handler).toHaveBeenCalledWith({ context: expect.objectContaining({ request: mockHttpRequest, - onGetComponentDocumentation: handler, + onGetDocumentation: handler, + }), + input: { id: 'button' }, + foundDocumentation: expect.objectContaining({ + id: 'button', + name: 'Button', }), - input: { componentId: 'button' }, - foundComponent: expect.objectContaining({ id: 'button', name: 'Button' }), }); }); @@ -232,7 +234,9 @@ describe('getComponentDocumentationTool', () => { }, }; - getManifestSpy.mockResolvedValue(manifestWithReactDocgen); + getManifestsSpy.mockResolvedValue({ + componentManifest: manifestWithReactDocgen, + }); const request = { jsonrpc: '2.0' as const, @@ -241,7 +245,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'button', + id: 'button', }, }, }; @@ -290,7 +294,7 @@ describe('getComponentDocumentationTool', () => { params: { name: GET_TOOL_NAME, arguments: { - componentId: 'button', + id: 'button', }, }, }; diff --git a/packages/mcp/src/tools/get-component-documentation.ts b/packages/mcp/src/tools/get-component-documentation.ts deleted file mode 100644 index 198c5769..00000000 --- a/packages/mcp/src/tools/get-component-documentation.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as v from 'valibot'; -import type { McpServer } from 'tmcp'; -import type { StorybookContext } from '../types.ts'; -import { getManifest, errorToMCPContent } from '../utils/get-manifest.ts'; -import { formatComponentManifest } from '../utils/format-manifest.ts'; - -export const GET_TOOL_NAME = 'get-component-documentation'; - -const GetComponentDocumentationInput = v.object({ - componentId: v.string(), -}); - -type GetComponentDocumentationInput = v.InferOutput< - typeof GetComponentDocumentationInput ->; - -export async function addGetComponentDocumentationTool( - server: McpServer, - enabled?: Parameters['tool']>[0]['enabled'], -) { - server.tool( - { - name: GET_TOOL_NAME, - title: 'Get Documentation for Component', - description: 'Get detailed documentation for a specific UI component', - schema: GetComponentDocumentationInput, - enabled, - }, - async (input: GetComponentDocumentationInput) => { - try { - const manifest = await getManifest( - server.ctx.custom?.request, - server.ctx.custom?.manifestProvider, - ); - - const component = manifest.components[input.componentId]; - - if (!component) { - await server.ctx.custom?.onGetComponentDocumentation?.({ - context: server.ctx.custom, - input: { componentId: input.componentId }, - }); - - return { - content: [ - { - type: 'text' as const, - text: `Component not found: "${input.componentId}". Use the list-all-components tool to see available components.`, - }, - ], - isError: true, - }; - } - - await server.ctx.custom?.onGetComponentDocumentation?.({ - context: server.ctx.custom, - input: { componentId: input.componentId }, - foundComponent: component, - }); - - const format = server.ctx.custom?.format ?? 'markdown'; - return { - content: [ - { - type: 'text' as const, - text: formatComponentManifest(component, format), - }, - ], - }; - } catch (error) { - return errorToMCPContent(error); - } - }, - ); -} diff --git a/packages/mcp/src/tools/get-documentation.test.ts b/packages/mcp/src/tools/get-documentation.test.ts new file mode 100644 index 00000000..313bd16f --- /dev/null +++ b/packages/mcp/src/tools/get-documentation.test.ts @@ -0,0 +1,508 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpServer } from 'tmcp'; +import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot'; +import { addGetDocumentationTool, GET_TOOL_NAME } from './get-documentation.ts'; +import type { StorybookContext } from '../types.ts'; +import smallManifestFixture from '../../fixtures/small-manifest.fixture.json' with { type: 'json' }; +import smallDocsManifestFixture from '../../fixtures/small-docs-manifest.fixture.json' with { type: 'json' }; +import * as getManifest from '../utils/get-manifest.ts'; + +describe('getDocumentationTool', () => { + let server: McpServer; + let getManifestsSpy: any; + + beforeEach(async () => { + const adapter = new ValibotJsonSchemaAdapter(); + server = new McpServer( + { + name: 'test-server', + version: '1.0.0', + description: 'Test server for get tool', + }, + { + adapter, + capabilities: { + tools: { listChanged: true }, + }, + }, + ).withContext(); + + // initialize test session + await server.receive( + { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 'test', version: '1.0.0' }, + }, + }, + { sessionId: 'test-session' }, + ); + await addGetDocumentationTool(server); + + // Mock getManifests to return the fixture + getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + }); + }); + + it('should return formatted documentation for a single component', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Button + + ID: button + + ## Stories + + ### Primary + + The primary button variant. + + \`\`\` + const Primary = () => + \`\`\`", + "type": "text", + }, + ], + } + `); + }); + + it('should return an error when a component is not found', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'nonexistent', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "Component or Docs Entry not found: "nonexistent". Use the list-all-documentation tool to see available components and documentation entries.", + "type": "text", + }, + ], + "isError": true, + } + `); + }); + + it('should handle fetch errors gracefully', async () => { + getManifestsSpy.mockRejectedValue( + new getManifest.ManifestGetError( + 'Failed to fetch manifest: 404 Not Found', + 'https://example.com/manifest.json', + ), + ); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "Error getting manifest: Failed to fetch manifest: 404 Not Found", + "type": "text", + }, + ], + "isError": true, + } + `); + }); + + it('should call onGetDocumentation handler when provided', async () => { + const handler = vi.fn(); + + const request = { + jsonrpc: '2.0' as const, + id: 2, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + // Pass the handler and request in the context for this specific request + await server.receive(request, { + custom: { + request: mockHttpRequest, + onGetDocumentation: handler, + }, + }); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith({ + context: expect.objectContaining({ + request: mockHttpRequest, + onGetDocumentation: handler, + }), + input: { id: 'button' }, + foundDocumentation: expect.objectContaining({ + id: 'button', + name: 'Button', + }), + }); + }); + + it('should include props section when reactDocgen is present', async () => { + const manifestWithReactDocgen = { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + description: 'A button component', + reactDocgen: { + props: { + variant: { + description: 'Button style variant', + required: false, + defaultValue: { value: '"primary"', computed: false }, + tsType: { + name: 'union', + raw: '"primary" | "secondary"', + elements: [ + { name: 'literal', value: '"primary"' }, + { name: 'literal', value: '"secondary"' }, + ], + }, + }, + disabled: { + description: 'Disable the button', + required: false, + tsType: { + name: 'boolean', + }, + }, + }, + }, + }, + }, + }; + + getManifestsSpy.mockResolvedValue({ + componentManifest: manifestWithReactDocgen, + }); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Button + + ID: button + + A button component + + ## Props + + \`\`\` + export type Props = { + /** + Button style variant + */ + variant?: "primary" | "secondary" = "primary"; + /** + Disable the button + */ + disabled?: boolean; + } + \`\`\`", + "type": "text", + }, + ], + } + `); + }); + + it('should format component as XML when format is "xml"', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest, format: 'xml' as const }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": " + button + Button + + Primary + + The primary button variant. + + + const Primary = () => + + + ", + "type": "text", + }, + ], + } + `); + }); + + describe('docs manifest entries', () => { + beforeEach(() => { + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + docsManifest: smallDocsManifestFixture, + }); + }); + + it('should return formatted documentation for a docs entry', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'getting-started', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Getting Started Guide + + # Getting Started + + Welcome to the component library. This guide will help you get up and running. + + ## Installation + + \`\`\`bash + npm install my-component-library + \`\`\` + + ## Usage + + Import components and use them in your application.", + "type": "text", + }, + ], + } + `); + }); + + it('should return component documentation when id matches both component and docs entry', async () => { + // When an ID exists in both manifests, prefer component documentation + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + docsManifest: { + v: 1, + docs: { + button: { + id: 'button', + name: 'Button Docs', + title: 'Button Documentation', + path: 'docs/button.mdx', + content: 'This is the button docs entry', + }, + }, + }, + }); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + // Should return the component, not the docs entry + expect((response.result as any).content[0].text).toContain('## Stories'); + expect((response.result as any).content[0].text).toContain('Primary'); + }); + + it('should format docs entry as XML when format is "xml"', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'getting-started', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest, format: 'xml' as const }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": " + Getting Started Guide + + # Getting Started + + Welcome to the component library. This guide will help you get up and running. + + ## Installation + + \`\`\`bash + npm install my-component-library + \`\`\` + + ## Usage + + Import components and use them in your application. + + ", + "type": "text", + }, + ], + } + `); + }); + + it('should call onGetDocumentation handler with docs entry when found', async () => { + const handler = vi.fn(); + + const request = { + jsonrpc: '2.0' as const, + id: 2, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'getting-started', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + await server.receive(request, { + custom: { + request: mockHttpRequest, + onGetDocumentation: handler, + }, + }); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith({ + context: expect.objectContaining({ + request: mockHttpRequest, + onGetDocumentation: handler, + }), + input: { id: 'getting-started' }, + foundDocumentation: expect.objectContaining({ + id: 'getting-started', + name: 'Getting Started', + }), + }); + }); + }); +}); diff --git a/packages/mcp/src/tools/get-documentation.ts b/packages/mcp/src/tools/get-documentation.ts new file mode 100644 index 00000000..88be5267 --- /dev/null +++ b/packages/mcp/src/tools/get-documentation.ts @@ -0,0 +1,84 @@ +import * as v from 'valibot'; +import type { McpServer } from 'tmcp'; +import type { ComponentManifest, Doc, StorybookContext } from '../types.ts'; +import { getManifests, errorToMCPContent } from '../utils/get-manifest.ts'; +import { + formatComponentManifest, + formatDocsManifest, +} from '../utils/format-manifest.ts'; +import { LIST_TOOL_NAME } from './list-all-documentation.ts'; + +export const GET_TOOL_NAME = 'get-documentation'; + +const GetDocumentationInput = v.object({ + id: v.string(), +}); + +export async function addGetDocumentationTool( + server: McpServer, + enabled?: Parameters['tool']>[0]['enabled'], +) { + server.tool( + { + name: GET_TOOL_NAME, + title: 'Get Documentation', + description: + 'Get detailed documentation for a specific UI component or docs entry', + schema: GetDocumentationInput, + enabled, + }, + async (input: v.InferOutput) => { + try { + const { componentManifest, docsManifest } = await getManifests( + server.ctx.custom?.request, + server.ctx.custom?.manifestProvider, + ); + + const component = componentManifest.components[input.id]; + const docsEntry = docsManifest?.docs[input.id]; + + if (!component && !docsEntry) { + await server.ctx.custom?.onGetDocumentation?.({ + context: server.ctx.custom, + input, + }); + + return { + content: [ + { + type: 'text' as const, + text: `Component or Docs Entry not found: "${input.id}". Use the ${LIST_TOOL_NAME} tool to see available components and documentation entries.`, + }, + ], + isError: true, + }; + } + + const documentation = component ?? docsEntry; + + await server.ctx.custom?.onGetDocumentation?.({ + context: server.ctx.custom, + input, + foundDocumentation: documentation, + }); + + const format = server.ctx.custom?.format ?? 'markdown'; + return { + content: [ + { + type: 'text' as const, + text: component + ? formatComponentManifest( + documentation as ComponentManifest, + format, + ) + : formatDocsManifest(documentation as Doc, format), + }, + ], + }; + } catch (error) { + return errorToMCPContent(error); + } + }, + ); +} diff --git a/packages/mcp/src/tools/list-all-components.test.ts b/packages/mcp/src/tools/list-all-documentation.test.ts similarity index 52% rename from packages/mcp/src/tools/list-all-components.test.ts rename to packages/mcp/src/tools/list-all-documentation.test.ts index 22ab0e48..ab80f9c8 100644 --- a/packages/mcp/src/tools/list-all-components.test.ts +++ b/packages/mcp/src/tools/list-all-documentation.test.ts @@ -2,16 +2,17 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { McpServer } from 'tmcp'; import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot'; import { - addListAllComponentsTool, + addListAllDocumentationTool, LIST_TOOL_NAME, -} from './list-all-components.ts'; +} from './list-all-documentation.ts'; import type { StorybookContext } from '../types.ts'; import smallManifestFixture from '../../fixtures/small-manifest.fixture.json' with { type: 'json' }; +import smallDocsManifestFixture from '../../fixtures/small-docs-manifest.fixture.json' with { type: 'json' }; import * as getManifest from '../utils/get-manifest.ts'; -describe('listAllComponentsTool', () => { +describe('listAllDocumentationTool', () => { let server: McpServer; - let getManifestSpy: any; + let getManifestsSpy: any; beforeEach(async () => { const adapter = new ValibotJsonSchemaAdapter(); @@ -43,11 +44,13 @@ describe('listAllComponentsTool', () => { }, { sessionId: 'test-session' }, ); - await addListAllComponentsTool(server); + await addListAllDocumentationTool(server); - // Mock getManifest to return the fixture - getManifestSpy = vi.spyOn(getManifest, 'getManifest'); - getManifestSpy.mockResolvedValue(smallManifestFixture); + // Mock getManifests to return the fixture + getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + }); }); it('should return a list of all components', async () => { @@ -83,7 +86,7 @@ describe('listAllComponentsTool', () => { }); it('should handle fetch errors gracefully', async () => { - getManifestSpy.mockRejectedValue( + getManifestsSpy.mockRejectedValue( new getManifest.ManifestGetError( 'Failed to fetch manifest: 404 Not Found', 'https://example.com/manifest.json', @@ -119,7 +122,7 @@ describe('listAllComponentsTool', () => { }); it('should handle unexpected errors gracefully', async () => { - getManifestSpy.mockRejectedValue(new Error('Network timeout')); + getManifestsSpy.mockRejectedValue(new Error('Network timeout')); const request = { jsonrpc: '2.0' as const, @@ -149,7 +152,7 @@ describe('listAllComponentsTool', () => { `); }); - it('should call onListAllComponents handler when provided', async () => { + it('should call onListAllDocumentation handler when provided', async () => { const handler = vi.fn(); const request = { @@ -165,16 +168,18 @@ describe('listAllComponentsTool', () => { const mockHttpRequest = new Request('https://example.com/mcp'); // Pass the handler and request in the context for this specific request await server.receive(request, { - custom: { request: mockHttpRequest, onListAllComponents: handler }, + custom: { request: mockHttpRequest, onListAllDocumentation: handler }, }); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith({ context: expect.objectContaining({ request: mockHttpRequest, - onListAllComponents: handler, + onListAllDocumentation: handler, }), - manifest: smallManifestFixture, + manifests: { + componentManifest: smallManifestFixture, + }, }); }); @@ -227,4 +232,147 @@ describe('listAllComponentsTool', () => { } `); }); + + describe('with docs manifest', () => { + beforeEach(() => { + getManifestsSpy.mockResolvedValue({ + componentManifest: smallManifestFixture, + docsManifest: smallDocsManifestFixture, + }); + }); + + it('should return both components and docs entries', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: LIST_TOOL_NAME, + arguments: {}, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Components + + - Button (button): A simple button component + - Card (card): A container component for grouping related content. + - Input (input): A text input component with validation support. + + # Docs + + - Getting Started Guide (getting-started): # Getting Started Welcome to the component library. This guide will help you get up and ru... + - Theming and Customization (theming): # Theming Learn how to customize the look and feel of components using our theming system....", + "type": "text", + }, + ], + } + `); + }); + + it('should format both components and docs entries as XML when format is "xml"', async () => { + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: LIST_TOOL_NAME, + arguments: {}, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest, format: 'xml' as const }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": " + + button + Button + + A simple button component + + + + card + Card + + A container component for grouping related content. + + + + input + Input + + A text input component with validation support. + + + + + + getting-started + Getting Started Guide + + # Getting Started Welcome to the component library. This guide will help you get up and ru... + + + + theming + Theming and Customization + + # Theming Learn how to customize the look and feel of components using our theming system.... + + + ", + "type": "text", + }, + ], + } + `); + }); + + it('should include docs manifest in onListAllDocumentation handler call', async () => { + const handler = vi.fn(); + + const request = { + jsonrpc: '2.0' as const, + id: 2, + method: 'tools/call', + params: { + name: LIST_TOOL_NAME, + arguments: {}, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + await server.receive(request, { + custom: { request: mockHttpRequest, onListAllDocumentation: handler }, + }); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith({ + context: expect.objectContaining({ + request: mockHttpRequest, + onListAllDocumentation: handler, + }), + manifests: { + componentManifest: smallManifestFixture, + docsManifest: smallDocsManifestFixture, + }, + }); + }); + }); }); diff --git a/packages/mcp/src/tools/list-all-components.ts b/packages/mcp/src/tools/list-all-documentation.ts similarity index 53% rename from packages/mcp/src/tools/list-all-components.ts rename to packages/mcp/src/tools/list-all-documentation.ts index faa48d85..c2c613a9 100644 --- a/packages/mcp/src/tools/list-all-components.ts +++ b/packages/mcp/src/tools/list-all-documentation.ts @@ -1,45 +1,42 @@ import type { McpServer } from 'tmcp'; import type { StorybookContext } from '../types.ts'; -import { getManifest, errorToMCPContent } from '../utils/get-manifest.ts'; -import { formatComponentManifestMapToList } from '../utils/format-manifest.ts'; +import { getManifests, errorToMCPContent } from '../utils/get-manifest.ts'; +import { formatManifestsToLists } from '../utils/format-manifest.ts'; -export const LIST_TOOL_NAME = 'list-all-components'; +export const LIST_TOOL_NAME = 'list-all-documentation'; -export async function addListAllComponentsTool( +export async function addListAllDocumentationTool( server: McpServer, enabled?: Parameters['tool']>[0]['enabled'], ) { server.tool( { name: LIST_TOOL_NAME, - title: 'List All Components', + title: 'List All Documentation', description: - 'List all available UI components from the component library', + 'List all available UI components and documentation entries from the Storybook', enabled, }, async () => { try { - const manifest = await getManifest( + const manifests = await getManifests( server.ctx.custom?.request, server.ctx.custom?.manifestProvider, ); const format = server.ctx.custom?.format ?? 'markdown'; - const componentList = formatComponentManifestMapToList( - manifest, - format, - ); + const lists = formatManifestsToLists(manifests, format); - await server.ctx.custom?.onListAllComponents?.({ + await server.ctx.custom?.onListAllDocumentation?.({ context: server.ctx.custom, - manifest, + manifests, }); return { content: [ { type: 'text', - text: componentList, + text: lists, }, ], }; diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 5895e3ab..d214b06e 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -34,36 +34,36 @@ export type StorybookContext = { path: string, ) => Promise; /** - * Optional handler called when list-all-components tool is invoked. + * Optional handler called when list-all-documentation tool is invoked. * Receives the context and the component manifest. */ - onListAllComponents?: (params: { + onListAllDocumentation?: (params: { context: StorybookContext; - manifest: ComponentManifestMap; + manifests: AllManifests; }) => void | Promise; /** * Optional handler called when get-component-documentation tool is invoked. * Receives the context, input parameters, and the found component (if any). */ - onGetComponentDocumentation?: (params: { + onGetDocumentation?: (params: { context: StorybookContext; - input: { componentId: string }; - foundComponent?: ComponentManifest; + input: { id: string }; + foundDocumentation?: ComponentManifest | Doc; }) => void | Promise; }; const JSDocTag = v.record(v.string(), v.array(v.string())); +const Error = v.object({ + name: v.string(), + message: v.string(), +}); + const BaseManifest = v.object({ name: v.string(), description: v.optional(v.string()), jsDocTags: v.optional(JSDocTag), - error: v.optional( - v.object({ - name: v.string(), - message: v.string(), - }), - ), + error: v.optional(Error), }); const Story = v.object({ @@ -71,6 +71,21 @@ const Story = v.object({ snippet: v.optional(v.string()), }); +/** + * A docs entry represents MDX documentation that can be attached to a component + * or standalone (unattached). + */ +const Doc = v.object({ + id: v.string(), + name: v.string(), + title: v.string(), + path: v.string(), + content: v.string(), + summary: v.optional(v.string()), + error: v.optional(Error), +}); +export type Doc = v.InferOutput; + export const ComponentManifest = v.object({ ...BaseManifest.entries, id: v.string(), @@ -80,6 +95,7 @@ export const ComponentManifest = v.object({ stories: v.optional(v.array(Story)), // loose schema for react-docgen types, as they are pretty complex reactDocgen: v.optional(v.custom(() => true)), + docs: v.optional(v.record(v.string(), Doc)), }); export type ComponentManifest = v.InferOutput; @@ -88,3 +104,18 @@ export const ComponentManifestMap = v.object({ components: v.record(v.string(), ComponentManifest), }); export type ComponentManifestMap = v.InferOutput; + +/** + * Manifest for unattached/standalone documentation entries. + * Served at /manifests/docs.json + */ +export const DocsManifestMap = v.object({ + v: v.number(), + docs: v.record(v.string(), Doc), +}); +export type DocsManifestMap = v.InferOutput; + +export type AllManifests = { + componentManifest: ComponentManifestMap; + docsManifest?: DocsManifestMap; +}; diff --git a/packages/mcp/src/utils/format-manifest.test.ts b/packages/mcp/src/utils/format-manifest.test.ts index df983e19..cd9bf713 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { formatComponentManifest, - formatComponentManifestMapToList, + formatManifestsToLists, } from './format-manifest'; -import type { ComponentManifest, ComponentManifestMap } from '../types'; +import type { AllManifests, ComponentManifest } from '../types'; describe('formatComponentManifest', () => { const manifest: ComponentManifest = { @@ -32,32 +32,34 @@ describe('formatComponentManifest', () => { }); }); -describe('formatComponentManifestMapToList', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', +describe('formatManifestsToLists', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, }, }, }; it('should use markdown formatter by default', () => { - const result = formatComponentManifestMapToList(manifest); + const result = formatManifestsToLists(manifests); expect(result).toContain('# Components'); expect(result).toContain('- Button (button)'); }); it('should use markdown formatter when format is "markdown"', () => { - const result = formatComponentManifestMapToList(manifest, 'markdown'); + const result = formatManifestsToLists(manifests, 'markdown'); expect(result).toContain('# Components'); expect(result).toContain('- Button (button)'); }); it('should use xml formatter when format is "xml"', () => { - const result = formatComponentManifestMapToList(manifest, 'xml'); + const result = formatManifestsToLists(manifests, 'xml'); expect(result).toContain(''); expect(result).toContain('Button'); expect(result).toContain('button'); diff --git a/packages/mcp/src/utils/format-manifest.ts b/packages/mcp/src/utils/format-manifest.ts index 4f60ce84..09bca1de 100644 --- a/packages/mcp/src/utils/format-manifest.ts +++ b/packages/mcp/src/utils/format-manifest.ts @@ -1,6 +1,7 @@ import type { + AllManifests, ComponentManifest, - ComponentManifestMap, + Doc, OutputFormat, } from '../types.ts'; import type { ManifestFormatter } from './manifest-formatter/types.ts'; @@ -14,9 +15,6 @@ const formatters: Record = { /** * Format a single component manifest. - * @param componentManifest - The component manifest to format - * @param format - The desired output format (defaults to 'markdown') - * @returns Formatted string representation of the component */ export function formatComponentManifest( componentManifest: ComponentManifest, @@ -26,14 +24,21 @@ export function formatComponentManifest( } /** - * Format a component manifest map into a list. - * @param manifest - The component manifest map to format - * @param format - The desired output format (defaults to 'markdown') - * @returns Formatted string representation of the component list + * Format a single docs manifest. */ -export function formatComponentManifestMapToList( - manifest: ComponentManifestMap, +export function formatDocsManifest( + doc: Doc, format: OutputFormat = 'markdown', ): string { - return formatters[format].formatComponentManifestMapToList(manifest); + return formatters[format].formatDocsManifest(doc); +} + +/** + * Format a component manifest and optionally a docs manifest into lists. + */ +export function formatManifestsToLists( + manifests: AllManifests, + format: OutputFormat = 'markdown', +): string { + return formatters[format].formatManifestsToLists(manifests); } diff --git a/packages/mcp/src/utils/get-manifest.test.ts b/packages/mcp/src/utils/get-manifest.test.ts index c13b5bfa..b74cada3 100644 --- a/packages/mcp/src/utils/get-manifest.test.ts +++ b/packages/mcp/src/utils/get-manifest.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { getManifest, ManifestGetError } from './get-manifest'; -import type { ComponentManifestMap } from '../types'; +import { getManifests, ManifestGetError } from './get-manifest.ts'; +import type { ComponentManifestMap, DocsManifestMap } from '../types.ts'; global.fetch = vi.fn(); @@ -13,6 +13,92 @@ function createMockRequest(url: string): Request { }); } +/** + * Helper to create a successful JSON fetch response + */ +function createJsonResponse(data: unknown) { + return { + ok: true, + headers: { + get: vi.fn().mockReturnValue('application/json'), + }, + text: vi.fn().mockResolvedValue(JSON.stringify(data)), + }; +} + +/** + * Helper to create a 404 response + */ +function create404Response() { + return { + ok: false, + status: 404, + statusText: 'Not Found', + }; +} + +/** + * Helper to create a fetch mock that returns different responses based on URL + */ +function createFetchMock(responses: { + components?: unknown | Error; + docs?: unknown | Error; +}) { + return vi.fn().mockImplementation((url: string) => { + if (url.includes('components.json')) { + if (responses.components instanceof Error) { + return Promise.reject(responses.components); + } + return Promise.resolve( + responses.components !== undefined + ? createJsonResponse(responses.components) + : create404Response(), + ); + } + if (url.includes('docs.json')) { + if (responses.docs instanceof Error) { + return Promise.reject(responses.docs); + } + return Promise.resolve( + responses.docs !== undefined + ? createJsonResponse(responses.docs) + : create404Response(), + ); + } + return Promise.resolve(create404Response()); + }); +} + +/** + * Helper to create a manifestProvider mock that returns different responses based on path + */ +function createManifestProviderMock(responses: { + components?: string | Error; + docs?: string | Error; +}) { + return vi + .fn() + .mockImplementation((_request: Request | undefined, path: string) => { + if (path.includes('components.json')) { + if (responses.components instanceof Error) { + return Promise.reject(responses.components); + } + return responses.components !== undefined + ? Promise.resolve(responses.components) + : Promise.reject(new Error('Components not found')); + } + if (path.includes('docs.json')) { + if (responses.docs instanceof Error) { + return Promise.reject(responses.docs); + } + return responses.docs !== undefined + ? Promise.resolve(responses.docs) + : Promise.reject(new Error('Docs not found')); + } + return Promise.reject(new Error('Unknown path')); + }); +} + describe('getManifest', () => { beforeEach(() => { // Reset the fetch mock between tests since we're checking call counts @@ -21,14 +107,14 @@ describe('getManifest', () => { describe('error cases', () => { it('should throw ManifestGetError when request is not provided and using default provider', async () => { - await expect(getManifest()).rejects.toThrow(ManifestGetError); - await expect(getManifest()).rejects.toThrow( + await expect(getManifests()).rejects.toThrow(ManifestGetError); + await expect(getManifests()).rejects.toThrow( "You must either pass the original request forward to the server context, or set a custom manifestProvider that doesn't need the request", ); }); it('should throw ManifestGetError when request is undefined and using default provider', async () => { - await expect(getManifest(undefined)).rejects.toThrow(ManifestGetError); - await expect(getManifest(undefined)).rejects.toThrow( + await expect(getManifests(undefined)).rejects.toThrow(ManifestGetError); + await expect(getManifests(undefined)).rejects.toThrow( "You must either pass the original request forward to the server context, or set a custom manifestProvider that doesn't need the request", ); }); @@ -40,8 +126,8 @@ describe('getManifest', () => { }); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow(ManifestGetError); - await expect(getManifest(request)).rejects.toThrow( + await expect(getManifests(request)).rejects.toThrow(ManifestGetError); + await expect(getManifests(request)).rejects.toThrow( 'Failed to fetch manifest: 404 Not Found', ); }); @@ -54,7 +140,7 @@ describe('getManifest', () => { }); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow( + await expect(getManifests(request)).rejects.toThrow( 'Failed to fetch manifest: 500 Internal Server Error', ); }); @@ -68,25 +154,30 @@ describe('getManifest', () => { }); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow(ManifestGetError); - await expect(getManifest(request)).rejects.toThrow( + await expect(getManifests(request)).rejects.toThrow(ManifestGetError); + await expect(getManifests(request)).rejects.toThrow( 'Invalid content type: expected application/json, got text/html', ); }); it('should throw ManifestGetError when response is not valid JSON', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: vi.fn().mockReturnValue('application/json'), - }, - text: vi.fn().mockResolvedValue('not valid json{'), + global.fetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('components.json')) { + return Promise.resolve({ + ok: true, + headers: { + get: vi.fn().mockReturnValue('application/json'), + }, + text: vi.fn().mockResolvedValue('not valid json{'), + }); + } + return Promise.resolve(create404Response()); }); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow(ManifestGetError); - await expect(getManifest(request)).rejects.toThrow( - 'Failed to get manifest:', + await expect(getManifests(request)).rejects.toThrow(ManifestGetError); + await expect(getManifests(request)).rejects.toThrow( + 'Failed to parse component manifest:', ); }); @@ -105,11 +196,9 @@ describe('getManifest', () => { }); const request = createMockRequest('https://example.com/mcp'); - await expect( - getManifest(request), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[ManifestGetError: Failed to get manifest: Invalid key: Expected "v" but received undefined]`, - ); + await expect(getManifests(request)).rejects + .toThrowErrorMatchingInlineSnapshot(`[ManifestGetError: Failed to parse component manifest: +Invalid key: Expected "v" but received undefined]`); }); it('should throw ManifestGetError when components object is empty', async () => { @@ -127,8 +216,8 @@ describe('getManifest', () => { }); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow(ManifestGetError); - await expect(getManifest(request)).rejects.toThrow( + await expect(getManifests(request)).rejects.toThrow(ManifestGetError); + await expect(getManifests(request)).rejects.toThrow( 'No components found in the manifest', ); }); @@ -139,8 +228,8 @@ describe('getManifest', () => { .mockRejectedValue(new Error('Network connection failed')); const request = createMockRequest('https://example.com/mcp'); - await expect(getManifest(request)).rejects.toThrow(ManifestGetError); - await expect(getManifest(request)).rejects.toThrow( + await expect(getManifests(request)).rejects.toThrow(ManifestGetError); + await expect(getManifests(request)).rejects.toThrow( 'Network connection failed', ); }); @@ -154,7 +243,7 @@ describe('getManifest', () => { const request = createMockRequest('https://example.com/mcp'); try { - await getManifest(request); + await getManifests(request); } catch (error) { expect(error).toBeInstanceOf(ManifestGetError); expect((error as ManifestGetError).url).toBe( @@ -178,21 +267,60 @@ describe('getManifest', () => { }, }; - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: vi.fn().mockReturnValue('application/json'), - }, - text: vi.fn().mockResolvedValue(JSON.stringify(validManifest)), - }); + global.fetch = createFetchMock({ components: validManifest }); const request = createMockRequest('https://example.com/mcp'); - const result = await getManifest(request); + const result = await getManifests(request); - expect(result).toEqual(validManifest); - expect(global.fetch).toHaveBeenCalledExactlyOnceWith( + expect(result).toEqual({ componentManifest: validManifest }); + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(global.fetch).toHaveBeenCalledWith( 'https://example.com/manifests/components.json', ); + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/manifests/docs.json', + ); + }); + + it('should successfully fetch and parse both component and docs manifests', async () => { + const validComponentManifest: ComponentManifestMap = { + v: 1, + components: { + button: { + id: 'button', + path: 'src/components/Button.tsx', + name: 'Button', + description: 'A button component', + }, + }, + }; + + const validDocsManifest: DocsManifestMap = { + v: 1, + docs: { + 'getting-started': { + id: 'getting-started', + name: 'Getting Started', + title: 'Getting Started Guide', + path: 'docs/getting-started.mdx', + content: '# Getting Started\n\nWelcome to our component library.', + }, + }, + }; + + global.fetch = createFetchMock({ + components: validComponentManifest, + docs: validDocsManifest, + }); + + const request = createMockRequest('https://example.com/mcp'); + const result = await getManifests(request); + + expect(result).toEqual({ + componentManifest: validComponentManifest, + docsManifest: validDocsManifest, + }); + expect(global.fetch).toHaveBeenCalledTimes(2); }); }); @@ -211,17 +339,22 @@ describe('getManifest', () => { }; const request = createMockRequest('https://example.com/mcp'); - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(validManifest)); + const manifestProvider = createManifestProviderMock({ + components: JSON.stringify(validManifest), + }); - const result = await getManifest(request, manifestProvider); + const result = await getManifests(request, manifestProvider); - expect(result).toEqual(validManifest); - expect(manifestProvider).toHaveBeenCalledExactlyOnceWith( + expect(result).toEqual({ componentManifest: validManifest }); + expect(manifestProvider).toHaveBeenCalledTimes(2); + expect(manifestProvider).toHaveBeenCalledWith( request, './manifests/components.json', ); + expect(manifestProvider).toHaveBeenCalledWith( + request, + './manifests/docs.json', + ); // fetch should not be called when manifestProvider is used expect(global.fetch).not.toHaveBeenCalled(); }); @@ -239,15 +372,15 @@ describe('getManifest', () => { }, }; - // Custom provider that doesn't need the request - const manifestProvider = vi - .fn() - .mockResolvedValue(JSON.stringify(validManifest)); + const manifestProvider = createManifestProviderMock({ + components: JSON.stringify(validManifest), + }); - const result = await getManifest(undefined, manifestProvider); + const result = await getManifests(undefined, manifestProvider); - expect(result).toEqual(validManifest); - expect(manifestProvider).toHaveBeenCalledExactlyOnceWith( + expect(result).toEqual({ componentManifest: validManifest }); + expect(manifestProvider).toHaveBeenCalledTimes(2); + expect(manifestProvider).toHaveBeenCalledWith( undefined, './manifests/components.json', ); @@ -268,42 +401,39 @@ describe('getManifest', () => { }, }; - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - headers: { - get: vi.fn().mockReturnValue('application/json'), - }, - text: vi.fn().mockResolvedValue(JSON.stringify(validManifest)), - }); + global.fetch = createFetchMock({ components: validManifest }); const request = createMockRequest('https://example.com/mcp'); - const result = await getManifest(request); + const result = await getManifests(request); - expect(result).toEqual(validManifest); - expect(global.fetch).toHaveBeenCalledExactlyOnceWith( + expect(result).toEqual({ componentManifest: validManifest }); + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(global.fetch).toHaveBeenCalledWith( 'https://example.com/manifests/components.json', ); }); it('should handle errors from manifestProvider', async () => { const request = createMockRequest('https://example.com/mcp'); - const manifestProvider = vi - .fn() - .mockRejectedValue(new Error('File not found')); + const manifestProvider = createManifestProviderMock({ + components: new Error('File not found'), + }); - await expect(getManifest(request, manifestProvider)).rejects.toThrow( + await expect(getManifests(request, manifestProvider)).rejects.toThrow( ManifestGetError, ); - await expect(getManifest(request, manifestProvider)).rejects.toThrow( - 'Failed to get manifest: File not found', + await expect(getManifests(request, manifestProvider)).rejects.toThrow( + 'Failed to get component manifest: File not found', ); }); it('should handle invalid JSON from manifestProvider', async () => { const request = createMockRequest('https://example.com/mcp'); - const manifestProvider = vi.fn().mockResolvedValue('not valid json{'); + const manifestProvider = createManifestProviderMock({ + components: 'not valid json{', + }); - await expect(getManifest(request, manifestProvider)).rejects.toThrow( + await expect(getManifests(request, manifestProvider)).rejects.toThrow( ManifestGetError, ); }); diff --git a/packages/mcp/src/utils/get-manifest.ts b/packages/mcp/src/utils/get-manifest.ts index c5354107..aae45de6 100644 --- a/packages/mcp/src/utils/get-manifest.ts +++ b/packages/mcp/src/utils/get-manifest.ts @@ -1,10 +1,15 @@ -import { ComponentManifestMap } from '../types.ts'; +import { + ComponentManifestMap, + DocsManifestMap, + type AllManifests, +} from '../types.ts'; import * as v from 'valibot'; /** - * The path to the component manifest file relative to the Storybook build + * The paths to the manifest files relative to the Storybook build */ -export const MANIFEST_PATH = './manifests/components.json'; +export const COMPONENT_MANIFEST_PATH = './manifests/components.json'; +export const DOCS_MANIFEST_PATH = './manifests/docs.json'; /** * Error thrown when getting or parsing a manifest fails @@ -62,53 +67,97 @@ export const errorToMCPContent = (error: unknown): MCPErrorResult => { }; /** - * Gets a component manifest from a request or using a custom provider + * Parses a JSON string and validates it against a Valibot schema + */ +function parseManifest< + T extends v.BaseSchema>, +>({ + jsonString, + schema, + name, + url, +}: { + jsonString: string; + schema: T; + name: string; + url: string; +}): v.InferOutput { + try { + return v.parse(v.pipe(v.string(), v.parseJson(), schema), jsonString); + } catch (error) { + throw new ManifestGetError( + `Failed to parse ${name} manifest: +${error instanceof v.ValiError ? error.issues.map((i) => i.message).join('\n') : String(error)}`, + url, + ); + } +} + +/** + * Gets component and docs manifest from a request or using a custom provider * * @param request - The HTTP request to get the manifest for (optional when using custom manifestProvider) * @param manifestProvider - Optional custom function to get the manifest * @returns A promise that resolves to the parsed ComponentManifestMap * @throws {ManifestGetError} If getting the manifest fails or the response is invalid */ -export async function getManifest( +export async function getManifests( request?: Request, manifestProvider?: ( request: Request | undefined, path: string, ) => Promise, -): Promise { - try { - // Use custom manifestProvider if provided, otherwise fallback to default - const manifestString = await (manifestProvider ?? defaultManifestProvider)( - request, - MANIFEST_PATH, - ); - const manifestData: unknown = JSON.parse(manifestString); - - const manifest = v.parse(ComponentManifestMap, manifestData); +): Promise { + const provider = manifestProvider ?? defaultManifestProvider; + + // Fetch both component and docs manifests in parallel + const [componentResult, docsResult] = await Promise.allSettled([ + provider(request, COMPONENT_MANIFEST_PATH), + provider(request, DOCS_MANIFEST_PATH), + ]); + + const getUrl = (path: string) => + request + ? getManifestUrlFromRequest(request, path) + : 'Unknown manifest source'; - if (Object.keys(manifest.components).length === 0) { - const url = request - ? getManifestUrlFromRequest(request, MANIFEST_PATH) - : 'Unknown manifest source'; - throw new ManifestGetError(`No components found in the manifest`, url); - } + if (componentResult.status === 'rejected') { + throw new ManifestGetError( + `Failed to get component manifest: ${componentResult.reason instanceof Error ? componentResult.reason.message : String(componentResult.reason)}`, + getUrl(COMPONENT_MANIFEST_PATH), + componentResult.reason instanceof Error + ? componentResult.reason + : undefined, + ); + } - return manifest; - } catch (error) { - if (error instanceof ManifestGetError) { - throw error; - } + const componentManifest = parseManifest({ + jsonString: componentResult.value, + schema: ComponentManifestMap, + name: 'component', + url: getUrl(COMPONENT_MANIFEST_PATH), + }); - // Wrap network errors and other unexpected errors - const url = request - ? getManifestUrlFromRequest(request, MANIFEST_PATH) - : 'Unknown manifest source'; + if (Object.keys(componentManifest.components).length === 0) { throw new ManifestGetError( - `Failed to get manifest: ${error instanceof Error ? error.message : String(error)}`, - url, - error instanceof Error ? error : undefined, + `No components found in the manifest`, + getUrl(COMPONENT_MANIFEST_PATH), ); } + + if (docsResult.status === 'rejected') { + return { componentManifest }; + } + + // Handle docs manifest result (optional - only exists when addon-docs is used) + const docsManifest = parseManifest({ + jsonString: docsResult.value, + schema: DocsManifestMap, + name: 'docs', + url: getUrl(DOCS_MANIFEST_PATH), + }); + + return { componentManifest, docsManifest }; } /** diff --git a/packages/mcp/src/utils/manifest-formatter/__snapshots__/markdown.test.ts.snap b/packages/mcp/src/utils/manifest-formatter/__snapshots__/markdown.test.ts.snap index eb3a41dd..fbdb979a 100644 --- a/packages/mcp/src/utils/manifest-formatter/__snapshots__/markdown.test.ts.snap +++ b/packages/mcp/src/utils/manifest-formatter/__snapshots__/markdown.test.ts.snap @@ -426,7 +426,7 @@ export type Props = { \`\`\`" `; -exports[`MarkdownFormatter - formatComponentManifestMapToList > formats the full manifest fixture 1`] = ` +exports[`MarkdownFormatter - formatManifestsToLists > formats the full manifest fixture 1`] = ` "# Components - Button (button): A versatile button component for user interactions diff --git a/packages/mcp/src/utils/manifest-formatter/__snapshots__/xml.test.ts.snap b/packages/mcp/src/utils/manifest-formatter/__snapshots__/xml.test.ts.snap index 76fcdc4f..2ea00b52 100644 --- a/packages/mcp/src/utils/manifest-formatter/__snapshots__/xml.test.ts.snap +++ b/packages/mcp/src/utils/manifest-formatter/__snapshots__/xml.test.ts.snap @@ -533,7 +533,7 @@ Callback function when the input value changes " `; -exports[`XmlFormatter - formatComponentManifestMapToList > formats the full manifest fixture 1`] = ` +exports[`XmlFormatter - formatManifestsToLists > formats the full manifest fixture 1`] = ` " button diff --git a/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.test.ts b/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.test.ts new file mode 100644 index 00000000..dada8638 --- /dev/null +++ b/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.test.ts @@ -0,0 +1,201 @@ +import { describe, it, expect } from 'vitest'; +import { extractDocsSummary } from './extract-docs-summary.ts'; + +describe('extractDocsSummary', () => { + describe('import statement removal', () => { + it('should remove single import statements', () => { + const content = `import { Button } from './Button'; + +This is the actual content.`; + expect(extractDocsSummary(content)).toBe('This is the actual content.'); + }); + + it('should remove multiple import statements', () => { + const content = `import { Button } from './Button'; +import { Meta, Story } from '@storybook/blocks'; +import React from 'react'; + +This is the content after imports.`; + expect(extractDocsSummary(content)).toBe( + 'This is the content after imports.', + ); + }); + + it('should remove default imports', () => { + const content = `import Button from './Button'; + +Button documentation here.`; + expect(extractDocsSummary(content)).toBe('Button documentation here.'); + }); + + it('should remove namespace imports', () => { + const content = `import * as Icons from './icons'; + +Icons documentation.`; + expect(extractDocsSummary(content)).toBe('Icons documentation.'); + }); + + it('should remove side-effect imports', () => { + const content = `import './styles.css'; + +Styled content.`; + expect(extractDocsSummary(content)).toBe('Styled content.'); + }); + }); + + describe('expression removal', () => { + it('should remove simple expressions', () => { + const content = `Some text {expression} more text.`; + expect(extractDocsSummary(content)).toBe('Some text more text.'); + }); + + it('should remove JSX comments', () => { + const content = `Some text {/* comment */} more text.`; + expect(extractDocsSummary(content)).toBe('Some text more text.'); + }); + + it('should remove nested expressions', () => { + const content = `Text {outer {inner} value} end.`; + expect(extractDocsSummary(content)).toBe('Text end.'); + }); + + it('should remove complex expressions with function calls', () => { + const content = `Count: {items.length} items.`; + expect(extractDocsSummary(content)).toBe('Count: items.'); + }); + + it('should remove expressions with template literals', () => { + const content = 'Text {`template ${value}`} end.'; + expect(extractDocsSummary(content)).toBe('Text end.'); + }); + }); + + describe('JSX/HTML element text extraction', () => { + it('should extract text from simple elements', () => { + const content = `

This is a paragraph.

`; + expect(extractDocsSummary(content)).toBe('This is a paragraph.'); + }); + + it('should extract text from nested elements', () => { + const content = `

Nested bold text.

`; + expect(extractDocsSummary(content)).toBe('Nested bold text.'); + }); + + it('should remove self-closing tags', () => { + const content = `Text before
text after end.`; + expect(extractDocsSummary(content)).toBe('Text before text after end.'); + }); + + it('should handle PascalCase component names', () => { + const content = `Component content`; + expect(extractDocsSummary(content)).toBe('Component content'); + }); + + it('should handle elements with attributes', () => { + const content = `
Content here
`; + expect(extractDocsSummary(content)).toBe('Content here'); + }); + + it('should handle multiple sibling elements', () => { + const content = `

First paragraph.

Second paragraph.

`; + expect(extractDocsSummary(content)).toBe( + 'First paragraph. Second paragraph.', + ); + }); + }); + + describe('combined scenarios', () => { + it('should handle typical MDX file content', () => { + const content = `import { Button } from './Button'; +import { Meta, Story } from '@storybook/blocks'; + + + +# Button Component + +The Button component is used for user interactions. + +
+ + + +
`; + const result = extractDocsSummary(content); + expect(result).toBe( + '# Button Component The Button component is used for user interactions. Click me', + ); + }); + + it('should handle MDX with expressions and components', () => { + const content = `
+ Welcome to {appName}! + +

Inner text content.

+
+
`; + expect(extractDocsSummary(content)).toBe( + 'Welcome to ! Inner text content.', + ); + }); + + it('should handle empty content', () => { + expect(extractDocsSummary('')).toBeUndefined(); + }); + + it('should handle content with only imports', () => { + const content = `import { Button } from './Button'; +import { Meta } from '@storybook/blocks';`; + expect(extractDocsSummary(content)).toBeUndefined(); + }); + + it('should handle content with only expressions and tags', () => { + const content = `{expression} `; + expect(extractDocsSummary(content)).toBeUndefined(); + }); + }); + + describe('truncation', () => { + it('should truncate content longer than 90 characters', () => { + const content = + 'This is a very long description that exceeds the maximum summary length limit and should be truncated with an ellipsis.'; + const result = extractDocsSummary(content); + expect(result).toBe( + 'This is a very long description that exceeds the maximum summary length limit and should b...', + ); + expect(result?.length).toBe(93); // 90 chars + '...' + }); + + it('should not truncate content at exactly 90 characters', () => { + const content = 'A'.repeat(90); + expect(extractDocsSummary(content)).toBe(content); + }); + + it('should not truncate content shorter than 90 characters', () => { + const content = 'Short description.'; + expect(extractDocsSummary(content)).toBe(content); + }); + }); + + describe('whitespace handling', () => { + it('should collapse multiple spaces', () => { + const content = `Text with multiple spaces.`; + expect(extractDocsSummary(content)).toBe('Text with multiple spaces.'); + }); + + it('should collapse newlines into spaces', () => { + const content = `Line one. +Line two. +Line three.`; + expect(extractDocsSummary(content)).toBe( + 'Line one. Line two. Line three.', + ); + }); + + it('should trim leading and trailing whitespace', () => { + const content = ` + Trimmed content. + `; + expect(extractDocsSummary(content)).toBe('Trimmed content.'); + }); + }); +}); diff --git a/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.ts b/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.ts new file mode 100644 index 00000000..34fd2c29 --- /dev/null +++ b/packages/mcp/src/utils/manifest-formatter/extract-docs-summary.ts @@ -0,0 +1,61 @@ +import { MAX_SUMMARY_LENGTH } from './types.ts'; + +/** + * Extracts a summary from MDX content. + * The summary is created by: + * 1. Removing import statements + * 2. Removing JSX/MDX expressions + * 3. Extracting only text content from JSX/HTML elements + * 4. Truncating to MAX_SUMMARY_LENGTH characters if needed + * + * @param content - The MDX content string + * @returns A summary string, or undefined if no meaningful text content is found + */ +export function extractDocsSummary(content: string): string | undefined { + let result = content; + + // Step 1: Remove import statements (can be single or multi-line) + // Matches: import ... from '...' or import '...' + result = result.replace( + /^\s*import\s+(?:[\s\S]*?from\s+)?['"][^'"]+['"];?\s*$/gm, + '', + ); + + // Step 2: Remove JSX/MDX expressions like {expression} or {/* comments */} + // Handle nested braces by iteratively removing innermost expressions + let prevResult = ''; + while (prevResult !== result) { + prevResult = result; + result = result.replace(/\{[^{}]*\}/g, ''); + } + + // Step 3: Remove self-closing tags like or
+ result = result.replace(/<[^>]+\/>/g, ''); + + // Step 4: Extract text content from JSX/HTML elements + // Iteratively remove tags from innermost to outermost + prevResult = ''; + while (prevResult !== result) { + prevResult = result; + // Remove tags but keep content between them + // Match opening and closing tags of the same type + result = result.replace(/<(\w+)[^>]*>([\s\S]*?)<\/\1>/g, '$2'); + } + + // Step 5: Remove any remaining standalone opening or closing tags + result = result.replace(/<[^>]+>/g, ''); + + // Step 6: Clean up whitespace + // Replace multiple whitespace characters (including newlines) with single space + result = result.replace(/\s+/g, ' ').trim(); + + if (!result) { + return undefined; + } + + if (result.length > MAX_SUMMARY_LENGTH) { + return `${result.slice(0, MAX_SUMMARY_LENGTH)}...`; + } + + return result; +} diff --git a/packages/mcp/src/utils/manifest-formatter/markdown.test.ts b/packages/mcp/src/utils/manifest-formatter/markdown.test.ts index 0c5dc30e..61da021c 100644 --- a/packages/mcp/src/utils/manifest-formatter/markdown.test.ts +++ b/packages/mcp/src/utils/manifest-formatter/markdown.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect } from 'vitest'; import { markdownFormatter } from './markdown.ts'; -import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; +import type { + AllManifests, + ComponentManifest, + ComponentManifestMap, +} from '../../types.ts'; import fullManifestFixture from '../../../fixtures/full-manifest.fixture.json' with { type: 'json' }; describe('MarkdownFormatter - formatComponentManifest', () => { @@ -442,28 +446,30 @@ describe('MarkdownFormatter - formatComponentManifest', () => { }); }); -describe('MarkdownFormatter - formatComponentManifestMapToList', () => { +describe('MarkdownFormatter - formatManifestsToLists', () => { it('formats the full manifest fixture', () => { - const result = - markdownFormatter.formatComponentManifestMapToList(fullManifestFixture); + const result = markdownFormatter.formatManifestsToLists({ + componentManifest: fullManifestFixture as ComponentManifestMap, + }); expect(result).toMatchSnapshot(); }); describe('component list structure', () => { it('should format a single component', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` "# Components @@ -473,29 +479,30 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { }); it('should format multiple components', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - }, - card: { - id: 'card', - name: 'Card', - path: 'src/components/Card.tsx', - }, - input: { - id: 'input', - name: 'Input', - path: 'src/components/Input.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + card: { + id: 'card', + name: 'Card', + path: 'src/components/Card.tsx', + }, + input: { + id: 'input', + name: 'Input', + path: 'src/components/Input.tsx', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` "# Components @@ -509,20 +516,21 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { describe('summary section', () => { it('should include summary when provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - summary: 'A versatile button component', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + summary: 'A versatile button component', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` "# Components @@ -532,41 +540,43 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { }); it('should prefer summary over description', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - summary: 'Button summary', - description: 'Button description', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + summary: 'Button summary', + description: 'Button description', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toContain('Button summary'); expect(result).not.toContain('Button description'); }); it('should use description when summary is not provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: 'A simple button component', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: 'A simple button component', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` "# Components @@ -576,21 +586,22 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { }); it('should truncate long descriptions to 90 characters', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: - 'This is a very long description that exceeds ninety characters and should be truncated with ellipsis', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: + 'This is a very long description that exceeds ninety characters and should be truncated with ellipsis', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toContain('...'); expect(result).toMatchInlineSnapshot(` @@ -601,39 +612,41 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { }); it('should not truncate descriptions under 90 characters', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: 'A button component for user interactions', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: 'A button component for user interactions', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).not.toContain('...'); expect(result).toContain('A button component for user interactions'); }); it('should omit summary when neither summary nor description provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, }, }, }; - const result = - markdownFormatter.formatComponentManifestMapToList(manifest); + const result = markdownFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` "# Components @@ -642,4 +655,185 @@ describe('MarkdownFormatter - formatComponentManifestMapToList', () => { `); }); }); + + describe('docs manifest section', () => { + it('should include docs section when docsManifest is provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'getting-started': { + id: 'getting-started', + name: 'Getting Started', + title: 'Getting Started Guide', + path: 'docs/getting-started.mdx', + content: 'Welcome to our component library.', + }, + }, + }, + }; + + const result = markdownFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + "# Components + + - Button (button) + + # Docs + + - Getting Started Guide (getting-started): Welcome to our component library." + `); + }); + + it('should format multiple docs entries', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'getting-started': { + id: 'getting-started', + name: 'Getting Started', + title: 'Getting Started Guide', + path: 'docs/getting-started.mdx', + content: 'Welcome to our component library.', + }, + theming: { + id: 'theming', + name: 'Theming', + title: 'Theming Guide', + path: 'docs/theming.mdx', + content: 'Learn how to customize the theme.', + }, + }, + }, + }; + + const result = markdownFormatter.formatManifestsToLists(manifests); + + expect(result).toContain('# Components'); + expect(result).toContain('# Docs'); + expect(result).toContain('- Getting Started Guide (getting-started)'); + expect(result).toContain('- Theming Guide (theming)'); + }); + + it('should omit docs section when docsManifest is not provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + }; + + const result = markdownFormatter.formatManifestsToLists(manifests); + + expect(result).not.toContain('# Docs'); + }); + + it('should use doc.summary when provided instead of extracting from content', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'custom-summary': { + id: 'custom-summary', + name: 'Custom Summary', + title: 'Custom Summary Doc', + path: 'docs/custom-summary.mdx', + content: + 'This is a very long content that would normally be extracted and truncated.', + summary: 'This is a custom summary', + }, + }, + }, + }; + + const result = markdownFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + "# Components + + - Button (button) + + # Docs + + - Custom Summary Doc (custom-summary): This is a custom summary" + `); + }); + + it('should extract summary from content when doc.summary is not provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'auto-summary': { + id: 'auto-summary', + name: 'Auto Summary', + title: 'Auto Summary Doc', + path: 'docs/auto-summary.mdx', + content: 'This content will be extracted automatically.', + }, + }, + }, + }; + + const result = markdownFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + "# Components + + - Button (button) + + # Docs + + - Auto Summary Doc (auto-summary): This content will be extracted automatically." + `); + }); + }); }); diff --git a/packages/mcp/src/utils/manifest-formatter/markdown.ts b/packages/mcp/src/utils/manifest-formatter/markdown.ts index 32874667..6274f141 100644 --- a/packages/mcp/src/utils/manifest-formatter/markdown.ts +++ b/packages/mcp/src/utils/manifest-formatter/markdown.ts @@ -1,8 +1,8 @@ -import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; -import type { ManifestFormatter } from './types.ts'; +import type { ComponentManifest } from '../../types.ts'; +import { MAX_SUMMARY_LENGTH, type ManifestFormatter } from './types.ts'; import { parseReactDocgen } from '../parse-react-docgen.ts'; - -const MAX_SUMMARY_LENGTH = 90; +import { dedent } from '../dedent.ts'; +import { extractDocsSummary } from './extract-docs-summary.ts'; /** * Markdown formatter for component manifests. @@ -108,13 +108,21 @@ export const markdownFormatter: ManifestFormatter = { return parts.join('\n').trim(); }, - formatComponentManifestMapToList(manifest: ComponentManifestMap): string { + formatDocsManifest(doc) { + return dedent`# ${doc.title} + + ${doc.content}`; + }, + + formatManifestsToLists(manifests) { const parts: string[] = []; parts.push('# Components'); parts.push(''); - for (const component of Object.values(manifest.components)) { + for (const component of Object.values( + manifests.componentManifest.components, + )) { const summary = component.summary ?? (component.description @@ -132,6 +140,18 @@ export const markdownFormatter: ManifestFormatter = { parts.push(''); + if (!manifests.docsManifest) { + return parts.join('\n').trim(); + } + + parts.push('# Docs'); + parts.push(''); + + for (const doc of Object.values(manifests.docsManifest.docs)) { + const summary = doc.summary ?? extractDocsSummary(doc.content); + parts.push(`- ${doc.title} (${doc.id})${summary ? `: ${summary}` : ''}`); + } + return parts.join('\n').trim(); }, }; diff --git a/packages/mcp/src/utils/manifest-formatter/types.ts b/packages/mcp/src/utils/manifest-formatter/types.ts index e34a7ccf..5055c808 100644 --- a/packages/mcp/src/utils/manifest-formatter/types.ts +++ b/packages/mcp/src/utils/manifest-formatter/types.ts @@ -1,4 +1,9 @@ -import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; +import type { AllManifests, ComponentManifest, Doc } from '../../types.ts'; + +/** + * Maximum length for a summary before truncation. + */ +export const MAX_SUMMARY_LENGTH = 90; /** * Interface for manifest formatters. @@ -8,15 +13,18 @@ import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; export interface ManifestFormatter { /** * Format a single component manifest into the target format. - * @param componentManifest - The component manifest to format - * @returns Formatted string representation of the component */ formatComponentManifest(componentManifest: ComponentManifest): string; + /** + * Format a single doc manifest into the target format. + */ + formatDocsManifest(doc: Doc): string; + /** * Format a component manifest map into a list in the target format. * @param manifest - The component manifest map to format * @returns Formatted string representation of the component list */ - formatComponentManifestMapToList(manifest: ComponentManifestMap): string; + formatManifestsToLists(manifests: AllManifests): string; } diff --git a/packages/mcp/src/utils/manifest-formatter/xml.test.ts b/packages/mcp/src/utils/manifest-formatter/xml.test.ts index f2673e83..280ff504 100644 --- a/packages/mcp/src/utils/manifest-formatter/xml.test.ts +++ b/packages/mcp/src/utils/manifest-formatter/xml.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect } from 'vitest'; import { xmlFormatter } from './xml.ts'; -import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; +import type { + AllManifests, + ComponentManifest, + ComponentManifestMap, +} from '../../types.ts'; import fullManifestFixture from '../../../fixtures/full-manifest.fixture.json' with { type: 'json' }; import withErrorsFixture from '../../../fixtures/with-errors.fixture.json' with { type: 'json' }; @@ -467,27 +471,30 @@ describe('XmlFormatter - formatComponentManifest', () => { }); }); -describe('XmlFormatter - formatComponentManifestMapToList', () => { +describe('XmlFormatter - formatManifestsToLists', () => { it('formats the full manifest fixture', () => { - const result = - xmlFormatter.formatComponentManifestMapToList(fullManifestFixture); + const result = xmlFormatter.formatManifestsToLists({ + componentManifest: fullManifestFixture as ComponentManifestMap, + }); expect(result).toMatchSnapshot(); }); describe('component list structure', () => { it('should format a single component', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -500,28 +507,30 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should format multiple components', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - }, - card: { - id: 'card', - name: 'Card', - path: 'src/components/Card.tsx', - }, - input: { - id: 'input', - name: 'Input', - path: 'src/components/Input.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + card: { + id: 'card', + name: 'Card', + path: 'src/components/Card.tsx', + }, + input: { + id: 'input', + name: 'Input', + path: 'src/components/Input.tsx', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -544,19 +553,21 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { describe('summary section', () => { it('should include summary when provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - summary: 'A versatile button component', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + summary: 'A versatile button component', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -572,39 +583,44 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should prefer summary over description', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - summary: 'Short summary', - description: 'This is a longer description that should be ignored', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + summary: 'Short summary', + description: + 'This is a longer description that should be ignored', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toContain('Short summary'); expect(result).not.toContain('longer description'); }); it('should use description when summary is not provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: 'A simple button component', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: 'A simple button component', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -620,20 +636,22 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should truncate long descriptions to 90 characters', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: - 'This is a very long description that exceeds ninety characters and should be truncated with ellipsis', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: + 'This is a very long description that exceeds ninety characters and should be truncated with ellipsis', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -649,20 +667,22 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should not truncate descriptions under 90 characters', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - description: - 'A description with exactly eighty characters is fine and should not be truncated', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + description: + 'A description with exactly eighty characters is fine and should not be truncated', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toContain( 'A description with exactly eighty characters is fine and should not be truncated', @@ -671,18 +691,20 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should omit summary section when neither summary nor description provided', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).not.toContain(''); expect(result).toMatchInlineSnapshot(` @@ -698,38 +720,40 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { describe('complete manifest', () => { it('should format a complete manifest with varied components', () => { - const manifest: ComponentManifestMap = { - v: 1, - components: { - button: { - id: 'button', - name: 'Button', - path: 'src/components/Button.tsx', - summary: 'A versatile button component', - }, - card: { - id: 'card', - name: 'Card', - path: 'src/components/Card.tsx', - description: 'A flexible container for grouping content', - }, - input: { - id: 'input', - name: 'Input', - path: 'src/components/Input.tsx', - summary: 'Text input with validation', - description: - 'A comprehensive input component with validation, error states, and accessibility features', - }, - modal: { - id: 'modal', - name: 'Modal', - path: 'src/components/Modal.tsx', + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + summary: 'A versatile button component', + }, + card: { + id: 'card', + name: 'Card', + path: 'src/components/Card.tsx', + description: 'A flexible container for grouping content', + }, + input: { + id: 'input', + name: 'Input', + path: 'src/components/Input.tsx', + summary: 'Text input with validation', + description: + 'A comprehensive input component with validation, error states, and accessibility features', + }, + modal: { + id: 'modal', + name: 'Modal', + path: 'src/components/Modal.tsx', + }, }, }, }; - const result = xmlFormatter.formatComponentManifestMapToList(manifest); + const result = xmlFormatter.formatManifestsToLists(manifests); expect(result).toMatchInlineSnapshot(` " @@ -763,6 +787,211 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); }); + describe('docs manifest section', () => { + it('should include docs section when docsManifest is provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'getting-started': { + id: 'getting-started', + name: 'Getting Started', + title: 'Getting Started Guide', + path: 'docs/getting-started.mdx', + content: 'Welcome to our component library.', + }, + }, + }, + }; + + const result = xmlFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + " + + button + Button + + + + + getting-started + Getting Started Guide + + Welcome to our component library. + + + " + `); + }); + + it('should format multiple docs entries', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'getting-started': { + id: 'getting-started', + name: 'Getting Started', + title: 'Getting Started Guide', + path: 'docs/getting-started.mdx', + content: 'Welcome to our component library.', + }, + theming: { + id: 'theming', + name: 'Theming', + title: 'Theming Guide', + path: 'docs/theming.mdx', + content: 'Learn how to customize the theme.', + }, + }, + }, + }; + + const result = xmlFormatter.formatManifestsToLists(manifests); + + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('Getting Started Guide'); + expect(result).toContain('Theming Guide'); + }); + + it('should omit docs section when docsManifest is not provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + }; + + const result = xmlFormatter.formatManifestsToLists(manifests); + + expect(result).not.toContain(''); + }); + + it('should use doc.summary when provided instead of extracting from content', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'custom-summary': { + id: 'custom-summary', + name: 'Custom Summary', + title: 'Custom Summary Doc', + path: 'docs/custom-summary.mdx', + content: + 'This is a very long content that would normally be extracted and truncated.', + summary: 'This is a custom summary', + }, + }, + }, + }; + + const result = xmlFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + " + + button + Button + + + + + custom-summary + Custom Summary Doc + + This is a custom summary + + + " + `); + }); + + it('should extract summary from content when doc.summary is not provided', () => { + const manifests: AllManifests = { + componentManifest: { + v: 1, + components: { + button: { + id: 'button', + name: 'Button', + path: 'src/components/Button.tsx', + }, + }, + }, + docsManifest: { + v: 1, + docs: { + 'auto-summary': { + id: 'auto-summary', + name: 'Auto Summary', + title: 'Auto Summary Doc', + path: 'docs/auto-summary.mdx', + content: 'This content will be extracted automatically.', + }, + }, + }, + }; + + const result = xmlFormatter.formatManifestsToLists(manifests); + + expect(result).toMatchInlineSnapshot(` + " + + button + Button + + + + + auto-summary + Auto Summary Doc + + This content will be extracted automatically. + + + " + `); + }); + }); + describe('with-errors fixture', () => { it('should format success component with mixed stories (only successful ones)', () => { const component = @@ -844,9 +1073,9 @@ describe('XmlFormatter - formatComponentManifestMapToList', () => { }); it('should format list of components with errors', () => { - const result = xmlFormatter.formatComponentManifestMapToList( - withErrorsFixture as ComponentManifestMap, - ); + const result = xmlFormatter.formatManifestsToLists({ + componentManifest: withErrorsFixture as ComponentManifestMap, + }); expect(result).toMatchInlineSnapshot(` " diff --git a/packages/mcp/src/utils/manifest-formatter/xml.ts b/packages/mcp/src/utils/manifest-formatter/xml.ts index 126ac6b7..a1b30163 100644 --- a/packages/mcp/src/utils/manifest-formatter/xml.ts +++ b/packages/mcp/src/utils/manifest-formatter/xml.ts @@ -1,16 +1,14 @@ -import type { ComponentManifest, ComponentManifestMap } from '../../types.ts'; -import type { ManifestFormatter } from './types.ts'; +import { MAX_SUMMARY_LENGTH, type ManifestFormatter } from './types.ts'; +import { extractDocsSummary } from './extract-docs-summary.ts'; import { dedent } from '../dedent.ts'; import { parseReactDocgen } from '../parse-react-docgen.ts'; -const MAX_SUMMARY_LENGTH = 90; - /** * XML formatter for component manifests. * Formats component data into XML structure with tags like , , etc. */ export const xmlFormatter: ManifestFormatter = { - formatComponentManifest(componentManifest: ComponentManifest): string { + formatComponentManifest(componentManifest) { const parts: string[] = []; // Component opening tag @@ -98,12 +96,23 @@ export const xmlFormatter: ManifestFormatter = { return parts.join('\n'); }, - formatComponentManifestMapToList(manifest: ComponentManifestMap): string { + formatDocsManifest(doc) { + return dedent` + ${doc.title} + + ${doc.content} + + `; + }, + + formatManifestsToLists(manifests) { const parts: string[] = []; parts.push(''); - for (const component of Object.values(manifest.components)) { + for (const component of Object.values( + manifests.componentManifest.components, + )) { parts.push(dedent` ${component.id} ${component.name}`); @@ -127,6 +136,29 @@ export const xmlFormatter: ManifestFormatter = { parts.push(''); + if (!manifests.docsManifest) { + return parts.join('\n'); + } + + parts.push(''); + + for (const doc of Object.values(manifests.docsManifest.docs)) { + const summary = doc.summary ?? extractDocsSummary(doc.content); + parts.push(dedent` + ${doc.id} + ${doc.title}`); + + if (summary) { + parts.push(dedent` + ${summary} + `); + } + + parts.push(''); + } + + parts.push(''); + return parts.join('\n'); }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80737509..93d719ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,14 @@ settings: catalogs: default: '@storybook/addon-a11y': - specifier: 10.1.2 - version: 10.1.2 + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 '@storybook/addon-docs': - specifier: 10.1.2 - version: 10.1.2 + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 '@storybook/react-vite': - specifier: 10.1.2 - version: 10.1.2 + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 '@tmcp/adapter-valibot': specifier: ^0.1.4 version: 0.1.4 @@ -25,8 +25,8 @@ catalogs: specifier: ^0.4.1 version: 0.4.1 storybook: - specifier: 10.1.2 - version: 10.1.2 + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 tmcp: specifier: ^1.16.0 version: 1.18.1 @@ -36,6 +36,73 @@ catalogs: vite: specifier: 7.2.2 version: 7.2.2 + experiments: + '@eslint/js': + specifier: 9.39.1 + version: 9.39.1 + '@storybook/addon-a11y': + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + '@storybook/addon-docs': + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + '@storybook/addon-vitest': + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + '@storybook/react-vite': + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + '@types/node': + specifier: 24.10.1 + version: 24.10.1 + '@types/react': + specifier: 19.2.6 + version: 19.2.6 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3 + '@vitejs/plugin-react-swc': + specifier: 4.2.2 + version: 4.2.2 + '@vitest/browser-playwright': + specifier: 4.0.6 + version: 4.0.6 + eslint: + specifier: 9.39.1 + version: 9.39.1 + eslint-plugin-react-hooks: + specifier: 7.0.1 + version: 7.0.1 + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24 + eslint-plugin-storybook: + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + globals: + specifier: 16.5.0 + version: 16.5.0 + react: + specifier: 19.2.0 + version: 19.2.0 + react-dom: + specifier: 19.2.0 + version: 19.2.0 + storybook: + specifier: 10.2.0-alpha.14 + version: 10.2.0-alpha.14 + typescript: + specifier: 5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: 8.47.0 + version: 8.47.0 + vite: + specifier: 7.2.2 + version: 7.2.2 + vitest: + specifier: 4.0.6 + version: 4.0.6 importers: @@ -106,13 +173,13 @@ importers: devDependencies: '@storybook/addon-docs': specifier: 'catalog:' - version: 10.1.2(@types/react@18.3.26)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + version: 10.2.0-alpha.14(@types/react@18.3.26)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@storybook/addon-mcp': specifier: workspace:* version: link:../../packages/addon-mcp '@storybook/react-vite': specifier: 'catalog:' - version: 10.1.2(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + version: 10.2.0-alpha.14(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@types/react': specifier: ^18.2.65 version: 18.3.26 @@ -130,7 +197,7 @@ importers: version: 18.3.1(react@18.3.1) storybook: specifier: 'catalog:' - version: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tinyexec: specifier: ^1.0.2 version: 1.0.2 @@ -163,13 +230,13 @@ importers: version: 1.1.11(@types/react-dom@19.2.3(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.1.2(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + version: 10.2.0-alpha.14(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/mcp': specifier: workspace:* version: link:../packages/mcp '@storybook/react-vite': specifier: 'catalog:' - version: 10.1.2(esbuild@0.25.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + version: 10.2.0-alpha.14(esbuild@0.25.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@tsconfig/node-ts': specifier: ^23.6.1 version: 23.6.2 @@ -241,10 +308,10 @@ importers: version: 5.83.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: specifier: 'catalog:' - version: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook-addon-test-codegen: specifier: ^3.0.0 - version: 3.0.0(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + version: 3.0.0(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) tinyexec: specifier: ^1.0.1 version: 1.0.2 @@ -258,6 +325,131 @@ importers: specifier: ^7.1.12 version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + eval/evals/110-flight-booking-reshaped/experiments/storybook-mcp-docs-claude-code-claude-sonnet-4.5-2026-01-08T14-02-29/project: + dependencies: + react: + specifier: catalog:experiments + version: 19.2.0 + react-dom: + specifier: catalog:experiments + version: 19.2.0(react@19.2.0) + reshaped: + specifier: ^3.9.0 + version: 3.9.0(postcss@8.5.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + devDependencies: + '@eslint/js': + specifier: catalog:experiments + version: 9.39.1 + '@storybook/addon-a11y': + specifier: catalog:experiments + version: 10.2.0-alpha.14(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + '@storybook/addon-docs': + specifier: catalog:experiments + version: 10.2.0-alpha.14(@types/react@19.2.6)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/addon-mcp': + specifier: workspace:* + version: link:../../../../../../packages/addon-mcp + '@storybook/addon-vitest': + specifier: catalog:experiments + version: 10.2.0-alpha.14(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.6) + '@storybook/react-vite': + specifier: catalog:experiments + version: 10.2.0-alpha.14(esbuild@0.25.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@types/node': + specifier: catalog:experiments + version: 24.10.1 + '@types/react': + specifier: catalog:experiments + version: 19.2.6 + '@types/react-dom': + specifier: catalog:experiments + version: 19.2.3(@types/react@19.2.6) + '@vitejs/plugin-react-swc': + specifier: catalog:experiments + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + '@vitest/browser-playwright': + specifier: catalog:experiments + version: 4.0.6(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6) + eslint: + specifier: catalog:experiments + version: 9.39.1(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: catalog:experiments + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: catalog:experiments + version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-storybook: + specifier: catalog:experiments + version: 10.2.0-alpha.14(eslint@9.39.1(jiti@2.6.1))(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3) + globals: + specifier: catalog:experiments + version: 16.5.0 + storybook: + specifier: catalog:experiments + version: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + typescript: + specifier: catalog:experiments + version: 5.9.3 + typescript-eslint: + specifier: catalog:experiments + version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: catalog:experiments + version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + vitest: + specifier: catalog:experiments + version: 4.0.6(@types/node@24.10.1)(@vitest/browser-playwright@4.0.6)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + + eval/evals/110-flight-booking-reshaped/experiments/storybook-mcp-docs-claude-code-claude-sonnet-4.5-2026-01-08T14-06-26/project: + dependencies: + react: + specifier: catalog:experiments + version: 19.2.0 + react-dom: + specifier: catalog:experiments + version: 19.2.0(react@19.2.0) + reshaped: + specifier: ^3.9.0 + version: 3.9.0(postcss@8.5.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + devDependencies: + '@eslint/js': + specifier: catalog:experiments + version: 9.39.1 + '@types/node': + specifier: catalog:experiments + version: 24.10.1 + '@types/react': + specifier: catalog:experiments + version: 19.2.6 + '@types/react-dom': + specifier: catalog:experiments + version: 19.2.3(@types/react@19.2.6) + '@vitejs/plugin-react-swc': + specifier: catalog:experiments + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + eslint: + specifier: catalog:experiments + version: 9.39.1(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: catalog:experiments + version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: catalog:experiments + version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + globals: + specifier: catalog:experiments + version: 16.5.0 + typescript: + specifier: catalog:experiments + version: 5.9.3 + typescript-eslint: + specifier: catalog:experiments + version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: catalog:experiments + version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + packages/addon-mcp: dependencies: '@storybook/mcp': @@ -278,7 +470,7 @@ importers: devDependencies: storybook: specifier: 'catalog:' - version: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) packages/mcp: dependencies: @@ -712,6 +904,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -849,12 +1047,20 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1': - resolution: {integrity: sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3': + resolution: {integrity: sha512-9TGZuAX+liGkNKkwuo3FYJu7gHWT0vkBcf7GkOe7s7fmC19XwH/4u5u7sDIFrMooe558ORcmuBvBz7Ur5PlbHw==} peerDependencies: typescript: '>= 4.3.x' vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1077,10 +1283,6 @@ packages: cpu: [x64] os: [win32] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2009,28 +2211,46 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@storybook/addon-a11y@10.1.2': - resolution: {integrity: sha512-O4Rp+ftREulVLREOKwx3IKkUiSLx+F3V5zQ++h6bzlmwRuaknMHs5P3qSnjJprYV6K8qXTRbvgR2qKBgga8Qxw==} + '@storybook/addon-a11y@10.2.0-alpha.14': + resolution: {integrity: sha512-RdW2P3QLta8p4FGrQZWrcgdhuyA6PqoLVQ1Xr+hOOYDkO1Zhr1KgcoGDYtGYM0GMm//Z40dLkYkqYPWCs4g2pg==} peerDependencies: - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 - '@storybook/addon-docs@10.1.2': - resolution: {integrity: sha512-2D89qp6WwNxbiyylixJDC9C8tU8qgRS68HFcYruSNVX3dcCoty7xVytdWJIoDdTjvYlKZZdK23eD9r7+AEA3oA==} + '@storybook/addon-docs@10.2.0-alpha.14': + resolution: {integrity: sha512-4kQUfHk2uxrEfLVYyE5N7f94K33iWqIRUfxRd+NhT+SUwJkVHPQ8BqQ29Z9qwJ7RO6otAH2RwZO/BmZBqTgECA==} peerDependencies: - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 + + '@storybook/addon-vitest@10.2.0-alpha.14': + resolution: {integrity: sha512-wCb3lQyul+7TFjyJEu12GYfbd0bmc8qoPCFX6te1D79sS+SK4PBK2sjq8/3OH3VLtUbSrutw2uaTD7aQQsFrDw==} + peerDependencies: + '@vitest/browser': ^3.0.0 || ^4.0.0 + '@vitest/browser-playwright': ^4.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 + storybook: ^10.2.0-alpha.14 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/runner': + optional: true + vitest: + optional: true - '@storybook/builder-vite@10.1.2': - resolution: {integrity: sha512-gEIduoOUQZL0xS3LJu/9WjPRppg2wptNp6ifLZiRYF6R3T0q4IBSzQ3oXIeHOcwhKUW//vRSuci2NDe1llUjMw==} + '@storybook/builder-vite@10.2.0-alpha.14': + resolution: {integrity: sha512-PZQ0oSVSfKaGe+h48MSGoH3wmcYRPbcmuX8G7GLORViW+x7gILLr0e3D5MF5C9rc4BHpfxikaXa3Gq5krUYeKQ==} peerDependencies: - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/csf-plugin@10.1.2': - resolution: {integrity: sha512-dwFKoKsV73SEKdaA78/AZlMa8+pAt2YS8f8cAvRLtsCxus9u0sJqxR/5axlZk0KLHnoJ+exZDD/zpK/HCsqtNw==} + '@storybook/csf-plugin@10.2.0-alpha.14': + resolution: {integrity: sha512-uACCM01OIzeBPOFeNDygxjIqLTlYDQHpzVvMveRz8kcHrP+1jOi6Cq5efMXMmmtb2ltwhr39bblkqLcZCz0lkg==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 vite: '*' webpack: '*' peerDependenciesMeta: @@ -2052,27 +2272,27 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.1.2': - resolution: {integrity: sha512-HxmL6rD99qaZervm3S/g0QjflSpCk31kZX6+guLBD85KzH+sgo7XNjlipNqfdzAOfWpri0rh6zEGyNI0erFlag==} + '@storybook/react-dom-shim@10.2.0-alpha.14': + resolution: {integrity: sha512-QXIfC1XF1wRV2Y3/K7+yhqNUiWkStixk08SNOQ83RUI8ULzKb4GltRHyvt16gjQHHp+8LB5duV6PY8GI/3Il2g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 - '@storybook/react-vite@10.1.2': - resolution: {integrity: sha512-S7as4gJtoYTmFqlpOhLcLiUlA4uxg7zsUTbNtmbszk3P7ZNCHU2WvVuE/h4CXZJNzhtsuzbNjXudKtMNcRXL1A==} + '@storybook/react-vite@10.2.0-alpha.14': + resolution: {integrity: sha512-mrT0kUObQwc4G73f8FKEXdvPT1cZ/iORW8S7KrIyeV83u0FcMPxHcZcYGLIfdvpOgLNRBK6pCSadIKLV5pOD6w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 - '@storybook/react@10.1.2': - resolution: {integrity: sha512-9fWP4EdQ2nYCtKr9wHGFrzxb/osPv0wJ3bbcZxZ1xN6RyDFDFBkwBJpl7NN+3KTFloXZJXYOkkc5rqmbMPi0fA==} + '@storybook/react@10.2.0-alpha.14': + resolution: {integrity: sha512-IsBgaa+3Erluy5asRTwxZcMeSfo9x8TVuixUXd3LTOpdbi5rjgF7MrEDyNe8oDnxH4vEiQnX59QzMaMZ7CDZmw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.1.2 + storybook: ^10.2.0-alpha.14 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: @@ -2292,6 +2512,9 @@ packages: '@types/react@18.3.26': resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} + '@types/react@19.2.6': + resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} @@ -2304,6 +2527,102 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.47.0': + resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.47.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.47.0': + resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.47.0': + resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.52.0': + resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.47.0': + resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.52.0': + resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.47.0': + resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.52.0': + resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.47.0': + resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.47.0': + resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.52.0': + resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.47.0': + resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.52.0': + resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.47.0': + resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.52.0': + resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.47.0': + resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.52.0': + resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@valibot/to-json-schema@1.3.0': resolution: {integrity: sha512-82Vv6x7sOYhv5YmTRgSppSqj1nn2pMCk5BqCMGWYp0V/fq+qirrbGncqZAtZ09/lrO40ne/7z8ejwE728aVreg==} peerDependencies: @@ -2753,6 +3072,10 @@ packages: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3066,6 +3389,23 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-storybook@10.2.0-alpha.14: + resolution: {integrity: sha512-W5K/nx+HWNKqWL3bY+9Cn+oJpE+StDAt1s8RDPmWAEj+SnHD7MUY3JZlZ7y5I4tEFZS6yMQKZKri7iz/kKq7Vw==} + peerDependencies: + eslint: '>=8' + storybook: ^10.2.0-alpha.14 + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -3307,8 +3647,9 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} hasBin: true globals@14.0.0: @@ -3333,6 +3674,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3351,6 +3695,12 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -3383,6 +3733,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} @@ -3490,8 +3844,9 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} @@ -3599,8 +3954,9 @@ packages: lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3696,6 +4052,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3887,9 +4247,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} @@ -4359,6 +4719,14 @@ packages: react: ^18 || ^19 react-dom: ^18 || ^19 + reshaped@3.9.0: + resolution: {integrity: sha512-lIrTyxgAiCpZtcZoH4QfQb+hO0MeLDDZZ71BRwnYMsvXSkPtqilj5luJfGd9HfoioSUQ5j5oXl0BPBsTXPRClQ==} + hasBin: true + peerDependencies: + postcss: ^8 + react: ^18 || ^19 + react-dom: ^18 || ^19 + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4590,8 +4958,8 @@ packages: peerDependencies: storybook: ^10.0.0 - storybook@10.1.2: - resolution: {integrity: sha512-yFL15WVQJeagmptyRadd2cwJlMVCo6xPoTPt/R+lQXIJmsTDHOFl5cZooIsvgALe3hTi5hsuVL3pG2bPEUuYGg==} + storybook@10.2.0-alpha.14: + resolution: {integrity: sha512-ZyGO3qVK1ARL08nyVSpb88k8KGIJTJ62nsVNdUCTeyDct/0G7OXFS3F/Ogk5o0EAAS3iymVBKnX/76v/PH3keA==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -4741,6 +5109,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -4851,6 +5225,13 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typescript-eslint@8.47.0: + resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5154,6 +5535,12 @@ packages: peerDependencies: zod: ^3.24.1 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -5626,6 +6013,11 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@2.6.1))': + dependencies: + eslint: 9.39.1(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.1': @@ -5753,6 +6145,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.0 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5762,10 +6160,9 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.3(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))': dependencies: - glob: 10.4.5 - magic-string: 0.30.21 + glob: 11.1.0 react-docgen-typescript: 2.4.0(typescript@5.9.3) vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) optionalDependencies: @@ -5832,6 +6229,12 @@ snapshots: '@types/react': 18.3.26 react: 19.2.0 + '@mdx-js/react@3.1.1(@types/react@19.2.6)(react@19.2.0)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.6 + react: 19.2.0 + '@modelcontextprotocol/inspector-cli@0.17.2': dependencies: '@modelcontextprotocol/sdk': 1.22.0 @@ -6069,11 +6472,7 @@ snapshots: '@oxlint/win32-x64@1.28.0': optional: true - '@pkgjs/parseargs@0.11.0': - optional: true - - '@polka/url@1.0.0-next.29': - optional: true + '@polka/url@1.0.0-next.29': {} '@publint/pack@0.1.2': {} @@ -7347,21 +7746,38 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@storybook/addon-a11y@10.1.2(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': + '@storybook/addon-a11y@10.2.0-alpha.14(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/addon-docs@10.1.2(@types/react@18.3.26)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/addon-docs@10.2.0-alpha.14(@types/react@18.3.26)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: '@mdx-js/react': 3.1.1(@types/react@18.3.26)(react@19.2.0) - '@storybook/csf-plugin': 10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/csf-plugin': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/react-dom-shim': 10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - esbuild + - rollup + - vite + - webpack + + '@storybook/addon-docs@10.2.0-alpha.14(@types/react@19.2.6)(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.6)(react@19.2.0) + '@storybook/csf-plugin': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-dom-shim': 10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@storybook/react-dom-shim': 10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -7370,11 +7786,25 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/addon-vitest@10.2.0-alpha.14(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.0.6)': dependencies: - '@storybook/csf-plugin': 10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + optionalDependencies: + '@vitest/browser': 4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6) + '@vitest/browser-playwright': 4.0.6(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6) + '@vitest/runner': 4.0.6 + vitest: 4.0.6(@types/node@24.10.1)(@vitest/browser-playwright@4.0.6)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/builder-vite@10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + dependencies: + '@storybook/csf-plugin': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) transitivePeerDependencies: @@ -7383,11 +7813,11 @@ snapshots: - rollup - webpack - '@storybook/builder-vite@10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/builder-vite@10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: - '@storybook/csf-plugin': 10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/csf-plugin': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) '@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ts-dedent: 2.2.0 vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) transitivePeerDependencies: @@ -7396,9 +7826,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/csf-plugin@10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) unplugin: 2.3.10 optionalDependencies: esbuild: 0.25.12 @@ -7406,9 +7836,9 @@ snapshots: vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) webpack: 5.102.1(@swc/core@1.13.5)(esbuild@0.25.12) - '@storybook/csf-plugin@10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/csf-plugin@10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) unplugin: 2.3.10 optionalDependencies: esbuild: 0.25.12 @@ -7428,37 +7858,37 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/react-dom-shim@10.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@storybook/react-dom-shim@10.2.0-alpha.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim@10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@storybook/react-dom-shim@10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim@10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': + '@storybook/react-dom-shim@10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-vite@10.1.2(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/react-vite@10.2.0-alpha.14(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - '@storybook/builder-vite': 10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) - '@storybook/react': 10.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + '@storybook/builder-vite': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/react': 10.2.0-alpha.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 18.3.1 react-docgen: 8.0.2 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.11 - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tsconfig-paths: 4.2.0 vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) transitivePeerDependencies: @@ -7469,19 +7899,19 @@ snapshots: - typescript - webpack - '@storybook/react-vite@10.1.2(esbuild@0.25.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': + '@storybook/react-vite@10.2.0-alpha.14(esbuild@0.25.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.3(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - '@storybook/builder-vite': 10.1.2(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) - '@storybook/react': 10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3) + '@storybook/builder-vite': 10.2.0-alpha.14(esbuild@0.25.12)(rollup@4.52.5)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(webpack@5.102.1(@swc/core@1.13.5)(esbuild@0.25.12)) + '@storybook/react': 10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.0 react-docgen: 8.0.2 react-dom: 19.2.0(react@19.2.0) resolve: 1.22.11 - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tsconfig-paths: 4.2.0 vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) transitivePeerDependencies: @@ -7492,27 +7922,27 @@ snapshots: - typescript - webpack - '@storybook/react@10.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)': + '@storybook/react@10.2.0-alpha.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@storybook/react-dom-shim': 10.2.0-alpha.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: 18.3.1 react-docgen: 8.0.2 react-dom: 18.3.1(react@18.3.1) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@storybook/react@10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)': + '@storybook/react@10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.1.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + '@storybook/react-dom-shim': 10.2.0-alpha.14(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) react: 19.2.0 react-docgen: 8.0.2 react-dom: 19.2.0(react@19.2.0) - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -7712,6 +8142,10 @@ snapshots: '@types/react': 18.3.26 optional: true + '@types/react-dom@19.2.3(@types/react@19.2.6)': + dependencies: + '@types/react': 19.2.6 + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -7726,6 +8160,10 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/react@19.2.6': + dependencies: + csstype: 3.2.3 + '@types/react@19.2.7': dependencies: csstype: 3.2.3 @@ -7737,6 +8175,150 @@ snapshots: '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.47.0 + '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.47.0 + eslint: 9.39.1(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.47.0 + '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.47.0 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.47.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) + '@typescript-eslint/types': 8.47.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.47.0': + dependencies: + '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/visitor-keys': 8.47.0 + + '@typescript-eslint/scope-manager@8.52.0': + dependencies: + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 + + '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.47.0': {} + + '@typescript-eslint/types@8.52.0': {} + + '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.47.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) + '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/visitor-keys': 8.47.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.47.0 + '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.52.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.52.0 + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.47.0': + dependencies: + '@typescript-eslint/types': 8.47.0 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.52.0': + dependencies: + '@typescript-eslint/types': 8.52.0 + eslint-visitor-keys: 4.2.1 + '@valibot/to-json-schema@1.3.0(valibot@1.2.0(typescript@5.9.3))': dependencies: valibot: 1.2.0(typescript@5.9.3) @@ -7775,6 +8357,19 @@ snapshots: - vite optional: true + '@vitest/browser-playwright@4.0.6(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6)': + dependencies: + '@vitest/browser': 4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6) + '@vitest/mocker': 4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + playwright: 1.56.1 + tinyrainbow: 3.0.3 + vitest: 4.0.6(@types/node@24.10.1)(@vitest/browser-playwright@4.0.6)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@4.0.6(vite@7.2.2(@types/node@20.19.0)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6)': dependencies: '@vitest/mocker': 4.0.6(vite@7.2.2(@types/node@20.19.0)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) @@ -7793,6 +8388,23 @@ snapshots: - vite optional: true + '@vitest/browser@4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6)': + dependencies: + '@vitest/mocker': 4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + '@vitest/utils': 4.0.6 + magic-string: 0.30.21 + pixelmatch: 7.1.0 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.0.3 + vitest: 4.0.6(@types/node@24.10.1)(@vitest/browser-playwright@4.0.6)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/coverage-v8@4.0.6(@vitest/browser@4.0.6(vite@7.2.2(@types/node@20.19.0)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6))(vitest@4.0.6)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -7845,6 +8457,14 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@20.19.0)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + '@vitest/mocker@4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))': + dependencies: + '@vitest/spy': 4.0.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -8251,6 +8871,8 @@ snapshots: commander@14.0.1: {} + commander@14.0.2: {} + commander@2.20.3: optional: true @@ -8553,6 +9175,30 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + eslint: 9.39.1(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): + dependencies: + eslint: 9.39.1(jiti@2.6.1) + + eslint-plugin-storybook@10.2.0-alpha.14(eslint@9.39.1(jiti@2.6.1))(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.52.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + transitivePeerDependencies: + - supports-color + - typescript + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -8838,14 +9484,14 @@ snapshots: glob-to-regexp@0.4.1: optional: true - glob@10.4.5: + glob@11.1.0: dependencies: foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 + jackspeak: 4.1.1 + minimatch: 10.1.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 + path-scurry: 2.0.1 globals@14.0.0: {} @@ -8866,6 +9512,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -8886,6 +9534,12 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} @@ -8914,6 +9568,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-size@0.5.5: optional: true @@ -9001,11 +9657,9 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: + jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 jest-worker@27.5.1: dependencies: @@ -9109,7 +9763,7 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 - lru-cache@10.4.3: {} + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: @@ -9190,6 +9844,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -9211,8 +9869,7 @@ snapshots: mri@1.2.0: {} - mrmime@2.0.1: - optional: true + mrmime@2.0.1: {} ms@2.1.3: {} @@ -9371,9 +10028,9 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: + path-scurry@2.0.1: dependencies: - lru-cache: 10.4.3 + lru-cache: 11.2.4 minipass: 7.1.2 path-to-regexp@3.3.0: {} @@ -9397,7 +10054,6 @@ snapshots: pixelmatch@7.1.0: dependencies: pngjs: 7.0.0 - optional: true pkce-challenge@4.1.0: {} @@ -9434,8 +10090,7 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - pngjs@7.0.0: - optional: true + pngjs@7.0.0: {} postcss-calc@10.1.1(postcss@8.5.6): dependencies: @@ -9921,6 +10576,19 @@ snapshots: transitivePeerDependencies: - encoding + reshaped@3.9.0(postcss@8.5.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@csstools/postcss-global-data': 3.1.0(postcss@8.5.6) + chalk: 4.1.2 + commander: 14.0.2 + cssnano: 7.1.1(postcss@8.5.6) + csstype: 3.1.3 + culori: 4.0.2 + postcss: 8.5.6 + postcss-custom-media: 11.0.6(postcss@8.5.6) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -10173,7 +10841,6 @@ snapshots: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 - optional: true sisteransi@1.0.5: {} @@ -10219,11 +10886,11 @@ snapshots: std-env@3.10.0: {} - storybook-addon-test-codegen@3.0.0(storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)): + storybook-addon-test-codegen@3.0.0(storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)): dependencies: - storybook: 10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + storybook: 10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10232,6 +10899,7 @@ snapshots: '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 esbuild: 0.25.12 + open: 10.2.0 recast: 0.23.11 semver: 7.7.3 use-sync-external-store: 1.6.0(react@18.3.1) @@ -10245,7 +10913,7 @@ snapshots: - react-dom - utf-8-validate - storybook@10.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + storybook@10.2.0-alpha.14(@testing-library/dom@10.4.1)(prettier@3.6.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -10254,6 +10922,7 @@ snapshots: '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 esbuild: 0.25.12 + open: 10.2.0 recast: 0.23.11 semver: 7.7.3 use-sync-external-store: 1.6.0(react@19.2.0) @@ -10388,13 +11057,16 @@ snapshots: toidentifier@1.0.1: {} - totalist@3.0.1: - optional: true + totalist@3.0.1: {} tr46@0.0.3: {} tree-kill@1.2.2: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-dedent@2.2.0: {} ts-node@10.9.2(@swc/core@1.13.5)(@types/node@20.19.0)(typescript@5.9.3): @@ -10498,6 +11170,17 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} ufo@1.6.1: {} @@ -10684,6 +11367,45 @@ snapshots: - tsx - yaml + vitest@4.0.6(@types/node@24.10.1)(@vitest/browser-playwright@4.0.6)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0): + dependencies: + '@vitest/expect': 4.0.6 + '@vitest/mocker': 4.0.6(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0)) + '@vitest/pretty-format': 4.0.6 + '@vitest/runner': 4.0.6 + '@vitest/snapshot': 4.0.6 + '@vitest/spy': 4.0.6 + '@vitest/utils': 4.0.6 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + '@vitest/browser-playwright': 4.0.6(playwright@1.56.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.0))(vitest@4.0.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 @@ -10796,4 +11518,8 @@ snapshots: dependencies: zod: 3.25.76 + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 80233c9e..5b3a1b09 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,15 +6,15 @@ packages: - eval/evals/*/experiments/*/project catalog: - '@storybook/addon-a11y': 10.1.2 - '@storybook/addon-docs': 10.1.2 - '@storybook/addon-vitest': 10.1.2 - '@storybook/react-vite': 10.1.2 + '@storybook/addon-a11y': 10.2.0-alpha.14 + '@storybook/addon-docs': 10.2.0-alpha.14 + '@storybook/addon-vitest': 10.2.0-alpha.14 + '@storybook/react-vite': 10.2.0-alpha.14 '@tmcp/adapter-valibot': ^0.1.4 '@tmcp/transport-http': ^0.8.0 '@tmcp/transport-stdio': ^0.4.1 - eslint-plugin-storybook: 10.1.2 - storybook: 10.1.2 + eslint-plugin-storybook: 10.2.0-alpha.14 + storybook: 10.2.0-alpha.14 tmcp: ^1.16.0 tsdown: ^0.15.12 typescript: ^5.9.3 @@ -25,10 +25,10 @@ catalog: catalogs: experiments: '@eslint/js': 9.39.1 - '@storybook/addon-a11y': 10.1.2 - '@storybook/addon-docs': 10.1.2 - '@storybook/addon-vitest': 10.1.2 - '@storybook/react-vite': 10.1.2 + '@storybook/addon-a11y': 10.2.0-alpha.14 + '@storybook/addon-docs': 10.2.0-alpha.14 + '@storybook/addon-vitest': 10.2.0-alpha.14 + '@storybook/react-vite': 10.2.0-alpha.14 '@types/node': 24.10.1 '@types/react': 19.2.6 '@types/react-dom': 19.2.3 @@ -37,11 +37,11 @@ catalogs: eslint: 9.39.1 eslint-plugin-react-hooks: 7.0.1 eslint-plugin-react-refresh: 0.4.24 - eslint-plugin-storybook: 10.1.2 + eslint-plugin-storybook: 10.2.0-alpha.14 globals: 16.5.0 react: 19.2.0 react-dom: 19.2.0 - storybook: 10.1.2 + storybook: 10.2.0-alpha.14 typescript: 5.9.3 typescript-eslint: 8.47.0 vite: 7.2.2