diff --git a/.changeset/config.json b/.changeset/config.json index 1c09d9df..a586c786 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@storybook/mcp-internal-storybook"] + "ignore": ["@storybook/mcp-internal-storybook", "@storybook/mcp-eval*"] } diff --git a/.changeset/upset-worlds-bow.md b/.changeset/upset-worlds-bow.md new file mode 100644 index 00000000..c4316cdc --- /dev/null +++ b/.changeset/upset-worlds-bow.md @@ -0,0 +1,5 @@ +--- +'@storybook/addon-mcp': patch +--- + +Improve visibility into which toolsets are available diff --git a/.github/actions/setup-node-and-install/action.yml b/.github/actions/setup-node-and-install/action.yml index 0efa11da..0a135261 100644 --- a/.github/actions/setup-node-and-install/action.yml +++ b/.github/actions/setup-node-and-install/action.yml @@ -1,22 +1,20 @@ name: 'Setup Node.js and Dependencies' -description: 'Sets up Node.js, caches and installs dependencies' +description: 'Sets up Node.js, caches, installs dependencies and turbo cache' runs: using: 'composite' steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Use Node.js 24 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: 24 - cache: 'pnpm' + cache: pnpm + node-version-file: '.nvmrc' + + - name: Cache for Turbo + uses: rharkor/caching-for-turbo@2b4b5b14a8d16b8556a58993a8ac331d56d8906d # v2.3.2 - name: Install dependencies shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d935b51f..3562bca3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -35,7 +35,8 @@ The addon supports configuring which toolsets are enabled: toolsets: { dev: true, // get-story-urls, get-ui-building-instructions docs: true, // list-all-components, get-component-documentation - } + }, + experimentalFormat: 'markdown' // Output format: 'markdown' (default) or 'xml' } } ``` @@ -61,12 +62,23 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic: - Uses `tmcp` with HTTP transport and Valibot schema validation - Factory pattern: `createStorybookMcpHandler()` returns a request handler -- Context-based: handlers accept `StorybookContext` to override source URLs and provide optional callbacks +- Context-based: handlers accept `StorybookContext` which includes the HTTP `Request` object and optional callbacks - **Exports tools and types** for reuse by `addon-mcp` and other consumers +- **Request-based manifest loading**: The `request` property in context is passed to tools, which use it to determine the manifest URL (defaults to same origin, replacing `/mcp` with the manifest path) +- **Optional manifestProvider**: Custom function to override default manifest fetching behavior + - Signature: `(request: Request, path: string) => Promise` + - Receives the `Request` object and a `path` parameter (currently always `'./manifests/components.json'`) + - The provider determines the base URL (e.g., mapping to S3 buckets) while the MCP server handles the path + - 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 +- **Output Format**: The `format` property in context controls output format: + - `'markdown'` (default): Token-efficient markdown with adaptive formatting + - `'xml'`: Legacy XML format + - Format is configurable via addon options or directly in `StorybookContext` + - Formatters are implemented in `packages/mcp/src/utils/manifest-formatter/` with separate files for XML and markdown ## Development Environment @@ -89,9 +101,39 @@ The `@storybook/mcp` package (in `packages/mcp`) is framework-agnostic: **Testing:** -- Only `packages/mcp` has tests (Vitest with coverage) -- Run `pnpm test run --coverage` in mcp package -- Prefer TDD when adding new tools +- **Unit tests**: Both `packages/mcp` and `packages/addon-mcp` have unit tests (Vitest with coverage) + - Run `pnpm test run --coverage` in individual package directories + - Run `pnpm test:run` at root to run all unit tests + - Prefer TDD when adding new tools +- **E2E tests**: `apps/internal-storybook/tests` contains E2E tests for the addon + - Run `pnpm test` in `apps/internal-storybook` directory + - Tests verify MCP endpoint works with latest Storybook prereleases + - Uses inline snapshots for response validation + - **When to update E2E tests**: + - Adding or modifying MCP tools (update tool discovery snapshots) + - Changing MCP protocol implementation (update session init tests) + - Modifying tool responses or schemas (update tool-specific tests) + - Adding new toolsets or changing toolset behavior (update filtering tests) + - **Running tests**: + - `pnpm test` in apps/internal-storybook - run E2E tests + - `pnpm vitest run -u` - update snapshots when responses change + - Tests start Storybook server automatically, wait for MCP endpoint, then run + - **Test structure**: + - `mcp-endpoint.e2e.test.ts` - MCP protocol and tool tests + - `check-deps.e2e.test.ts` - Storybook version validation + +**Formatting and checks (CRITICAL):** + +- **ALWAYS format code after making changes**: Run `pnpm run format` before committing +- **ALWAYS run checks after formatting**: Run `pnpm run check` to ensure all checks pass +- **Fix any failing checks**: Analyze check results and fix issues until all checks pass +- **This is mandatory for every commit** - formatting checks will fail in CI if not done +- The workflow is: + 1. Make your code changes + 2. Run `pnpm run format` to format all files + 3. Run `pnpm run check` to verify all checks pass + 4. Fix any failing checks and repeat step 3 until all pass + 5. Commit your changes **Type checking:** @@ -221,11 +263,12 @@ export { addMyTool, MY_TOOL_NAME } from './tools/my-tool.ts'; - Checks `features.experimentalComponentsManifest` flag - Checks for `experimental_componentManifestGenerator` preset - Only registers `addListAllComponentsTool` and `addGetComponentDocumentationTool` when enabled -- Context includes `source` URL pointing to `/manifests/components.json` endpoint +- 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, found components, and not found IDs + - `onGetComponentDocumentation`: Called when get tool is invoked, receives context, input with componentId, and optional foundComponent - Addon-mcp uses these handlers to collect telemetry on tool usage **Storybook internals used:** @@ -288,6 +331,7 @@ For detailed package-specific guidance, see: - `packages/addon-mcp/**` → `.github/instructions/addon-mcp.instructions.md` - `packages/mcp/**` → `.github/instructions/mcp.instructions.md` +- `eval/**` → `.github/instructions/eval.instructions.md` ## Documentation resources diff --git a/.github/instructions/addon-mcp.instructions.md b/.github/instructions/addon-mcp.instructions.md index fdda1587..a1fa386d 100644 --- a/.github/instructions/addon-mcp.instructions.md +++ b/.github/instructions/addon-mcp.instructions.md @@ -133,7 +133,7 @@ pnpm storybook # From root - starts internal-storybook with addon in dev mode ### Formatting -Use Prettier at the root level: +Use prettier at the root level: ```bash pnpm format # From root @@ -149,17 +149,24 @@ Launches the MCP inspector for debugging the addon's MCP server using the config ### Testing -The addon has comprehensive unit tests covering all utilities and tools: +The addon has comprehensive unit tests covering all utilities and tools. Tests can be run at the package level or from the monorepo root: ```bash +# From the package directory pnpm test # Run tests in watch mode pnpm test run # Run tests once pnpm test run --coverage # Run tests with coverage report + +# From the monorepo root (runs tests across all packages) +pnpm test # Run all tests in watch mode +pnpm test:run # Run all tests once +pnpm test:ci # Run tests with coverage and CI reporters ``` **Test Infrastructure:** -- **Framework**: Vitest 3.2.4 with @vitest/coverage-v8 +- **Framework**: Vitest with @vitest/coverage-v8 +- **Configuration**: Root-level vitest.config.ts with per-package projects - **Fixtures**: JSON fixtures in `fixtures/` directory for story index data **Test Coverage Baseline:** @@ -212,11 +219,11 @@ When adding new functionality: 3. Mock external dependencies (fetch, logger, telemetry) 4. Use fixtures for complex data structures 5. Test both success and error paths -6. Run `pnpm test run --coverage` to verify coverage +6. Run `pnpm test run --coverage` (from package) or `pnpm test:ci` (from root) to verify coverage **CI Integration:** -Tests run automatically on PRs and main branch pushes via `.github/workflows/check.yml` as the `test-addon-mcp` job. +Tests run automatically on PRs and main branch pushes via `.github/workflows/check.yml` in the `test` job, which runs `pnpm turbo run test:ci` across all packages. ## Code Style and Conventions @@ -229,7 +236,7 @@ Tests run automatically on PRs and main branch pushes via `.github/workflows/che ### Code Style -- Use Prettier for formatting (inherited from root config) +- Use prettier for formatting (inherited from root config) - Prefer async/await over callbacks - Export types and interfaces explicitly - Use descriptive variable and function names @@ -410,7 +417,6 @@ This addon implements MCP using `tmcp`: - `storybook` - Peer dependency (Storybook framework) - `valibot` - Schema validation for tool inputs/outputs -- `ts-dedent` - Template string formatting - `tsdown` - Build tool (rolldown-based) - `vite` - Peer dependency for middleware injection diff --git a/.github/instructions/eval.instructions.md b/.github/instructions/eval.instructions.md new file mode 100644 index 00000000..6ec703c0 --- /dev/null +++ b/.github/instructions/eval.instructions.md @@ -0,0 +1,687 @@ +````instructions +--- +applyTo: 'eval/**' +--- + +# Copilot Instructions for Storybook MCP Eval Framework + +## Project Overview + +This is an evaluation framework for testing AI coding agents' ability to build UI components using Storybook and MCP tools. The framework automates the process of running experiments, executing agents with prompts, and evaluating the results through multiple quality metrics. + +**Core Purpose**: Measure how effectively AI agents can use Storybook's MCP tools to build production-quality UI components. + +## Architecture + +### Framework Flow + +1. **Prepare**: Create fresh Vite + React + Storybook project from template +2. **Execute**: Run AI agent (Claude Code CLI) with prompt and optional context +3. **Evaluate**: Run automated checks (build, typecheck, lint, tests, a11y) +4. **Report**: Generate metrics, save results, optionally upload to Google Sheets + +### Key Components + +- **CLI Interface**: Interactive and non-interactive modes via `eval.ts` +- **Context System**: Four context modes (none, component manifest, MCP server, extra prompts) +- **Agent Abstraction**: Pluggable agent implementations (currently Claude Code CLI) +- **Evaluation Pipeline**: Parallel execution of multiple quality checks +- **Hooks System**: Lifecycle hooks for custom experiment logic +- **Telemetry**: Optional results upload to Google Sheets for tracking + +### File Structure + +``` +eval/ +├── eval.ts # Main CLI entry point +├── types.ts # Core types and schemas +├── lib/ +│ ├── collect-args.ts # Interactive CLI argument collection +│ ├── show-help.ts # Help text formatting +│ ├── generate-prompt.ts # Combines prompt parts +│ ├── prepare-experiment.ts # Project template setup +│ ├── agents/ +│ │ └── claude-code-cli.ts # Claude Code CLI agent implementation +│ └── evaluations/ +│ ├── evaluate.ts # Main evaluation orchestrator +│ ├── prepare-evaluations.ts # Install test dependencies +│ ├── build.ts # Vite build check +│ ├── typecheck.ts # TypeScript checking +│ ├── lint.ts # ESLint execution +│ ├── test-stories.ts # Vitest + a11y testing +│ ├── environment.ts # Git branch/commit tracking +│ └── save-to-sheets.ts # Google Sheets upload +├── evals/ # Evaluation definitions +│ └── {number}-{name}/ +│ ├── prompt.md # Main prompt +│ ├── hooks.ts # Optional lifecycle hooks +│ ├── components.json # Optional component manifest +│ ├── mcp.config.json # Optional MCP server config +│ ├── *.md # Optional additional context +│ ├── pre-evaluate/ # Optional files to copy before evaluation +│ ├── {hook-name}/ # Optional hook directories (see Lifecycle Hooks) +│ └── experiments/ # Generated experiment runs +└── templates/ + ├── project/ # Base Vite + React template + └── evaluation/ # Test/lint configs +``` + +### Context Modes + +The framework supports four distinct context types: + +1. **No Context** (`--no-context`): + - Agent uses only built-in tools + - Tests baseline agent capabilities + +2. **Component Manifest** (`--context components.json`): + - Provides component documentation via `@storybook/mcp` package + - Uses stdio transport with `packages/mcp/bin.ts` + - Best for testing agents with library/component documentation + - This uses the Storybook MCP server, not a custom MCP server + +3. **MCP Server** (`--context mcp.config.json` or inline JSON): + - Custom MCP server configuration (HTTP or stdio) + - Supports multiple named servers + - Flexible for testing different MCP tool combinations + - Use this for fully custom MCP servers; use components.json for Storybook MCP + +4. **Extra Prompts** (`--context extra-prompt-01.md,extra-prompt-02.md`): + - Appends additional markdown files to main prompt + - Useful for providing supplementary instructions + - Keeps main prompt clean while testing variations + +## Development Workflow + +### Prerequisites + +- Node.js 24+ (see root `.nvmrc`) +- pnpm 10.19.0+ (monorepo root enforces this) +- Claude Code CLI: `npm install -g claude-code` + +### Running Evaluations + +**Interactive mode (recommended):** +```bash +cd eval +node eval.ts +``` + +**Non-interactive mode:** +```bash +node eval.ts --agent claude-code --context components.json --upload --no-storybook 100-flight-booking-plain +``` + +**IMPORTANT**: Always use the `--no-storybook` flag when running evals to prevent the process from hanging at the end waiting for user input about starting Storybook. + +**Get help:** +```bash +node eval.ts --help +``` + +### Creating a New Eval + +1. **Create directory:** + ```bash + mkdir evals/200-my-component + ``` + +2. **Write `prompt.md`:** + ```markdown + Build a SearchBar component with autocomplete... + + + 1. Component MUST be default export in src/components/SearchBar.tsx + 2. Component MUST have data-testid="search-bar" + + ``` + +3. **Optional: Add context files:** + - `components.json` - Component manifest for `@storybook/mcp` + - `mcp.config.json` - Custom MCP server configuration + - `extra-prompt-*.md` - Supplementary instructions + +4. **Optional: Create `hooks.ts`:** + ```typescript + import type { Hooks } from '../../types.ts'; + + export default { + async postPrepareExperiment(args, log) { + // Custom setup (e.g., copy fixture data) + await fs.cp( + path.join(args.evalPath, 'fixtures'), + path.join(args.projectPath, 'public/fixtures'), + { recursive: true } + ); + } + } satisfies Hooks; + ``` + +5. **Optional: Create hook directories:** + - Create directories named after lifecycle hooks in kebab-case + - Files in these directories are copied to `projectPath` at that lifecycle point + - Example: `pre-evaluate/stories/MyComponent.stories.ts` copies test stories before evaluation + - See [Lifecycle Hooks](#lifecycle-hooks) for the full list of supported directories + +### Viewing Results + +**Conversation viewer (visualize agent activity):** +```bash +open conversation-viewer.html +# Select the full-conversation.js file from results/ +``` + +**Inspect generated project:** +```bash +cd evals/{eval-name}/experiments/{experiment-name}/project +pnpm storybook +``` + +## Code Style and Conventions + +### TypeScript Configuration + +- Uses `@tsconfig/node24` as base +- Module system: ESM with `"type": "module"` +- Module resolution: `bundler` +- Strict mode enabled + +### Code Style + +- **Always include file extensions in imports**: `import { foo } from './bar.ts'` +- Use Valibot for schema validation (see `types.ts` and `collect-args.ts`) +- Prefer async/await over callbacks +- Use `taskLog` (verbose) or `spinner` (normal) for user feedback +- Export types explicitly + +### Naming Conventions + +- Constants: SCREAMING_SNAKE_CASE (rare in this codebase) +- Functions: camelCase (e.g., `collectArgs`, `prepareExperiment`) +- Types/Interfaces: PascalCase (e.g., `ExperimentArgs`, `Context`) + +### Argument Parsing Pattern + +The `collect-args.ts` module demonstrates a robust pattern for handling CLI arguments: + +1. Parse raw args with `node:util.parseArgs` +2. Validate with Valibot schemas (including async transformations) +3. Prompt for missing values using `@clack/prompts` +4. Build rerun command incrementally for user convenience +5. Return fully-resolved arguments + +**Example:** +```typescript +const parsedArgValues = await v.parseAsync(ArgValuesSchema, nodeParsedArgs.values); + +const result = await p.group({ + agent: async () => { + if (parsedArgValues.agent) return parsedArgValues.agent; + return await p.select({ message: '...', options: [...] }); + } +}); +``` + +## Important Files + +### Core Framework + +- `eval.ts` - Main entry point, orchestrates entire flow +- `types.ts` - All TypeScript types and Valibot schemas +- `lib/collect-args.ts` - CLI argument parsing and validation +- `lib/show-help.ts` - Help text formatting +- `lib/generate-prompt.ts` - Combines prompt parts with constraints +- `lib/prepare-experiment.ts` - Project template setup + +### Agent Integration + +- `lib/agents/claude-code-cli.ts` - Claude Code CLI wrapper + - Streams JSON messages from Claude + - Parses tool calls and todo lists + - Calculates token counts using `ai-tokenizer` + - Tracks conversation for debugging + +### Evaluation Pipeline + +- `lib/evaluations/evaluate.ts` - Main orchestrator + - Runs checks in parallel: build, typecheck, lint, test, environment + - Creates unified logging interface (verbose vs. normal) + - Formats results and optionally uploads +- `lib/evaluations/prepare-evaluations.ts` - Installs test dependencies +- `lib/evaluations/build.ts` - Vite build verification +- `lib/evaluations/typecheck.ts` - TypeScript compilation check +- `lib/evaluations/lint.ts` - ESLint execution +- `lib/evaluations/test-stories.ts` - Vitest + a11y testing +- `lib/evaluations/save-to-sheets.ts` - Google Sheets upload + +### Templates + +- `templates/project/` - Base Vite + React + Storybook template + - Minimal setup with TypeScript + - `src/main.tsx` - React root (agents modify this) + - `vite.config.ts` - Vite configuration +- `templates/evaluation/` - Testing infrastructure + - `.storybook/` - Storybook config with Vitest addon + - `eslint.config.js` - ESLint rules + - `vitest.config.ts` - Vitest + a11y setup + +## Agent Implementation Details + +### Claude Code CLI Integration + +The Claude Code CLI agent (`lib/agents/claude-code-cli.ts`) implements a sophisticated integration: + +**Key Features:** + +1. **Auto-approval of MCP servers**: Sends "1\n" to stdin to automatically trust MCP servers +2. **Streaming JSON parsing**: Parses `--output-format=stream-json` line-by-line +3. **Token counting**: Uses `ai-tokenizer` with Claude encoding to calculate tokens per message +4. **Cost tracking**: Calculates USD cost based on Anthropic pricing +5. **Todo list tracking**: Extracts todo progress from `TodoWrite` tool calls for progress display +6. **Conversation logging**: Saves complete conversation with metadata to `full-conversation.js` + +**Message Types:** + +- `SystemInitMessage`: Session start, tools available, MCP servers +- `AssistantMessage`: Agent responses with text and/or tool calls +- `UserMessage`: Tool results from user +- `ResultMessage`: Final summary with usage stats + +**Output Format:** + +The agent generates `full-conversation.js` that's viewable in `conversation-viewer.html`: +```javascript +const prompt = `...`; +const promptTokenCount = 1234; +const promptCost = 0.0123; +const messages = [...]; // All messages with metadata +globalThis.loadConversation?.({ prompt, promptTokenCount, promptCost, messages }); +``` + +### Adding a New Agent + +To add support for a new coding agent: + +1. Create `lib/agents/my-agent.ts` +2. Implement the `Agent` interface from `types.ts`: + ```typescript + export const myAgent: Agent = { + async execute(prompt, experimentArgs, mcpServerConfig) { + // 1. Setup MCP config if provided + // 2. Execute agent with prompt + // 3. Stream/parse output + // 4. Save conversation log + // 5. Return ExecutionSummary + return { cost, duration, durationApi, turns }; + } + }; + ``` +3. Add to `agents` object in `eval.ts` +4. Update `ArgValuesSchema` in `collect-args.ts` to include new agent option + +## Evaluation Metrics + +Each experiment produces comprehensive metrics: + +### Execution Metrics (from agent) + +- **cost**: Total API cost in USD +- **duration**: Total execution time in seconds +- **durationApi**: API request time in seconds +- **turns**: Number of conversation turns + +### Quality Metrics (from evaluation) + +- **buildSuccess**: Boolean - can the project build? +- **typeCheckErrors**: Number of TypeScript errors +- **lintErrors**: Number of ESLint errors +- **test.passed**: Number of passing tests +- **test.failed**: Number of failing tests +- **a11y.violations**: Number of accessibility violations + +### Output Files + +**`summary.json`**: Complete metrics +```json +{ + "cost": 0.1234, + "duration": 45, + "durationApi": 38, + "turns": 8, + "buildSuccess": true, + "typeCheckErrors": 0, + "lintErrors": 0, + "test": { "passed": 3, "failed": 0 }, + "a11y": { "violations": 1 } +} +``` + +**`full-conversation.js`**: Complete conversation log for debugging +**`test-results.json`**: Detailed Vitest results with a11y violations +**`build-output.txt`**: Vite build logs +**`typecheck-output.txt`**: TypeScript compiler output +**`lint-output.txt`**: ESLint output + +## Lifecycle Hooks + +Evals can customize behavior at each lifecycle step through two mechanisms: + +### Hook Directories + +Create directories named after lifecycle hooks (kebab-case) to automatically copy files to `projectPath` at that step: + +| Directory | When Contents Are Copied | +|-----------|-------------------------| +| `pre-prepare-experiment/` | Before project template is copied | +| `post-prepare-experiment/` | After dependencies are installed | +| `pre-execute-agent/` | Before agent starts execution | +| `post-execute-agent/` | After agent completes | +| `pre-evaluate/` | Before evaluation runs | +| `post-evaluate/` | After evaluation completes | +| `pre-save/` | Before results are saved | +| `post-save/` | After results are saved | + +**Example:** To add test stories that run against agent-generated components: +``` +evals/200-my-component/ +├── prompt.md +├── pre-evaluate/ +│ └── stories/ +│ └── MyComponent.stories.ts +``` + +The `pre-evaluate/stories/MyComponent.stories.ts` file will be copied to `project/stories/MyComponent.stories.ts` before evaluation runs. + +Directories merge with existing content in `projectPath`, and files overwrite if they already exist. + +### Hook Functions + +For programmatic customization, define hooks in `hooks.ts`: + +```typescript +import type { Hooks } from '../../types.ts'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +export default { + // Before project template is copied (after pre-prepare-experiment/ is copied) + prePrepareExperiment: async (args, log) => { + log.message('Custom pre-preparation'); + }, + + // After dependencies are installed (after post-prepare-experiment/ is copied) + postPrepareExperiment: async (args, log) => { + // Install additional dependencies + await addDependency('some-package', { cwd: args.projectPath, silent: true }); + }, + + // Before agent starts (after pre-execute-agent/ is copied) + preExecuteAgent: async (args, log) => { + log.message('Starting agent'); + }, + + // After agent completes (after post-execute-agent/ is copied) + postExecuteAgent: async (args, log) => { + log.message('Agent finished'); + }, + + // Before evaluation runs (after pre-evaluate/ is copied) + preEvaluate: async (args, log) => { + log.start('Custom pre-evaluation'); + }, + + // After evaluation completes (after post-evaluate/ is copied) + postEvaluate: async (args, log) => { + log.success('Custom post-evaluation'); + }, + + // Before results are saved (after pre-save/ is copied) + preSave: async (args, log) => { + log.message('Saving results'); + }, + + // After results are saved (after post-save/ is copied) + postSave: async (args, log) => { + log.success('All done'); + } +} satisfies Hooks; +``` + +**Execution Order:** For each lifecycle step, the framework first copies files from the hook directory (if it exists), then calls the hook function (if defined). + +**Logger Interface:** + +Both `taskLog` (verbose) and `spinner` (normal) are wrapped in a unified interface: +- `start(title)`: Start a new task +- `success(message)`: Mark task as successful +- `error(message)`: Mark task as failed +- `message(message)`: Log a message +- `complete(message)`: Complete the entire operation + +## Prompt Engineering + +### Best Practices + +1. **Use ``**: Specify exact file paths, component names, and testable criteria +2. **Use MUST/SHOULD/MAY**: Clear requirement priority +3. **Specify test identifiers**: Use `data-testid` for reliable testing +4. **Define exact content**: Specify button text, labels, placeholders +5. **Keep prompts focused**: Use extra prompts for supplementary info + +### Example Prompt Structure + +```markdown +Build a {component} that includes: + +- Feature 1 +- Feature 2 +- Feature 3 + + + 1. Component MUST be default export in src/components/{Name}.tsx + 2. Component MUST be added to main.tsx + 3. Component MUST take optional onSubmit prop + 4. Element X SHOULD have data-testid="x" + 5. Element Y SHOULD have "Text" as content + +``` + +### Constraints System + +The framework automatically appends constraints to all prompts (see `generate-prompt.ts`): + +```markdown + + IMPORTANT: Do not run npm, pnpm, yarn, or any package manager commands. + Dependencies have already been installed. Do not run build, test, or + dev server commands. Just write the code files. + +``` + +This prevents agents from running unnecessary commands and keeps them focused on code generation. + +## Testing Strategy + +### Test Infrastructure + +Each experiment's project includes: + +- **Vitest**: For running component tests +- **Playwright**: For browser automation +- **@storybook/addon-vitest**: For story-based testing +- **@storybook/addon-a11y**: For accessibility testing +- **ESLint**: For code quality + +### Expected Stories + +Evals should include `pre-evaluate/stories/*.stories.ts` files that: + +1. Import the component +2. Define basic stories (e.g., Default) +3. Use `play` functions for interaction testing +4. Export as default story objects + +**Example:** +```typescript +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; +import { FlightBooking } from '../src/components/FlightBooking'; + +const meta = { + component: FlightBooking, + tags: ['test'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByTestId('submit')); + await expect(canvas.getByText('Success')).toBeInTheDocument(); + } +}; +``` + +### Accessibility Testing + +The framework uses `@storybook/addon-a11y` which runs Axe checks on all stories: + +- Violations are counted per story +- Total violations across all passing tests are reported +- Failed tests don't contribute to a11y metrics + +## Dependencies + +### Framework Dependencies + +- `@clack/prompts` - Interactive CLI prompts +- `valibot` - Schema validation +- `tinyexec` - Command execution +- `nypm` - Package manager detection and operations +- `ai-tokenizer` - Token counting for Claude + +### Template Dependencies + +- **Project template**: Vite + React + TypeScript (minimal) +- **Evaluation template**: Vitest + Playwright + Storybook + ESLint + a11y + +### Agent Dependencies + +- `claude-code` - Claude Code CLI (must be installed globally) + +## Google Sheets Integration + +The framework can optionally upload results to Google Sheets for tracking experiments over time. + +**How it works:** + +1. Uses Google Apps Script web app as proxy +2. Appends row with metrics to spreadsheet +3. Includes git branch/commit for context +4. Respects `--upload` / `--no-upload` flag + +**Setup** (for maintainers): + +- Google Apps Script code is in `google-apps-script.js` +- Deployed as web app with spreadsheet access +- URL is hardcoded in `save-to-sheets.ts` + +## Conversation Viewer + +The `conversation-viewer.html` file provides a web-based interface for viewing agent conversations: + +**Features:** + +- Timeline view of all messages +- Token counts and costs per message +- Tool call visualization +- Todo list progress tracking +- Collapsible message details + +**Usage:** + +1. Open `conversation-viewer.html` in browser +2. Select `results/full-conversation.js` file +3. Browse conversation chronologically + +## MCP Server Configuration + +### Component Manifest Pattern + +When using `--context components.json`, the framework: + +1. Reads the manifest file from the eval directory +2. Creates `.mcp.json` in project with stdio server config: + ```json + { + "mcpServers": { + "storybook-mcp": { + "type": "stdio", + "command": "node", + "args": ["../../packages/mcp/bin.ts", "--manifestPath", "/path/to/components.json"] + } + } + } + ``` +3. Agent receives MCP tools from `@storybook/mcp` package + +### Custom MCP Server Pattern + +When using `--context mcp.config.json`, the framework: + +1. Reads the config file (or parses inline JSON) +2. Validates against `McpServerConfigSchema` +3. Writes to project's `.mcp.json` +4. Agent connects to specified servers (HTTP or stdio) + +**Example config:** +```json +{ + "my-server": { + "type": "http", + "url": "http://localhost:6006/mcp", + "headers": { "X-Custom": "value" } + } +} +``` + +## Tips for Development + +### Debugging Failed Experiments + +1. **Check `full-conversation.js`**: See exact agent activity +2. **Review `build-output.txt`**: Build errors +3. **Check `typecheck-output.txt`**: TypeScript issues +4. **Inspect `lint-output.txt`**: Code quality problems +5. **Read `test-results.json`**: Test failures and a11y violations +6. **Compare with `pre-evaluate/`**: See reference files copied before evaluation + +### Common Issues + +- **Dependencies not installed**: Framework handles this, but hooks may need to wait +- **MCP server not trusted**: Framework auto-approves via stdin +- **Tests fail to run**: Check that stories are in `stories/` directory and have `tags: ['test']` +- **Build fails**: Agent may have created invalid TypeScript + +### Performance Optimization + +- Evaluations run in parallel (build, typecheck, lint, test) +- Use `--verbose` only for debugging (slower) +- Skip `--upload` for faster local iteration + +## Notes for AI Assistants + +- The framework is designed for reproducibility - same inputs should give comparable outputs +- Always check `collect-args.ts` for the canonical list of CLI options +- Hooks are optional - most evals only need `pre-evaluate/` for test stories +- Hook directories copy files first, then hook functions run +- Extra prompts are append-only - they don't replace the main prompt +- The `CONSTRAINTS_PROMPT` is always appended to prevent package manager usage +- Agent token counting is approximate - uses client-side tokenizer, not actual API response +- Coverage metrics track quality trends across experiments +- The conversation viewer is critical for debugging agent behavior +- All experiment artifacts are saved - nothing is deleted automatically +- Timestamps use local time with timezone offset for consistent naming +```` diff --git a/.github/instructions/mcp.instructions.md b/.github/instructions/mcp.instructions.md index d55373f6..4ca8918b 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 +- **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. ### File Structure @@ -42,6 +42,59 @@ src/ 1. **Factory Pattern**: `createStorybookMcpHandler()` creates configured handler instances 2. **Tool Registration**: Tools are added to the server using `server.tool()` method 3. **Async Handler**: Returns a Promise-based request handler compatible with standard HTTP servers +4. **Request-based Context**: The `Request` object is passed through context to tools, which use it to construct the manifest URL + +### Manifest Provider API + +The handler accepts a `StorybookContext` with the following key properties: + +- **`request`**: The HTTP `Request` object being processed (automatically passed by the handler) +- **`manifestProvider`**: Optional custom function `(request: Request, path: string) => Promise` to override default manifest fetching + - **Parameters**: + - `request`: The HTTP `Request` object to determine base URL, headers, routing, etc. + - `path`: The manifest path (currently always `'./manifests/components.json'`) + - **Responsibility**: The provider determines the "first part" of the URL (base URL/origin) by examining the request. The MCP server provides the path. + - Default behavior: Constructs URL from request origin, replacing `/mcp` with the provided path + - Return value should be the manifest JSON as a string + +**Example with custom manifestProvider (local filesystem):** + +```typescript +import { createStorybookMcpHandler } from '@storybook/mcp'; +import { readFile } from 'node:fs/promises'; + +const handler = await createStorybookMcpHandler({ + manifestProvider: async (request, path) => { + // Custom logic: read from local filesystem + // The provider decides on the base path, MCP provides the manifest path + const basePath = '/path/to/manifests'; + // Remove leading './' from path if present + const normalizedPath = path.replace(/^\.\//, ''); + const fullPath = `${basePath}/${normalizedPath}`; + return await readFile(fullPath, 'utf-8'); + }, +}); +``` + +**Example with custom manifestProvider (S3 bucket mapping):** + +```typescript +import { createStorybookMcpHandler } from '@storybook/mcp'; + +const handler = await createStorybookMcpHandler({ + manifestProvider: async (request, path) => { + // Map requests to different S3 buckets based on hostname + const url = new URL(request.url); + const bucket = url.hostname.includes('staging') + ? 'staging-bucket' + : 'prod-bucket'; + const normalizedPath = path.replace(/^\.\, ''); + const manifestUrl = `https://${bucket}.s3.amazonaws.com/${normalizedPath}`; + const response = await fetch(manifestUrl); + return await response.text(); + }, +}); +``` ### Component Manifest and ReactDocgen Support @@ -113,24 +166,28 @@ Runs the development server with hot reload using Node's `--watch` flag. pnpm format ``` -Formats code using Prettier. +Formats code using prettier. To check formatting without applying changes: ```bash -pnpm format --check +pnpm format:check ``` ### Testing -```bash -pnpm test run -``` - -Or with coverage enabled: +Tests can be run at the package level or from the monorepo root: ```bash -pnpm test run --coverage +# From the package directory +pnpm test # Run tests in watch mode +pnpm test run # Run tests once +pnpm test run --coverage # Run tests with coverage + +# From the monorepo root (runs tests across all packages) +pnpm test # Run all tests in watch mode +pnpm test:run # Run all tests once +pnpm test:ci # Run tests with coverage and CI reporters ``` **Important**: Vitest automatically clears all mocks between tests, so you should never need to call `vi.clearAllMocks()` in a `beforeEach` hook. @@ -154,7 +211,7 @@ Launches the MCP inspector for debugging the MCP server using the configuration ### Code Style -- Use Prettier for formatting (config: `.prettierignore`) +- Use prettier for formatting (config: `.prettierrc`) - Prefer async/await over callbacks - Export types and interfaces explicitly - Use descriptive variable and function names diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec34b67e..5a093921 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,21 +1,24 @@ -name: Check and publish preview releases +name: Check on: pull_request: push: branches: - main + - next -permissions: - id-token: write # used to upload artifacts to codecov +env: + TURBO_ENV_MODE: loose jobs: - build-mcp: - name: Build @storybook/mcp + build: + name: Build runs-on: ubuntu-latest + permissions: + id-token: write # used to upload artifacts to codecov steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 2 # see https://docs.codecov.com/docs/environment-specific-requirements#github-actions @@ -23,176 +26,103 @@ jobs: uses: ./.github/actions/setup-node-and-install - name: Build - run: pnpm build --env-mode=loose --filter @storybook/mcp - - build-addon-mcp: - name: Build @storybook/addon-mcp - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 # see https://docs.codecov.com/docs/environment-specific-requirements#github-actions - - - name: Setup Node.js and Install Dependencies - uses: ./.github/actions/setup-node-and-install - - - name: Build - run: pnpm build --env-mode=loose --filter @storybook/addon-mcp + run: pnpm turbo run build build-storybook: name: Build internal Storybook runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - name: Build - run: pnpm build-storybook + run: pnpm turbo run build-storybook - test-mcp: - name: Test @storybook/mcp + test: + name: Test runs-on: ubuntu-latest + permissions: + id-token: write # used to upload artifacts to codecov steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 2 # see https://docs.codecov.com/docs/environment-specific-requirements#github-actions - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - name: Run tests with coverage - run: pnpm --filter @storybook/mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml + run: pnpm turbo run test:ci - - name: Upload test and coverage artifact - uses: actions/upload-artifact@v4 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: - name: test-mcp - path: | - packages/mcp/coverage/ - packages/mcp/test-report.junit.xml - - test-addon-mcp: - name: Test @storybook/addon-mcp - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js and Install Dependencies - uses: ./.github/actions/setup-node-and-install - - - name: Build @storybook/mcp (dependency) - run: pnpm build --filter @storybook/mcp - - - name: Run tests with coverage - run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml + use_oidc: true + fail_ci_if_error: true + files: | + coverage/lcov.info - - name: Upload test and coverage artifact - uses: actions/upload-artifact@v4 + - name: Upload test results to Codecov + uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + if: always() with: - name: test-addon-mcp - path: | - packages/addon-mcp/coverage/ - packages/addon-mcp/test-report.junit.xml + use_oidc: true + fail_ci_if_error: true + files: | + test-report.junit.xml - typecheck-mcp: - name: Type check @storybook/mcp + typecheck: + name: Type check runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - name: Run type checking - run: pnpm --filter @storybook/mcp typecheck + run: pnpm turbo run typecheck - typecheck-addon-mcp: - name: Type check @storybook/addon-mcp + publint: + name: Publint runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - - name: Build @storybook/mcp (dependency) - run: pnpm build --filter @storybook/mcp + - name: Run linting + run: pnpm turbo run publint - - name: Run type checking - run: pnpm --filter @storybook/addon-mcp typecheck - - release-preview: - name: Publish preview releases + lint: + name: Lint runs-on: ubuntu-latest - needs: [build-mcp, build-addon-mcp] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - - name: Build - run: pnpm build - - - name: Publish preview release - run: pnpm pkg-pr-new publish --pnpm --compact --no-template './packages/*' + - name: Run linting + run: pnpm turbo run lint:ci format-check: name: Check formatting runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Node.js and Install Dependencies uses: ./.github/actions/setup-node-and-install - name: Check formatting - run: pnpm format --check - - collect-coverage: - name: Collect coverage and test results - runs-on: ubuntu-latest - needs: [test-mcp, test-addon-mcp] - if: always() - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 # see https://docs.codecov.com/docs/environment-specific-requirements#github-actions - - - name: Download @storybook/mcp coverage - uses: actions/download-artifact@v4 - with: - name: test-mcp - path: packages/mcp/ - - - name: Download @storybook/addon-mcp coverage - uses: actions/download-artifact@v4 - with: - name: test-addon-mcp - path: packages/addon-mcp/ - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - use_oidc: true - fail_ci_if_error: true - files: | - packages/mcp/coverage/lcov.info - packages/addon-mcp/coverage/lcov.info - - name: Upload test results to Codecov - uses: codecov/test-results-action@v1 - with: - use_oidc: true - fail_ci_if_error: true - files: | - packages/mcp/test-report.junit.xml - packages/addon-mcp/test-report.junit.xml + run: pnpm turbo run format:check diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..264d3ccd --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,37 @@ +name: 'Copilot Setup Steps' + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +env: + TURBO_ENV_MODE: loose + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Build packages + run: pnpm turbo run build diff --git a/.github/workflows/publish-preview.yml b/.github/workflows/publish-preview.yml new file mode 100644 index 00000000..9530cc4d --- /dev/null +++ b/.github/workflows/publish-preview.yml @@ -0,0 +1,28 @@ +name: Publish preview + +on: + pull_request: + push: + branches: + - main + - next + +env: + TURBO_ENV_MODE: loose + +jobs: + release-preview: + name: Publish preview releases + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Build + run: pnpm build + + - name: Publish preview releases + run: pnpm pkg-pr-new publish --pnpm --no-template './packages/*' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69375cc1..13d82165 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: push: branches: - main + - next + +env: + TURBO_ENV_MODE: loose jobs: release: @@ -17,14 +21,14 @@ jobs: runs-on: ubuntu-latest steps: # see https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 id: app-token with: app-id: ${{ vars.STORYBOOK_BOT_APP_ID }} private-key: ${{ secrets.STORYBOOK_BOT_APP_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 @@ -34,9 +38,9 @@ jobs: - name: Create Release Pull Request or Publish to npm id: changesets - uses: changesets/action@v1 + uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 with: - # This expects you to have a script called release which does a build for your packages and calls changeset publish publish: pnpm release + commitMode: github-api env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index 66be58e4..81a46bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ coverage/ test-report.junit.xml # Turborepo .turbo + +*storybook.log +experiments/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 1b2e093d..eeeb7be5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1 @@ -dist -pnpm-lock.yaml -coverage +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 8f94d3d8..d5d1ddb0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "useTabs": true, - "singleQuote": true + "singleQuote": true, + "trailingComma": "all" } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bb7fe4cd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "oxc.enable": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "oxc.typeAware": true, + "vitest.disableWorkspaceWarning": true +} diff --git a/README.md b/README.md index 2076ba52..95893feb 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,17 @@ pnpm build # Start development mode (watches for changes in all packages) pnpm dev -# Run unit tests +# Run unit tests in watch mode pnpm test +# Run unit tests once +pnpm test:run + # Run Storybook with the addon for testing -pnpm storybook +pnpm --filter internal-storybook storybook ``` -The `pnpm storybook` command starts: +The Storybook command starts: - The internal test Storybook instance on `http://localhost:6006` - The addon in watch mode, so changes are reflected automatically @@ -63,57 +66,38 @@ The `pnpm storybook` command starts: ### Development -The `dev` command runs all packages in watch mode, automatically rebuilding when you make changes: +The `turbo watch build` command runs all packages in watch mode, automatically rebuilding when you make changes: ```bash # Start development mode for all packages -pnpm dev +pnpm turbo watch build ``` -This runs: - -- `packages/addon-mcp` in watch mode (using `tsdown --watch`) -- `packages/mcp` in watch mode (using Node's `--watch` flag) - -**Note:** Running `pnpm storybook` automatically starts the addon in dev mode alongside Storybook. In this mode, making changes to `addon-mcp` will automatically restart Storybook. So you typically only need one command: - ```bash # This is usually all you need - starts Storybook AND watches addon for changes pnpm storybook ``` -For more advanced workflows, you can run dev mode for a specific package: - -```bash -# Watch only the addon package -pnpm --filter @storybook/addon-mcp dev - -# Watch only the mcp package -pnpm --filter @storybook/mcp dev -``` - ### Building ```bash # Build all packages pnpm build - -# Build a specific package -pnpm --filter @storybook/addon-mcp build -pnpm --filter @storybook/mcp build ``` ### Testing +The monorepo uses a centralized Vitest configuration at the root level with projects configured for each package: + ```bash -# Watch tests +# Watch tests across all packages pnpm test -# Run tests -pnpm test run +# Run tests once across all packages +pnpm test:run -# Run tests with coverage -pnpm test run --coverage +# Run tests with coverage and CI reporters +pnpm test:ci ``` ### Debugging MCP Servers @@ -158,7 +142,7 @@ curl -X POST http://localhost:13316/mcp \ ### Debugging with Storybook -You can start the Storybook with +You can start Storybook with: ```bash pnpm storybook @@ -166,14 +150,44 @@ pnpm storybook This will build everything and start up Storybook with addon-mcp, and you can then connect your coding agent to it at `http://localhost:6006/mcp` and try it out. -### Formatting +### Formatting & Linting ```bash -# Format all files +# Format all files with Prettier pnpm format # Check formatting without changing files -pnpm format --check +pnpm format:check + +# Lint code with oxlint +pnpm lint + +# Lint with GitHub Actions format (for CI) +pnpm lint:ci + +# Check package exports with publint +pnpm publint +``` + +## 🔍 Quality Checks + +The monorepo includes several quality checks that run in CI: + +```bash +# Run all checks (build, test, lint, format, typecheck, publint) +pnpm check + +# Run checks in watch mode (experimental) +pnpm check:watch + +# Type checking (uses tsc directly, not turbo) +pnpm typecheck + +# Type checking with turbo (for individual packages) +pnpm turbo:typecheck + +# Testing with turbo (for individual packages) +pnpm turbo:test ``` ## 📝 Code Conventions @@ -224,8 +238,10 @@ We welcome contributions! Here's how to get started: ### Before Submitting - [ ] Code builds without errors (`pnpm build`) -- [ ] Tests pass (`pnpm test`) +- [ ] Tests pass (`pnpm test:run`) - [ ] Code is formatted (`pnpm format`) +- [ ] Code is linted (`pnpm lint`) +- [ ] Type checking passes (`pnpm typecheck`) - [ ] Changes tested with MCP inspector or internal Storybook - [ ] Changeset created if necessary (`pnpm changeset`) diff --git a/apps/internal-storybook/package.json b/apps/internal-storybook/package.json index 38f380d2..935f836e 100644 --- a/apps/internal-storybook/package.json +++ b/apps/internal-storybook/package.json @@ -5,8 +5,9 @@ "description": "The Storybook used to test the MCP packages in the MCP monorepo", "scripts": { "build-storybook": "storybook build", - "storybook": "storybook dev --port 6006 --loglevel debug", - "typecheck": "tsc --noEmit" + "storybook": "storybook dev --port 6006 --loglevel debug --no-open", + "test": "cd ../.. && pnpm vitest --project=e2e", + "typecheck": "tsc" }, "devDependencies": { "@storybook/addon-docs": "catalog:", @@ -14,9 +15,11 @@ "@storybook/react-vite": "catalog:", "@types/react": "^18.2.65", "@types/react-dom": "^18.2.21", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "storybook": "catalog:" + "storybook": "catalog:", + "tinyexec": "^1.0.2", + "vite": "catalog:" } } diff --git a/apps/internal-storybook/stories/components/Page.stories.ts b/apps/internal-storybook/stories/components/Page.stories.ts index 082c9155..4b8862bd 100644 --- a/apps/internal-storybook/stories/components/Page.stories.ts +++ b/apps/internal-storybook/stories/components/Page.stories.ts @@ -20,7 +20,7 @@ export const LoggedOut: Story = {}; export const LoggedIn: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const loginButton = await canvas.getByRole('button', { + const loginButton = canvas.getByRole('button', { name: /Log in/i, }); await userEvent.click(loginButton); diff --git a/apps/internal-storybook/tests/check-deps.e2e.test.ts b/apps/internal-storybook/tests/check-deps.e2e.test.ts new file mode 100644 index 00000000..7264e9fd --- /dev/null +++ b/apps/internal-storybook/tests/check-deps.e2e.test.ts @@ -0,0 +1,55 @@ +import { describe, it } from 'vitest'; +import { x } from 'tinyexec'; + +const PACKAGES_TO_CHECK = [ + '@storybook/addon-docs', + '@storybook/react-vite', + 'storybook', +]; + +describe('Storybook Dependencies', () => { + it('should be using latest versions from registry', async () => { + const outdated: Array<{ pkg: string; current: string; latest: string }> = + []; + + for (const pkg of PACKAGES_TO_CHECK) { + // Get local version + const listResult = await x('pnpm', ['list', pkg, '--json'], { + nodeOptions: { cwd: process.cwd() }, + }); + 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() }, + }); + const latestVersion = viewResult.stdout.trim(); + + // Compare versions + if (currentVersion !== latestVersion) { + outdated.push({ pkg, current: currentVersion, latest: latestVersion }); + } + } + + // If there are outdated packages, fail the test with instructions + if (outdated.length > 0) { + const outdatedList = outdated + .map(({ pkg, current, latest }) => ` - ${pkg}: ${current} → ${latest}`) + .join('\n'); + + const currentVersion = outdated[0]!.current.replace(/\./g, '\\.'); + const latestVersion = outdated[0]!.latest; + + throw new Error( + `Storybook dependencies are outdated. Update the catalog in pnpm-workspace.yaml:\n\n sed -i '' 's/${currentVersion}/${latestVersion}/g' pnpm-workspace.yaml && pnpm install\n\nOutdated packages:\n${outdatedList}`, + ); + } + }); +}); diff --git a/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts new file mode 100644 index 00000000..b307a26e --- /dev/null +++ b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts @@ -0,0 +1,525 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { x } from 'tinyexec'; + +const STORYBOOK_DIR = new URL('..', import.meta.url).pathname; +const MCP_ENDPOINT = 'http://localhost:6006/mcp'; +const STARTUP_TIMEOUT = 15_000; + +let storybookProcess: ReturnType | null = null; + +/** + * Helper to create MCP protocol requests + */ +function createMCPRequestBody( + method: string, + params: any = {}, + id: number = 1, +) { + return { + jsonrpc: '2.0', + id, + method, + params, + }; +} + +/** + * Helper to make MCP requests + */ +async function mcpRequest(method: string, params: any = {}, id: number = 1) { + const response = await fetch(MCP_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(createMCPRequestBody(method, params, id)), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // MCP responses come as SSE (Server-Sent Events) format + const text = await response.text(); + // Remove "data: " prefix if present + const jsonText = text.replace(/^data: /, '').trim(); + return JSON.parse(jsonText); +} + +/** + * Wait for MCP endpoint to be ready by polling it directly + */ +async function waitForMcpEndpoint( + maxAttempts = 90, + interval = 500, +): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + let attempts = 0; + + const intervalId = setInterval(async () => { + attempts++; + try { + const response = await fetch(MCP_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createMCPRequestBody('tools/list')), + }); + if (response.ok) { + clearInterval(intervalId); + resolve(); + return; + } + } catch (error) { + // Server not ready yet + } + + if (attempts >= maxAttempts) { + clearInterval(intervalId); + reject( + new Error('MCP endpoint failed to start within the timeout period'), + ); + } + }, interval); + + return promise; +} + +describe('MCP Endpoint E2E Tests', () => { + beforeAll(async () => { + storybookProcess = x('pnpm', ['storybook'], { + nodeOptions: { + cwd: STORYBOOK_DIR, + }, + }); + + // Wait for MCP endpoint to be ready + await waitForMcpEndpoint(); + }, STARTUP_TIMEOUT); + + afterAll(async () => { + if (!storybookProcess || !storybookProcess.process) { + return; + } + const kill = Promise.withResolvers(); + storybookProcess.process.on('exit', kill.resolve); + storybookProcess.kill('SIGTERM'); + await kill.promise; + storybookProcess = null; + }); + + describe('Session Initialization', () => { + it('should successfully initialize an MCP session', async () => { + const response = await mcpRequest('initialize', { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { + name: 'e2e-test-client', + version: '1.0.0', + }, + }); + + expect(response).toMatchObject({ + jsonrpc: '2.0', + id: 1, + result: { + protocolVersion: '2025-06-18', + capabilities: { + tools: { listChanged: true }, + }, + serverInfo: { + name: '@storybook/addon-mcp', + description: expect.stringContaining('agents'), + }, + }, + }); + + expect(response.result.serverInfo.version).toBeDefined(); + }); + + it('should return error for invalid protocol version', async () => { + const response = await mcpRequest('initialize', { + protocolVersion: '1.0.0', // Invalid version + capabilities: {}, + clientInfo: { + name: 'test', + version: '1.0.0', + }, + }); + + expect(response.error).toMatchInlineSnapshot(` + { + "code": 0, + "message": "MCP error -32602: Invalid protocol version format", + } + `); + }); + }); + + describe('Tools Discovery', () => { + it('should list available tools', async () => { + const response = await mcpRequest('tools/list'); + + expect(response.result).toHaveProperty('tools'); + // Dev and docs tools should be present + expect(response.result.tools).toHaveLength(4); + + expect(response.result.tools).toMatchInlineSnapshot(` + [ + { + "description": "Get the URL for one or more stories.", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "stories": { + "items": { + "properties": { + "absoluteStoryPath": { + "type": "string", + }, + "explicitStoryName": { + "type": "string", + }, + "exportName": { + "type": "string", + }, + }, + "required": [ + "exportName", + "absoluteStoryPath", + ], + "type": "object", + }, + "type": "array", + }, + }, + "required": [ + "stories", + ], + "type": "object", + }, + "name": "get-story-urls", + "title": "Get stories' URLs", + }, + { + "description": "Instructions on how to do UI component development. + + ALWAYS call this tool before doing any UI/frontend/React/component development, including but not + limited to adding or updating new components, pages, screens or layouts.", + "inputSchema": { + "properties": {}, + "type": "object", + }, + "name": "get-ui-building-instructions", + "title": "UI Component Building Instructions", + }, + { + "description": "List all available UI components from the component library", + "inputSchema": { + "properties": {}, + "type": "object", + }, + "name": "list-all-components", + "title": "List All Components", + }, + { + "description": "Get detailed documentation for a specific UI component", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "componentId": { + "type": "string", + }, + }, + "required": [ + "componentId", + ], + "type": "object", + }, + "name": "get-component-documentation", + "title": "Get Documentation for Component", + }, + ] + `); + }); + }); + + describe('Tool: get-story-urls', () => { + it('should return story URLs for valid stories', async () => { + const cwd = process.cwd(); + const storyPath = cwd.endsWith('/apps/internal-storybook') + ? `${cwd}/stories/components/Button.stories.ts` + : `${cwd}/apps/internal-storybook/stories/components/Button.stories.ts`; + + const response = await mcpRequest('tools/call', { + name: 'get-story-urls', + arguments: { + stories: [ + { + exportName: 'Primary', + absoluteStoryPath: storyPath, + }, + ], + }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "http://localhost:6006/?path=/story/example-button--primary", + "type": "text", + }, + ], + } + `); + }); + + it('should return error message for non-existent story', async () => { + const response = await mcpRequest('tools/call', { + name: 'get-story-urls', + arguments: { + stories: [ + { + exportName: 'NonExistent', + absoluteStoryPath: `${process.cwd()}/stories/components/NonExistent.stories.ts`, + }, + ], + }, + }); + + // The tool returns error messages as regular content, not isError + expect(response.result).toHaveProperty('content'); + expect(response.result.content).toHaveLength(1); + expect(response.result.content[0].text).toContain('No story found'); + expect(response.result.content[0].text).toContain('NonExistent'); + }); + }); + describe('Tool: get-ui-building-instructions', () => { + it('should return UI building instructions', async () => { + const response = await mcpRequest('tools/call', { + name: 'get-ui-building-instructions', + arguments: {}, + }); + + expect(response.result).toHaveProperty('content'); + expect(response.result.content[0]).toHaveProperty('type', 'text'); + + const text = response.result.content[0].text; + expect(text).toContain('stories'); + expect(text.length).toBeGreaterThan(100); + }); + }); + + describe('Tool: list-all-components', () => { + it('should list all components from manifest', async () => { + const response = await mcpRequest('tools/call', { + name: 'list-all-components', + arguments: {}, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Components + + - 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", + "type": "text", + }, + ], + } + `); + }); + }); + + describe('Tool: get-component-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', + arguments: {}, + }); + + const listText = listResponse.result.content[0].text; + // Match markdown format: - ComponentName (component-id) + const idMatch = listText.match(/- \w+ \(([^)]+)\)/); + expect(idMatch).toBeTruthy(); + const componentId = idMatch![1]; + + // Now get documentation for that component + const response = await mcpRequest('tools/call', { + name: 'get-component-documentation', + arguments: { + componentId, + }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "# Button + + ID: example-button + + Primary UI component for user interaction + + ## Stories + + ### Primary + + \`\`\` + import { Button } from "@my-org/my-component-library"; + + const Primary = () => ; + \`\`\` + + ### Secondary + + \`\`\` + import { Button } from "@my-org/my-component-library"; + + const Secondary = () => ; + \`\`\` + + ### Large + + \`\`\` + import { Button } from "@my-org/my-component-library"; + + const Large = () => ; + \`\`\` + + ### Small + + \`\`\` + import { Button } from "@my-org/my-component-library"; + + const Small = () => ; + \`\`\` + + ## Props + + \`\`\` + export type Props = { + /** + Is this the principal call to action on the page? + */ + primary?: boolean = false; + /** + What background color to use + */ + backgroundColor?: string; + /** + How large should the button be? + */ + size?: 'small' | 'medium' | 'large' = 'medium'; + /** + Button contents + */ + label: string; + /** + Optional click handler + */ + onClick?: () => void; + } + \`\`\`", + "type": "text", + }, + ], + } + `); + }); + + it('should return error for non-existent component', async () => { + const response = await mcpRequest('tools/call', { + name: 'get-component-documentation', + arguments: { + componentId: 'non-existent-component-id', + }, + }); + + expect(response.result).toMatchInlineSnapshot(` + { + "content": [ + { + "text": "Component not found: "non-existent-component-id". Use the list-all-components tool to see available components.", + "type": "text", + }, + ], + "isError": true, + } + `); + }); + }); + + describe('Toolset Filtering', () => { + it('should respect X-MCP-Toolsets header for dev tools only', async () => { + const response = await fetch(MCP_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-MCP-Toolsets': 'dev', + }, + body: JSON.stringify(createMCPRequestBody('tools/list')), + }); + + const text = await response.text(); + const jsonText = text.replace(/^data: /, '').trim(); + const result = JSON.parse(jsonText); + + const toolNames = result.result.tools.map((tool: any) => tool.name); + + expect(toolNames).toMatchInlineSnapshot(` + [ + "get-story-urls", + "get-ui-building-instructions", + ] + `); + }); + + it('should respect X-MCP-Toolsets header for docs tools only', async () => { + const response = await fetch(MCP_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-MCP-Toolsets': 'docs', + }, + body: JSON.stringify(createMCPRequestBody('tools/list')), + }); + + const text = await response.text(); + const jsonText = text.replace(/^data: /, '').trim(); + const result = JSON.parse(jsonText); + + const toolNames = result.result.tools.map((tool: any) => tool.name); + + expect(toolNames).toMatchInlineSnapshot(` + [ + "list-all-components", + "get-component-documentation", + ] + `); + }); + }); + + describe('HTTP Methods', () => { + it('should return HTML when Accept header is text/html', async () => { + const response = await fetch(MCP_ENDPOINT, { + method: 'GET', + headers: { + Accept: 'text/html', + }, + }); + + expect(response.ok).toBe(true); + expect(response.headers.get('content-type')).toContain('text/html'); + + const html = await response.text(); + expect(html.toLowerCase()).toContain(''); + }); + }); +}); diff --git a/apps/internal-storybook/tsconfig.json b/apps/internal-storybook/tsconfig.json index 34fb4457..8d0fc06c 100644 --- a/apps/internal-storybook/tsconfig.json +++ b/apps/internal-storybook/tsconfig.json @@ -1,22 +1,17 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2023", // Node 20 according to https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping#node-20 - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "moduleResolution": "bundler", - "module": "esnext", "jsx": "react", - "isolatedModules": true, - "verbatimModuleSyntax": true, - "strict": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noImplicitAny": true, - "lib": ["esnext", "dom", "dom.iterable"], - "baseUrl": "." + "lib": [ + "es2024", + "ESNext.Array", + "ESNext.Collection", + "ESNext.Iterator", + "ESNext.Promise", + "DOM", + "DOM.AsyncIterable", + "DOM.Iterable" + ] }, - "include": ["stories/**/*"] + "include": ["stories/**/*", "**/*.ts"] } diff --git a/apps/internal-storybook/vite.config.ts b/apps/internal-storybook/vite.config.ts index c600eab0..3ccce877 100644 --- a/apps/internal-storybook/vite.config.ts +++ b/apps/internal-storybook/vite.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); diff --git a/apps/internal-storybook/vitest.config.ts b/apps/internal-storybook/vitest.config.ts new file mode 100644 index 00000000..8be9d074 --- /dev/null +++ b/apps/internal-storybook/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + name: 'e2e', + testTimeout: 15_000, + hookTimeout: 15_000, + }, +}); diff --git a/eval/.claude/settings.local.json b/eval/.claude/settings.local.json new file mode 100644 index 00000000..0efb24eb --- /dev/null +++ b/eval/.claude/settings.local.json @@ -0,0 +1,3 @@ +{ + "enabledMcpjsonServers": ["storybook-addon-mcp", "storybook-mcp"] +} diff --git a/eval/.storybook/main.ts b/eval/.storybook/main.ts new file mode 100644 index 00000000..d4f4e69e --- /dev/null +++ b/eval/.storybook/main.ts @@ -0,0 +1,37 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +import { dirname } from 'path'; + +import { fileURLToPath } from 'url'; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value: string): any { + return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); +} + +const config: StorybookConfig = { + stories: [ + '../evals/*/experiments/*/project/stories/*.stories.@(js|jsx|mjs|ts|tsx)', + '../templates/result-docs/*.stories.@(js|jsx|mjs|ts|tsx)', + ], + addons: [ + getAbsolutePath('@storybook/addon-a11y'), + getAbsolutePath('storybook-addon-test-codegen'), + ], + framework: { + name: getAbsolutePath('@storybook/react-vite'), + options: {}, + }, + core: { + builder: { + name: '@storybook/builder-vite', + options: { + viteConfigPath: './templates/project/vite.config.ts', + }, + }, + }, +}; +export default config; diff --git a/eval/.storybook/manager.ts b/eval/.storybook/manager.ts new file mode 100644 index 00000000..131b301d --- /dev/null +++ b/eval/.storybook/manager.ts @@ -0,0 +1,7 @@ +import { addons } from 'storybook/manager-api'; + +addons.setConfig({ + sidebar: { + showRoots: false, + }, +}); diff --git a/eval/.storybook/preview.ts b/eval/.storybook/preview.ts new file mode 100644 index 00000000..5bd16e17 --- /dev/null +++ b/eval/.storybook/preview.ts @@ -0,0 +1,21 @@ +import type { Preview } from '@storybook/react-vite'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo', + }, + }, +}; + +export default preview; diff --git a/eval/.storybook/vitest.setup.ts b/eval/.storybook/vitest.setup.ts new file mode 100644 index 00000000..ea170b04 --- /dev/null +++ b/eval/.storybook/vitest.setup.ts @@ -0,0 +1,7 @@ +import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; +import { setProjectAnnotations } from '@storybook/react-vite'; +import * as projectAnnotations from './preview'; + +// This is an important step to apply the right configuration when testing your stories. +// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations +setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/eval/README.md b/eval/README.md new file mode 100644 index 00000000..f8379594 --- /dev/null +++ b/eval/README.md @@ -0,0 +1,220 @@ +# Storybook MCP Evaluation Framework + +A CLI-based evaluation framework for testing AI coding agents' ability to build UI components with Storybook and MCP tools. + +## What is this? + +This framework runs automated experiments where AI coding agents (like Claude Code CLI) are given prompts to build UI components. Each experiment: + +1. **Prepares** a fresh Vite + React + Storybook project +2. **Executes** the agent with a prompt and optional context (MCP servers, component manifests, or extra prompts) +3. **Evaluates** the results using automated checks: build success, type checking, linting, tests, and accessibility + +The goal is to measure how well agents can use Storybook's MCP tools to build production-quality components. + +> [!NOTE] +> All eval results that are uploaded (opt-outable) are publicly available in [this Google Sheet](https://docs.google.com/spreadsheets/d/1TAvPyK6S6J-Flc1-gNrQpwmd6NWVXoTrQhaQ35y13vw/edit?usp=sharing). + +## Quick Start + +```bash +# Interactive mode (recommended) +node eval.ts + +# With all options specified +node eval.ts --agent claude-code --context components.json --upload 100-flight-booking-plain +``` + +## CLI Options + +| Option | Short | Type | Description | +| ------------- | ----- | ------- | ---------------------------------------------------------------------------------------- | +| `--agent` | `-a` | string | Which agent to use (`claude-code`) | +| `--context` | `-c` | string | Context type: `false`, `*.json` (manifest), `mcp.config.json`, or `*.md` (extra prompts) | +| `--verbose` | `-v` | boolean | Show detailed logs during execution | +| `--storybook` | `-s` | boolean | Auto-start Storybook after completion | +| `--upload` | `-u` | boolean | Upload results to Google Sheets (default: true) | +| `--help` | `-h` | - | Display help information | + +**Positional argument:** The eval directory name (e.g., `100-flight-booking-plain`) + +### Context Types + +The framework supports four context modes: + +1. **No context** (`--no-context`): Agent uses only default tools +2. **Component manifest** (`--context components.json`): Provides component documentation via the `@storybook/mcp` package +3. **MCP server config** (`--context mcp.config.json` or inline JSON): Custom MCP server setup (use this for fully custom MCP servers, not for Storybook MCP) +4. **Extra prompts** (`--context extra-prompt-01.md,extra-prompt-02.md`): Additional markdown files appended to main prompt + +## Project Structure + +``` +eval/ +├── evals/ # Evaluation definitions +│ └── 100-flight-booking-plain/ +│ ├── prompt.md # Main prompt for the agent +│ ├── components.json # Optional: component manifest +│ ├── mcp.config.json # Optional: MCP server config +│ ├── extra-prompt-*.md # Optional: additional context +│ ├── hooks.ts # Optional: lifecycle hooks +│ └── experiments/ # Generated experiment runs +│ └── {context}-{agent}-{timestamp}/ +│ ├── prompt.md # Full prompt sent to agent +│ ├── project/ # Generated project code +│ └── results/ # Evaluation results +│ ├── summary.json +│ ├── full-conversation.js +│ ├── build-output.txt +│ ├── typecheck-output.txt +│ ├── lint-output.txt +│ └── test-results.json +├── templates/ +│ ├── project/ # Base Vite + React + Storybook template +│ └── evaluation/ # Test/lint configs for evaluations +└── lib/ + ├── agents/ # Agent implementations + ├── evaluations/ # Evaluation runners (build, test, lint, etc.) + └── *.ts # Core framework logic +``` + +## Creating an Eval + +1. **Create eval directory:** + + ```bash + mkdir evals/200-my-component + ``` + +2. **Write `prompt.md`:** + + ```markdown + Build a SearchBar component with autocomplete... + + + + 1. Component MUST be default export in src/components/SearchBar.tsx + 2. Component MUST have data-testid="search-bar" + + ``` + +3. **Optional: Add context files:** + - `components.json` - Component manifest for Storybook MCP + - `mcp.config.json` - Custom MCP server configuration + - `extra-prompt-*.md` - Supplementary instructions + +4. **Optional: Create `hooks.ts`:** + + ```typescript + import type { Hooks } from '../../types.ts'; + + export default { + async postPrepareExperiment(args, log) { + // Custom setup (e.g., copy fixtures) + }, + } satisfies Hooks; + ``` + +## Evaluation Metrics + +Each experiment produces: + +- **Build success**: Can the project build without errors? +- **Type check**: TypeScript compilation errors count +- **Lint**: ESLint errors count +- **Tests**: Storybook test results (passed/failed) +- **Accessibility**: Axe violations count +- **Cost**: API usage cost in USD +- **Duration**: Total time and API time in seconds +- **Turns**: Number of agent conversation turns + +## Output Files + +### `summary.json` + +Complete metrics from execution and evaluation: + +```json +{ + "cost": 0.1234, + "duration": 45, + "turns": 8, + "buildSuccess": true, + "typeCheckErrors": 0, + "lintErrors": 0, + "test": { "passed": 3, "failed": 0 }, + "a11y": { "violations": 1 } +} +``` + +### `full-conversation.js` + +Complete conversation log viewable in `conversation-viewer.html`: + +- All assistant and user messages +- Tool calls with arguments +- Token counts and costs per message +- Todo list progress + +### Test Results + +- `test-results.json` - Detailed test outcomes +- `build-output.txt` - Build logs +- `typecheck-output.txt` - TypeScript errors +- `lint-output.txt` - ESLint output + +## Lifecycle Hooks + +Customize experiment behavior with `hooks.ts`: + +```typescript +export default { + prePrepareExperiment: async (args, log) => { + // Before project template copy + }, + postPrepareExperiment: async (args, log) => { + // After dependencies installed + }, + preExecuteAgent: async (args, log) => { + // Before agent starts + }, + postExecuteAgent: async (args, log) => { + // After agent completes + }, + preEvaluate: async (args, log) => { + // Before evaluation runs + }, + postEvaluate: async (args, log) => { + // After evaluation completes + }, +} satisfies Hooks; +``` + +## Viewing Results + +**Conversation viewer:** + +```bash +# Open the HTML file and select the full-conversation.js file +open conversation-viewer.html +``` + +**Storybook:** + +```bash +cd evals/100-flight-booking-plain/experiments/{experiment-name}/project +pnpm storybook +``` + +## Tips + +- Use `--verbose` to see detailed agent activity and tool calls +- Check `full-conversation.js` to debug agent behavior +- Use extra prompts to guide agent without modifying main prompt +- Component manifests work best when agents need library documentation + +## Requirements + +- Node.js 24+ +- pnpm 10.19.0+ +- Claude Code CLI (`npm install -g claude-code`) diff --git a/eval/clean-experiments.ts b/eval/clean-experiments.ts new file mode 100644 index 00000000..cfce1772 --- /dev/null +++ b/eval/clean-experiments.ts @@ -0,0 +1,20 @@ +import { glob, rm } from 'node:fs/promises'; +import * as path from 'node:path'; +import { installDependencies } from 'nypm'; + +const experimentsPaths = await glob('evals/*/experiments'); + +for await (const experimentsPath of experimentsPaths) { + const relativePath = path.relative(process.cwd(), experimentsPath); + try { + await rm(relativePath, { recursive: true, force: true }); + console.log(`Removed: ${relativePath}`); + } catch (error) { + console.error(`Failed to remove ${relativePath}:`, error); + } +} + +console.log('Updating lock file...'); +await installDependencies(); + +console.log('Done!'); diff --git a/eval/eval.ts b/eval/eval.ts new file mode 100644 index 00000000..b34db2f5 --- /dev/null +++ b/eval/eval.ts @@ -0,0 +1,180 @@ +import * as p from '@clack/prompts'; +import { claudeCodeCli } from './lib/agents/claude-code-cli.ts'; +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import type { ExperimentArgs } from './types.ts'; +import { prepareExperiment } from './lib/prepare-experiment.ts'; +import { evaluate } from './lib/evaluations/evaluate.ts'; +import { save } from './lib/save/save.ts'; +import { collectArgs } from './lib/collect-args.ts'; +import { generatePrompt } from './lib/generate-prompt.ts'; +import { x } from 'tinyexec'; +import { styleText } from 'node:util'; +import { showHelp } from './lib/show-help.ts'; + +// Check for --help flag before any processing +if (process.argv.includes('--help') || process.argv.includes('-h')) { + showHelp(); +} + +p.intro('🧪 Storybook MCP Eval'); + +const args = await collectArgs(); + +const evalPath = path.resolve(path.join('evals', args.eval)); +// Validate that eval directory exists +const dirExists = await fs + .access(evalPath) + .then(() => true) + .catch(() => false); +if (!dirExists) { + p.log.error(`Eval directory does not exist: ${evalPath}`); + process.exit(1); +} + +const localDateTimestamp = new Date( + Date.now() - new Date().getTimezoneOffset() * 60000, +) + .toISOString() + .slice(0, 19) + .replace(/[:.]/g, '-'); + +let contextPrefix = ''; +switch (args.context.type) { + case false: + contextPrefix = 'no-context'; + break; + case 'extra-prompts': + contextPrefix = args.context.prompts + .map((prompt) => + path.parse(prompt).name.toLowerCase().replace(/\s+/g, '-'), + ) + .join('-'); + break; + case 'mcp-server': + contextPrefix = Object.keys(args.context.mcpServerConfig) + .map((mcpServerName) => mcpServerName.toLowerCase().replace(/\s+/g, '-')) + .join('-'); + break; + case 'components-manifest': + contextPrefix = 'components-manifest'; + break; +} + +const experimentDirName = `${contextPrefix}-${args.agent}-${localDateTimestamp}`; +const experimentPath = path.join(evalPath, 'experiments', experimentDirName); +const projectPath = path.join(experimentPath, 'project'); +const resultsPath = path.join(experimentPath, 'results'); +const experimentArgs: ExperimentArgs = { + evalPath, + experimentPath, + projectPath, + resultsPath, + verbose: args.verbose, + upload: args.upload, + evalName: args.eval, + context: args.context, + agent: args.agent, + hooks: await import(path.join(evalPath, 'hooks.ts')) + .then((mod) => mod.default) + .catch(() => ({})), +}; + +p.log.info(`Running experiment '${args.eval}' with agent '${args.agent}'`); + +await prepareExperiment(experimentArgs); + +const prompt = await generatePrompt(evalPath, args.context); +await fs.writeFile(path.join(experimentPath, 'prompt.md'), prompt); + +const agents = { + 'claude-code': claudeCodeCli, +}; +const agent = agents[args.agent as keyof typeof agents]; +const promptSummary = await agent.execute( + prompt, + experimentArgs, + args.context.type === 'mcp-server' || + args.context.type === 'components-manifest' + ? args.context.mcpServerConfig + : undefined, +); + +const evaluationSummary = await evaluate(experimentArgs); + +await fs.writeFile( + path.join(resultsPath, 'summary.json'), + JSON.stringify({ ...promptSummary, ...evaluationSummary }, null, 2), +); + +p.log.info('Summary:'); +p.log.message(`🏗️ Build: ${evaluationSummary.buildSuccess ? '✅' : '❌'}`); +p.log.message( + `🔍 Type Check: ${evaluationSummary.typeCheckErrors === 0 ? '✅' : styleText('red', `❌ ${evaluationSummary.typeCheckErrors} errors`)}`, +); +p.log.message( + `✨ Lint: ${evaluationSummary.lintErrors === 0 ? '✅' : styleText('red', `❌ ${evaluationSummary.lintErrors} errors`)}`, +); + +if ( + evaluationSummary.test.failed === 0 && + evaluationSummary.test.passed === 0 +) { + p.log.message(`🧪 Tests: ❌ ${styleText('red', 'Failed to run')}`); + p.log.message(`🦾 Accessibility: ⚠️ ${styleText('yellow', 'Inconclusive')}`); +} else if (evaluationSummary.test.failed > 0) { + p.log.message( + `🧪 Tests: ${styleText('red', `❌ ${evaluationSummary.test.failed} failed`) + ' | ' + styleText('green', `${evaluationSummary.test.passed} passed`)}`, + ); + if (evaluationSummary.test.passed > 0) { + p.log.message( + `🦾 Accessibility: ⚠️ ${styleText('yellow', `${evaluationSummary.a11y.violations} violations from ${evaluationSummary.test.passed}/${evaluationSummary.test.passed + evaluationSummary.test.failed} tests`)}`, + ); + } else { + p.log.message( + `🦾 Accessibility: ⚠️ ${styleText('yellow', 'Inconclusive')}`, + ); + } +} else { + p.log.message('🧪 Tests: ✅'); + p.log.message( + `🦾 Accessibility: ${evaluationSummary.a11y.violations === 0 ? '✅' : styleText('yellow', `⚠️ ${evaluationSummary.a11y.violations} violations`)}`, + ); +} + +p.log.message( + `⏱️ Duration: ${promptSummary.duration}s (API: ${promptSummary.durationApi}s)`, +); +p.log.message(`💰 Cost: $${promptSummary.cost}`); +p.log.message(`🔄 Turns: ${promptSummary.turns}`); + +const chromaticUrl = await save( + experimentArgs, + evaluationSummary, + promptSummary, +); + +if (chromaticUrl) { + p.log.message( + `🔍 View results at:\n\u001b]8;;${chromaticUrl}\u0007${chromaticUrl}\u001b]8;;\u0007`, + ); +} + +const startStorybook = + args.storybook !== undefined + ? args.storybook + : await p.confirm({ + message: "Would you like to start the experiment's Storybook?", + }); + +p.outro('✨ Evaluation complete!'); + +if (startStorybook) { + console.log(''); + await x('pnpm', ['run', 'storybook'], { + nodeOptions: { + cwd: projectPath, + stdio: 'inherit', + }, + }); +} diff --git a/eval/evals/100-flight-booking-plain/extra-prompt-01.md b/eval/evals/100-flight-booking-plain/extra-prompt-01.md new file mode 100644 index 00000000..78f32778 --- /dev/null +++ b/eval/evals/100-flight-booking-plain/extra-prompt-01.md @@ -0,0 +1 @@ +Your final result message should include the word "banana" diff --git a/eval/evals/100-flight-booking-plain/extra-prompt-02.md b/eval/evals/100-flight-booking-plain/extra-prompt-02.md new file mode 100644 index 00000000..79ff3b4f --- /dev/null +++ b/eval/evals/100-flight-booking-plain/extra-prompt-02.md @@ -0,0 +1 @@ +It's extremely important that the submit button has three emojis in it diff --git a/eval/evals/100-flight-booking-plain/full-manifest.fixture.json b/eval/evals/100-flight-booking-plain/full-manifest.fixture.json new file mode 100644 index 00000000..b2e02ba6 --- /dev/null +++ b/eval/evals/100-flight-booking-plain/full-manifest.fixture.json @@ -0,0 +1,524 @@ +{ + "v": 1, + "components": { + "button": { + "id": "button", + "path": "src/components/Button.tsx", + "name": "Button", + "description": "A versatile button component that supports multiple variants, sizes, and states.\n\nThe Button component is a fundamental building block for user interactions. It can be styled as primary, secondary, or tertiary actions, and supports disabled and loading states.\n\n## Usage\n\nButtons should be used for actions that affect the current page or trigger operations. For navigation, consider using a Link component instead.", + "summary": "A versatile button component for user interactions", + "import": "import { Button } from '@storybook/design-system';", + "reactDocgen": { + "props": { + "variant": { + "description": "The visual style variant of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"primary\" | \"secondary\" | \"tertiary\" | \"danger\"", + "elements": [ + { "name": "literal", "value": "\"primary\"" }, + { "name": "literal", "value": "\"secondary\"" }, + { "name": "literal", "value": "\"tertiary\"" }, + { "name": "literal", "value": "\"danger\"" } + ] + }, + "defaultValue": { "value": "\"primary\"", "computed": false } + }, + "size": { + "description": "The size of the button", + "required": false, + "tsType": { + "name": "union", + "raw": "\"small\" | \"medium\" | \"large\"", + "elements": [ + { "name": "literal", "value": "\"small\"" }, + { "name": "literal", "value": "\"medium\"" }, + { "name": "literal", "value": "\"large\"" } + ] + }, + "defaultValue": { "value": "\"medium\"", "computed": false } + }, + "disabled": { + "description": "Whether the button is disabled", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "loading": { + "description": "Whether the button is in a loading state", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "fullWidth": { + "description": "Whether the button should take up the full width of its container", + "required": false, + "tsType": { "name": "boolean" }, + "defaultValue": { "value": "false", "computed": false } + }, + "onClick": { + "description": "Callback function when the button is clicked", + "required": false, + "tsType": { + "name": "signature", + "type": "function", + "signature": { + "arguments": [ + { "name": "event", "type": { "name": "MouseEvent" } } + ], + "return": { "name": "void" } + } + } + }, + "children": { + "description": "The content of the button", + "required": true, + "tsType": { "name": "ReactNode" } + } + } + }, + "stories": [ + { + "id": "button--primary", + "name": "Primary", + "description": "The primary button variant is used for the main call-to-action on a page. It has the highest visual prominence and should be used sparingly to guide users toward the most important action.\n\n## Best Practices\n\n- Use only one primary button per section\n- Keep button text concise and action-oriented\n- Ensure sufficient contrast for accessibility", + "summary": "Primary button with high visual prominence", + "import": "import { Button } from '@storybook/design-system';", + "jsDocTag": [ + { + "key": "example", + "value": "Primary Button" + } + ], + "snippet": "const Primary = () => " + }, + { + "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/eval/evals/100-flight-booking-plain/hooks.ts b/eval/evals/100-flight-booking-plain/hooks.ts new file mode 100644 index 00000000..ed34bf7c --- /dev/null +++ b/eval/evals/100-flight-booking-plain/hooks.ts @@ -0,0 +1,14 @@ +import type { Hooks } from '../../types.ts'; +import { log } from '@clack/prompts'; + +const hooks: Hooks = { + postPrepareExperiment: async (experimentArgs) => { + // Custom logic to run after preparing the experiment + log.success( + `Post-prepare hook executed for experiment at ${experimentArgs.experimentPath}`, + ); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate async work + }, +}; + +export default hooks; diff --git a/eval/evals/100-flight-booking-plain/pre-evaluate/.storybook/preview.ts b/eval/evals/100-flight-booking-plain/pre-evaluate/.storybook/preview.ts new file mode 100644 index 00000000..305d903d --- /dev/null +++ b/eval/evals/100-flight-booking-plain/pre-evaluate/.storybook/preview.ts @@ -0,0 +1,13 @@ +import type { Preview } from '@storybook/react-vite'; + +const preview: Preview = { + parameters: { + options: { + storySort: { + order: ['Summary', 'Conversation', 'Build', 'Typecheck', 'Lint'], + }, + }, + }, +}; + +export default preview; diff --git a/eval/evals/100-flight-booking-plain/pre-evaluate/stories/FlightBooking.stories.ts b/eval/evals/100-flight-booking-plain/pre-evaluate/stories/FlightBooking.stories.ts new file mode 100644 index 00000000..0bfd6f41 --- /dev/null +++ b/eval/evals/100-flight-booking-plain/pre-evaluate/stories/FlightBooking.stories.ts @@ -0,0 +1,274 @@ +import FlightBookingComponent from '../src/components/FlightBooking.tsx'; +import { userEvent, fn, expect, screen, waitFor } from 'storybook/test'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { StepFunction } from 'storybook/internal/types'; + +const meta = { + component: FlightBookingComponent, + args: { + onSubmit: fn(), + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +async function looseGetInteractiveElements( + testId: string, + label: string, + step: StepFunction, +) { + let elements: HTMLElement[] = []; + await step( + `Get element by test ID '${testId}' or label '${label}'`, + async () => { + elements = await waitFor(function getElement() { + const byTestId = screen.queryAllByTestId(testId); + if (byTestId.length > 0) { + return byTestId; + } + const candidates = [ + ...screen.queryAllByTestId(testId), + ...screen.queryAllByLabelText(label, { exact: false }), + ...screen.queryAllByPlaceholderText(label, { exact: false }), + ...screen.queryAllByText(label, { exact: false }), + ]; + + // Return all interactive elements + const interactive = candidates.filter((el) => { + if (!el) { + return false; + } + if ( + el.getAttribute('disabled') === '' || + el.getAttribute('aria-disabled') === 'true' + ) { + return false; + } + const tagName = el.tagName.toLowerCase(); + // Check for naturally interactive HTML elements + if ( + [ + 'button', + 'a', + 'input', + 'select', + 'textarea', + 'details', + 'summary', + 'audio', + 'video', + ].includes(tagName) + ) { + return true; + } + // Check for elements with interactive ARIA roles + const role = el.getAttribute('role'); + if ( + !!role && + [ + 'button', + 'link', + 'textbox', + 'checkbox', + 'radio', + 'combobox', + 'listbox', + 'option', + 'menuitem', + 'menuitemcheckbox', + 'menuitemradio', + 'tab', + 'switch', + 'slider', + 'spinbutton', + 'searchbox', + 'progressbar', + 'scrollbar', + ].includes(role) + ) { + return true; + } + // Check for cursor: pointer style + const computedStyle = window.getComputedStyle(el); + if (computedStyle.cursor === 'pointer') { + return true; + } + return false; + }); + interactive.push(null as any); + + return interactive; + }); + }, + ); + return elements!; +} + +export const Initial: Story = {}; + +export const FlightPicker: Story = { + play: async ({ step }) => { + const fromFlightTrigger = ( + await looseGetInteractiveElements('flight-trigger-from', 'From', step) + )[0]; + await expect(fromFlightTrigger).toBeInTheDocument(); + + if ( + fromFlightTrigger.tagName.toLowerCase() === 'input' && + (fromFlightTrigger as HTMLInputElement).type === 'text' + ) { + await userEvent.type(fromFlightTrigger, 'M'); + } else { + await userEvent.click(fromFlightTrigger); + } + + await expect( + (await looseGetInteractiveElements('airport-MEL', 'MEL', step))[0], + ).toBeInTheDocument(); + }, +}; + +export const DatePicker: Story = { + play: async ({ step }) => { + await userEvent.click( + ( + await looseGetInteractiveElements( + 'date-trigger-departure', + 'Departure Date', + step, + ) + )[0], + ); + await expect( + (await looseGetInteractiveElements('date-27', '27', step))[0], + ).toBeInTheDocument(); + }, +}; + +export const ReturnDatePickerIsUnavailableWhenOneWaySelected: Story = { + play: async ({ step }) => { + await userEvent.click( + (await looseGetInteractiveElements('one-way', 'One Way', step))[0], + ); + + const returnDatePicker = ( + await looseGetInteractiveElements( + 'date-trigger-return', + 'Return Date', + step, + ) + )[0]; + + // If the return datepicker exists, ensure it's disabled by trying to open it + if (returnDatePicker) { + await userEvent.click(returnDatePicker); + const date15 = ( + await looseGetInteractiveElements('date-15', '15', step) + )[0]; + await expect(date15).toBeNull(); + } else { + await expect(returnDatePicker).toBeNull(); + } + }, +}; + +export const Submitted: Story = { + play: async ({ canvasElement, args, step }) => { + await step('Enable return flight', async () => { + const returnToggle = ( + await looseGetInteractiveElements('return', 'Return', step) + )[0]; + await userEvent.click(returnToggle); + }); + + await step('Select from flight', async () => { + const fromFlightTrigger = ( + await looseGetInteractiveElements('flight-trigger-from', 'From', step) + )[0]; + await expect(fromFlightTrigger).toBeInTheDocument(); + + if ( + fromFlightTrigger.tagName.toLowerCase() === 'input' && + (fromFlightTrigger as HTMLInputElement).type === 'text' + ) { + await userEvent.type(fromFlightTrigger, 'M'); + } else { + await userEvent.click(fromFlightTrigger); + } + + const melbourneAirport = ( + await looseGetInteractiveElements('airport-MEL', 'MEL', step) + )[0]; + await userEvent.click(melbourneAirport); + }); + + await step('Select to flight', async () => { + const toFlightTrigger = ( + await looseGetInteractiveElements('flight-trigger-to', 'To', step) + )[0]; + await expect(toFlightTrigger).toBeInTheDocument(); + + if ( + toFlightTrigger.tagName.toLowerCase() === 'input' && + (toFlightTrigger as HTMLInputElement).type === 'text' + ) { + await userEvent.type(toFlightTrigger, 'L'); + } else { + await userEvent.click(toFlightTrigger); + } + const laxAirport = ( + await looseGetInteractiveElements('airport-LAX', 'LAX', step) + )[0]; + await userEvent.click(laxAirport); + }); + + await step('Select departure date', async () => { + await userEvent.click( + ( + await looseGetInteractiveElements( + 'date-trigger-departure', + 'Departure Date', + step, + ) + )[0], + ); + const date = ( + await looseGetInteractiveElements('date-27', '27', step) + ).at(-1)!; + await expect(date).toBeInTheDocument(); + await userEvent.click(date); + await userEvent.click(canvasElement); // dismiss datepicker popover + }); + + await step('Select return date', async () => { + await userEvent.click( + ( + await looseGetInteractiveElements( + 'date-trigger-return', + 'Return Date', + step, + ) + )[0], + ); + const date = ( + await looseGetInteractiveElements('date-28', '28', step) + ).at(-1)!; + await expect(date).toBeInTheDocument(); + await userEvent.click(date); + await userEvent.click(canvasElement); // dismiss datepicker popover + }); + + await userEvent.click( + ( + await looseGetInteractiveElements( + 'search-flights', + 'Search Flights', + step, + ) + )[0], + ); + await expect(args.onSubmit).toHaveBeenCalledOnce(); + }, +}; diff --git a/eval/evals/100-flight-booking-plain/prompt.md b/eval/evals/100-flight-booking-plain/prompt.md new file mode 100644 index 00000000..3be2bc5f --- /dev/null +++ b/eval/evals/100-flight-booking-plain/prompt.md @@ -0,0 +1,36 @@ +Create a flight booking component that includes: + +- An autocomplete component for choosing source and destination from the following list of airports: + SYD: – Sydney Airport, Australia + MEL: – Melbourne Airport (Tullamarine), Australia + LAX: – Los Angeles International Airport, USA + JFK: – John F. Kennedy International Airport, New York, USA + LHR: – Heathrow Airport, London, UK + CDG: – Charles de Gaulle Airport, Paris, France + ATL: – Hartsfield–Jackson Atlanta International Airport, USA + DXB: – Dubai International Airport, UAE + HKG: – Hong Kong International Airport, Hong Kong + BNE: – Brisbane Airport, Australia + PER: – Perth Airport, Australia + DFW: – Dallas Fort Worth International Airport, USA + +- A toggle button for return vs one way +- One or two date selects that when clicked on triggers a popover with a calendar widget. + +The calendar widget shouldn't allow selecting dates in the past and the return flight must be after the departure flight. + + + +1. The component MUST be a default export in src/components/FlightBooking.tsx +2. The component MUST be added to the main.tsx file as the ONLY component being rendered +3. The component MUST take an optional onSubmit() prop that is called when the submit button is clicked +4. The element for the "One Way"-toggle SHOULD have "One Way" as its only content and SHOULD have data-testid="one-way" +5. The element for the "Return"-toggle SHOULD have a "Return" as its only content and SHOULD have data-testid="return" +6. The autocomplete to open the From airport picker SHOULD have "From" as its placeholder and SHOULD have data-testid="flight-trigger-from" +7. The autocomplete to open the To airport picker SHOULD have "To" as its placeholder and SHOULD have data-testid="flight-trigger-to" +8. Each element to select an airport in the pickers SHOULD have include both the shortcode and full airport name in its content and SHOULD have data-testid="airport-{SHORTCODE}" (e.g., "airport-MEL", "airport-LAX") +9. The element to open the Departure Date date select SHOULD have "Departure Date" as its initial content and SHOULD have data-testid="date-trigger-departure" +10. The (optional) element to open the Return Date date select SHOULD have "Return Date" as its initial content and SHOULD have data-testid="date-trigger-return" +11. Each date in the date selects SHOULD the day of month as its only content and SHOULD have data-testid="date-{DAY}" (e.g., "date-27", "date-15") +12. The submit button SHOULD have "Search Flights" as its only content and SHOULD have data-testid="search-flights" + diff --git a/eval/evals/110-flight-booking-reshaped/components.json b/eval/evals/110-flight-booking-reshaped/components.json new file mode 100644 index 00000000..0d5e0253 --- /dev/null +++ b/eval/evals/110-flight-booking-reshaped/components.json @@ -0,0 +1,3548 @@ +{ + "v": 0, + "components": { + "components-actionbar": { + "id": "components-actionbar", + "name": "ActionBar", + "path": "./src/components/ActionBar/tests/ActionBar.stories.tsx", + "stories": [ + { + "name": "positionRelative", + "snippet": "const positionRelative = () => (\n \n \n \n \n \n \n\n \n \n \n \n \n \n);" + }, + { + "name": "positionAbsolute", + "snippet": "const positionAbsolute = () => (\n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "positionFixed", + "snippet": "const positionFixed = () => (\n <>\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n
\n \n);" + }, + { + "name": "elevated", + "snippet": "const elevated = () => (\n \n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "offset", + "snippet": "const offset = () => (\n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "active", + "snippet": "const active = () => {\n const barToggle = useToggle();\n\n return (\n <>\n \n \n \n \n \n );\n};" + }, + { + "name": "padding", + "snippet": "const padding = () => (\n \n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n
\n);" + } + ], + "import": "import { ActionBar, Button, Example, Placeholder, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "ActionBar", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/ActionBar/ActionBar.tsx", + "actualName": "ActionBar", + "exportName": "default" + } + }, + "components-alert": { + "id": "components-alert", + "name": "Alert", + "path": "./src/components/Alert/tests/Alert.stories.tsx", + "stories": [ + { + "name": "color", + "snippet": "const color = () => (\n \n {([\"neutral\", \"primary\", \"critical\", \"positive\", \"warning\"] as const).map((color) => (\n \n \n {}}\n >\n View now\n \n {}}\n >\n Dismiss\n \n \n }\n >\n Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum\n has been the industry's standard\n \n \n ))}\n \n);" + }, + { + "name": "inline", + "snippet": "const inline = () => (\n \n \n \n {}}>\n View now\n \n {}}>\n Dismiss\n \n \n }\n >\n Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has\n been the industry's standard\n \n \n \n);" + }, + { + "name": "bleed", + "snippet": "const bleed = () => (\n \n \n \n Content\n \n \n \n \n Content\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n
\n);" + } + ], + "import": "import { Alert, Example, Link, Placeholder } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Alert", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Alert/Alert.tsx", + "actualName": "Alert", + "exportName": "default" + } + }, + "components-autocomplete": { + "id": "components-autocomplete", + "name": "Autocomplete", + "path": "./src/components/Autocomplete/tests/Autocomplete.stories.tsx", + "stories": [ + { + "name": "active", + "snippet": "const active = (args) => {\n const toggle = useToggle(true);\n\n return (\n \n \n \n Label\n {\n args.handleOpen();\n toggle.activate();\n }}\n onClose={() => {\n args.handleClose();\n toggle.deactivate();\n }}\n onChange={(args) => console.log(args)}\n >\n {[\"Pizza\", \"Pie\", \"Ice-cream\"].map((v, i) => {\n return (\n \n {v}\n \n );\n })}\n \n \n \n \n );\n};" + }, + { + "name": "base", + "snippet": "const base = () => {\n const [value, setValue] = React.useState(\"\");\n\n return (\n \n \n \n Food\n setValue(args.value)}\n onBackspace={fn()}\n onItemSelect={fn()}>\n {[\"Pizza\", \"Pie\", \"Ice-cream\"].map((v, i) => {\n return (\n \n {v}\n \n );\n })}\n \n \n \n \n );\n};" + }, + { + "name": "itemData", + "snippet": "const itemData = () => {\n return (\n \n \n \n Label\n \n {[\"Pizza\", \"Pie\", \"Ice-cream\"].map((v, i) => {\n return (\n \n {v}\n \n );\n })}\n \n \n \n \n );\n};" + }, + { + "name": "itemDisabled", + "snippet": "const itemDisabled = () => {\n return (\n \n \n \n Label\n \n {[\"Pizza\", \"Pie\", \"Ice-cream\"].map((v, i) => {\n return (\n \n {v}\n \n );\n })}\n \n \n \n \n );\n};" + }, + { + "name": "multiselect", + "snippet": "const multiselect = () => {\n const options = [\n \"Pizza\",\n \"Pie\",\n \"Ice-cream\",\n \"Fries\",\n \"Salad\",\n \"Option 4\",\n \"Option 5\",\n \"Option 6\",\n ];\n\n const inputRef = React.useRef(null);\n const [values, setValues] = React.useState([\n \"Option 4\",\n \"Option 5\",\n \"Option 6\",\n \"Pizza\",\n \"Ice-cream\",\n ]);\n const [query, setQuery] = React.useState(\"\");\n\n const handleDismiss = (dismissedValue: string) => {\n const nextValues = values.filter((value) => value !== dismissedValue);\n setValues(nextValues);\n inputRef.current?.focus();\n };\n\n const valuesNode = values.map((value) => (\n handleDismiss(value)} key={value}>\n {value}\n \n ));\n\n return (\n \n Food\n setQuery(args.value)}\n onBackspace={() => {\n if (query.length === 0) {\n setValues((prev) => prev.slice(0, -1));\n }\n }}\n onItemSelect={(args) => {\n setQuery(\"\");\n setValues((prev) => [...prev, args.value]);\n }}\n >\n {options.map((v) => {\n if (!v.toLowerCase().includes(query.toLowerCase())) return;\n if (values.includes(v)) return;\n\n return (\n \n {v}\n \n );\n })}\n \n \n );\n};" + } + ], + "import": "import { Autocomplete, Badge, Example, FormControl } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Autocomplete", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Autocomplete/Autocomplete.tsx", + "actualName": "Autocomplete", + "exportName": "default" + } + }, + "components-avatar": { + "id": "components-avatar", + "name": "Avatar", + "path": "./src/components/Avatar/tests/Avatar.stories.tsx", + "stories": [ + { + "name": "src", + "snippet": "const src = () => (\n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "initials", + "snippet": "const initials = () => (\n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n);" + }, + { + "name": "squared", + "snippet": "const squared = () => (\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "colors", + "snippet": "const colors = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "fallback", + "snippet": "const fallback = (args) => {\n const [error, setError] = useState(false);\n\n return (\n {\n setError(true);\n args.handleError();\n },\n }}\n />\n );\n};" + }, + { + "name": "renderImage", + "snippet": "const renderImage = () => (\n \n \n }\n />\n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Avatar, Example, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Avatar", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Avatar/Avatar.tsx", + "actualName": "Avatar", + "exportName": "default" + } + }, + "components-badge": { + "id": "components-badge", + "name": "Badge", + "path": "./src/components/Badge/tests/Badge.stories.tsx", + "stories": [ + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n Badge\n \n \n Badge\n \n \n Badge\n \n \n);" + }, + { + "name": "color", + "snippet": "const color = () => (\n \n \n \n Badge\n \n Badge\n \n \n Badge\n \n \n \n\n \n \n Badge\n \n Badge\n \n \n Badge\n \n \n \n\n \n \n Badge\n \n Badge\n \n \n Badge\n \n \n \n\n \n \n Badge\n \n Badge\n \n \n Badge\n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n Badge\n \n Badge\n \n \n \n \n \n Badge\n Badge\n \n \n \n \n Badge\n \n Badge\n \n \n \n \n);" + }, + { + "name": "icon", + "snippet": "const icon = () => (\n \n \n \n \n Badge\n \n \n \n \n \n \n \n Badge\n \n \n \n \n \n \n \n Badge\n \n\n \n \n \n \n);" + }, + { + "name": "onDismiss", + "snippet": "const onDismiss = () => \n \n Badge\n \n \n;" + }, + { + "name": "rounded", + "snippet": "const rounded = () => (\n \n \n \n Badge\n \n Badge\n \n \n Badge\n \n \n \n \n \n \n 2\n \n \n 2\n \n \n 2\n \n \n \n \n);" + }, + { + "name": "empty", + "snippet": "const empty = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "highlighted", + "snippet": "const highlighted = () => (\n \n \n \n Badge\n \n \n \n);" + }, + { + "name": "container", + "snippet": "const container = () => {\n const [hidden, setHidden] = React.useState(false);\n\n return (\n setHidden(!hidden)}>Toggle badges}>\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n );\n};" + }, + { + "name": "href", + "snippet": "const href = () => (\n \n Badge\n \n);" + }, + { + "name": "onClick", + "snippet": "const onClick = () => Badge;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Avatar, Badge, Button, Example, Icon, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Badge", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Badge/Badge.tsx", + "actualName": "Badge", + "exportName": "default" + } + }, + "components-breadcrumbs": { + "id": "components-breadcrumbs", + "name": "Breadcrumbs", + "path": "./src/components/Breadcrumbs/tests/Breadcrumbs.stories.tsx", + "stories": [ + { + "name": "color", + "snippet": "const color = () => (\n \n \n \n {}}>Item 1\n {}}>Item 2\n Item 3\n \n \n\n \n \n {}}>Item 1\n {}}>Item 2\n Item 3\n \n \n \n);" + }, + { + "name": "item", + "snippet": "const item = () => (\n \n \n \n {}}>Item 1\n {}} disabled>\n Disabled item 2\n \n Item 3\n \n \n \n);" + }, + { + "name": "icon", + "snippet": "const icon = () => (\n \n \n \n {}}>\n Item 1\n \n {}}>Item 2\n Item 3\n \n \n \n);" + }, + { + "name": "slots", + "snippet": "const slots = () => (\n \n \n \n {}}>Item 1\n {}}>Item 2\n Item 3\n \n \n\n \n \n {}}>\n Item 1\n \n {}}>\n Item 2\n \n Item 3\n \n \n \n);" + }, + { + "name": "collapsed", + "snippet": "const collapsed = () => (\n \n \n \n {}}>Item 1\n {}}>Item 2\n {}}>Item 3\n {}}>Item 4\n Item 5\n \n \n\n \n \n {}}>Item 1\n {}}>Item 2\n {}}>Item 3\n {}}>Item 4\n Item 5\n \n \n\n \n \n {}}>Item 1\n {}}>Item 2\n {}}>Item 3\n {}}>Item 4\n Item 5\n \n \n \n);" + }, + { + "name": "multiline", + "snippet": "const multiline = () => (\n \n \n \n {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (\n {}} key={i}>\n Item {i}\n \n ))}\n \n \n \n);" + }, + { + "name": "onClick", + "snippet": "const onClick = () => \n Trigger\n Trigger\n \n;" + }, + { + "name": "href", + "snippet": "const href = () => (\n \n Trigger\n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Trigger\n \n
\n);" + } + ], + "import": "import { Badge, Breadcrumbs, Example } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Breadcrumbs", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Breadcrumbs/Breadcrumbs.tsx", + "actualName": "Breadcrumbs", + "exportName": "default" + } + }, + "components-button": { + "id": "components-button", + "name": "Button", + "path": "./src/components/Button/tests/Button.stories.tsx", + "stories": [ + { + "name": "variantAndColor", + "snippet": "const variantAndColor = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "icon", + "snippet": "const icon = () => (\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "elevated", + "snippet": "const elevated = () => (\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "rounded", + "snippet": "const rounded = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "loading", + "snippet": "const loading = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n
\n \n \n \n \n
\n
\n
\n\n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n
\n \n \n \n \n
\n
\n
\n
\n);" + }, + { + "name": "aligner", + "snippet": "const aligner = () => (\n \n \n \n Content\n \n \n \n \n);" + }, + { + "name": "href", + "snippet": "const href = () => ;" + }, + { + "name": "onClick", + "snippet": "const onClick = () => ;" + }, + { + "name": "hrefOnClick", + "snippet": "const hrefOnClick = (args) => (\n {\n e.preventDefault();\n args.handleClick(e);\n }}\n href=\"https://reshaped.so\"\n >\n Trigger\n \n);" + }, + { + "name": "as", + "snippet": "const as = () => (\n \n \n \n \n \n {}}\n render={(props) =>
}\n attributes={{ \"data-testid\": \"render-el\" }}\n >\n Trigger\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + }, + { + "name": "group", + "snippet": "const group = () => (\n \n \n \n \n \n \n \n \n ))}\n \n \n \n \n {([\"neutral\", \"primary\", \"critical\", \"positive\"] as const).map((color) => (\n \n \n \n \n \n ))}\n \n \n \n \n {([\"neutral\", \"primary\", \"critical\", \"positive\", \"media\"] as const).map((color) => (\n \n \n \n \n \n ))}\n \n \n\n \n \n {([\"neutral\", \"primary\", \"critical\", \"positive\"] as const).map((color) => (\n \n \n \n \n \n ))}\n \n \n \n);" + }, + { + "name": "groupClassName", + "snippet": "const groupClassName = () => (\n
\n \n \n \n
\n);" + } + ], + "import": "import { Avatar, Button, Example, Hotkey, Image, Placeholder, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Button", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Button/Button.tsx", + "actualName": "Button", + "exportName": "default" + } + }, + "components-calendar": { + "id": "components-calendar", + "name": "Calendar", + "path": "./src/components/Calendar/tests/Calendar.stories.tsx", + "stories": [ + { + "name": "defaultMonth", + "snippet": "const defaultMonth = () => (\n \n \n \n \n \n);" + }, + { + "name": "uncontrolled", + "snippet": "const uncontrolled = () => \n \n \n \n \n \n \n;" + }, + { + "name": "controlled", + "snippet": "const controlled = () => \n \n \n \n \n \n \n;" + }, + { + "name": "selectedDates", + "snippet": "const selectedDates = () => (\n \n \n \n \n \n);" + }, + { + "name": "minMax", + "snippet": "const minMax = () => (\n \n \n \n \n \n);" + }, + { + "name": "firstWeekDay", + "snippet": "const firstWeekDay = () => (\n \n \n \n \n \n);" + }, + { + "name": "translation", + "snippet": "const translation = () => (\n \n \n date.toLocaleDateString(\"nl\", { month: \"short\" })}\n renderSelectedMonthLabel={({ date }) =>\n date.toLocaleDateString(\"nl\", { month: \"long\", year: \"numeric\" })\n }\n renderWeekDay={({ date }) => date.toLocaleDateString(\"nl\", { weekday: \"short\" })}\n />\n \n \n);" + }, + { + "name": "ariaLabels", + "snippet": "const ariaLabels = () => (\n \n \n \"Test date\"}\n renderMonthAriaLabel={() => \"Test month\"}\n previousYearAriaLabel=\"Test previous year\"\n previousMonthAriaLabel=\"Test previous month\"\n monthSelectionAriaLabel=\"Test month selection\"\n />\n \n \n);" + }, + { + "name": "monthSelection", + "snippet": "const monthSelection = () => (\n \n \n \n \n \n);" + }, + { + "name": "keyboardNavigation", + "snippet": "const keyboardNavigation = () => (\n \n \n \n \n \n);" + } + ], + "import": "import { Calendar, Example } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Calendar", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Calendar/Calendar.tsx", + "actualName": "Calendar", + "exportName": "default" + } + }, + "components-card": { + "id": "components-card", + "name": "Card", + "path": "./src/components/Card/tests/Card.stories.tsx", + "stories": [ + { + "name": "padding", + "snippet": "const padding = () => (\n \n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n);" + }, + { + "name": "selected", + "snippet": "const selected = () => (\n \n \n \n \n \n \n \n);" + }, + { + "name": "elevated", + "snippet": "const elevated = () => (\n \n \n \n \n \n \n \n);" + }, + { + "name": "bleed", + "snippet": "const bleed = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "height", + "snippet": "const height = () => (\n \n \n \n \n \n);" + }, + { + "name": "onClick", + "snippet": "const onClick = () => Trigger;" + }, + { + "name": "href", + "snippet": "const href = () => Trigger;" + }, + { + "name": "as", + "snippet": "const as = () => ;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Card, Example, Placeholder } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Card", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Card/Card.tsx", + "actualName": "Card", + "exportName": "default" + } + }, + "components-carousel": { + "id": "components-carousel", + "name": "Carousel", + "path": "./src/components/Carousel/tests/Carousel.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n \n Content\n Content\n Content\n Content\n Content\n Content\n \n);" + }, + { + "name": "visibleItems", + "snippet": "const visibleItems = () => (\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "gap", + "snippet": "const gap = () => (\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "bleed", + "snippet": "const bleed = () => (\n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "navigationDisplay", + "snippet": "const navigationDisplay = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "instanceRef", + "snippet": "const instanceRef = (args) => {\n const carouselRef = React.useRef(null);\n const [index, setIndex] = React.useState(0);\n\n return (\n \n \n \n \n \n \n \n Index: {index}\n \n {\n args.handleChange(changeArgs);\n setIndex(changeArgs.index);\n }}\n >\n Item 0\n Item 1\n Item 2\n Item 3\n Item 4\n Item 5\n \n \n \n \n );\n};" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Item 0\n Item 1\n Item 2\n Item 3\n Item 4\n Item 5\n \n
\n);" + } + ], + "import": "import { Button, Carousel, Example, Placeholder, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [ + { + "name": "navigateTo", + "docblock": null, + "modifiers": [], + "params": [ + { + "name": "index", + "optional": false, + "type": { "name": "number" } + } + ], + "returns": null + } + ], + "displayName": "Carousel", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Carousel/Carousel.tsx", + "actualName": "Carousel", + "exportName": "default" + } + }, + "components-checkbox": { + "id": "components-checkbox", + "name": "Checkbox", + "path": "./src/components/Checkbox/tests/Checkbox.stories.tsx", + "stories": [ + { + "name": "render", + "snippet": "const render = () => (\n \n Content\n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n Checkbox\n \n \n Checkbox\n \n \n \n \n \n \n Checkbox\n \n \n Checkbox\n \n \n \n \n \n \n Checkbox\n \n \n Checkbox\n \n \n \n \n \n \n Checkbox\n \n \n \n \n);" + }, + { + "name": "error", + "snippet": "const error = () => (\n \n \n \n Checkbox\n \n \n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n \n \n Checkbox\n \n \n \n \n Checkbox\n \n \n \n \n Checkbox\n \n \n \n);" + }, + { + "name": "checked", + "snippet": "const checked = () => Content\n ;" + }, + { + "name": "defaultChecked", + "snippet": "const defaultChecked = () => Content\n ;" + }, + { + "name": "indeterminate", + "snippet": "const indeterminate = () => (\n \n Content\n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Content\n \n
\n);" + } + ], + "import": "import { Checkbox, Example, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Checkbox", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Checkbox/Checkbox.tsx", + "actualName": "Checkbox", + "exportName": "default" + } + }, + "components-checkboxgroup": { + "id": "components-checkboxgroup", + "name": "CheckboxGroup", + "path": "./src/components/CheckboxGroup/tests/CheckboxGroup.stories.tsx", + "stories": [ + { + "name": "value", + "snippet": "const value = () => \n \n {/* checked should be ignored */}\n Content\n \n Content 2\n \n;" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => \n \n {/* checked should be ignored */}\n Content\n \n Content 2\n \n;" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n Item 1\n Item 2\n \n);" + } + ], + "import": "import { Checkbox, CheckboxGroup, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "CheckboxGroup", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/CheckboxGroup/CheckboxGroup.tsx", + "actualName": "CheckboxGroup", + "exportName": "default" + } + }, + "components-contextmenu": { + "id": "components-contextmenu", + "name": "ContextMenu", + "path": "./src/components/ContextMenu/tests/ContextMenu.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n \n \n
\n \n \n\n \n Item 1\n Item 2\n \n \n
\n
\n
\n);" + }, + { + "name": "handlers", + "snippet": "const handlers = () =>
\n \n \n \n Item\n \n \n
;" + } + ], + "import": "import { ContextMenu, Example, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "ContextMenu", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/ContextMenu/ContextMenu.tsx", + "actualName": "ContextMenu", + "exportName": "default" + } + }, + "components-divider": { + "id": "components-divider", + "name": "Divider", + "path": "./src/components/Divider/tests/Divider.stories.tsx", + "stories": [ + { + "name": "rendering", + "snippet": "const rendering = () => (\n \n \n \n \n\n \n \n \n \n \n);" + }, + { + "name": "vertical", + "snippet": "const vertical = () => (\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "label", + "snippet": "const label = () => {\n return (\n \n \n \n Centered label\n Start label\n End label\n \n \n\n \n \n \n \n or pick second option\n \n \n \n \n \n );\n};" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Divider, Example, Placeholder, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Divider", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Divider/Divider.tsx", + "actualName": "Divider", + "exportName": "default" + } + }, + "components-dropdownmenu": { + "id": "components-dropdownmenu", + "name": "DropdownMenu", + "path": "./src/components/DropdownMenu/tests/DropdownMenu.stories.tsx", + "stories": [ + { + "name": "position", + "snippet": "const position = () => (\n \n \n \n \n {(attributes) => }\n \n \n Item 1\n Item 2\n \n \n \n\n \n \n \n {(attributes) => }\n \n \n Item 1\n Item 2\n \n \n \n\n \n \n \n {(attributes) => }\n \n \n Item 1\n Item 2\n \n \n \n
\n \n);" + }, + { + "name": "sections", + "snippet": "const sections = () => (\n \n \n \n \n {(attributes) => }\n \n \n \n Item 1\n Item 2\n \n\n \n Item 3\n Item 4\n \n \n \n \n \n);" + }, + { + "name": "submenu", + "snippet": "const submenu = () => (\n \n \n \n \n {(attributes) => }\n \n \n {}}>Item 1\n \n Item 2\n \n {}}>SubItem 1\n {}}>SubItem 2\n \n \n \n Item 3\n \n {}}>SubItem 2-1\n {}}>SubItem 2-2\n \n \n\n \n Item 4, disabled\n \n {}}>SubItem 3-1\n {}}>SubItem 3-2\n \n \n \n \n \n \n);" + }, + { + "name": "defaultActive", + "snippet": "const defaultActive = () => \n \n {(attributes) => }\n \n \n Item\n \n;" + }, + { + "name": "active", + "snippet": "const active = () => \n \n {(attributes) => }\n \n \n Item\n \n;" + }, + { + "name": "activeFalse", + "snippet": "const activeFalse = () => \n \n {(attributes) => }\n \n \n Item\n \n;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n {(attributes) => }\n \n \n Item\n \n \n
\n);" + }, + { + "name": "testScroll", + "snippet": "const testScroll = () => (\n \n \n
\n \n \n {(attributes) => }\n \n \n Item 1\n Item 2\n \n \n \n \n);" + }, + { + "name": "testTheme", + "snippet": "const testTheme = () => (\n \n \n \n \n \n \n \n \n);" + } + ], + "import": "import { Button, DropdownMenu, Example, Theme, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "DropdownMenu", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/DropdownMenu/DropdownMenu.tsx", + "actualName": "DropdownMenu", + "exportName": "default" + } + }, + "components-fileupload": { + "id": "components-fileupload", + "name": "FileUpload", + "path": "./src/components/FileUpload/tests/FileUpload.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n \n \n \n \n \n \n
\n Drop files to attach, or{\" \"}\n \n browse\n \n
\n
\n
\n
\n);" + }, + { + "name": "inline", + "snippet": "const inline = () => {\n return (\n \n \n \n \n Upload\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {(props) => }\n \n \n \n );\n};" + }, + { + "name": "height", + "snippet": "const height = () => (\n \n \n \n \n \n Drop files to attach\n \n \n \n \n);" + }, + { + "name": "onChange", + "snippet": "const onChange = () =>
\n Content\n \n
;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Content\n \n
\n);" + } + ], + "import": "import { Button, Example, FileUpload, Icon, Image, Link, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "FileUpload", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/FileUpload/FileUpload.tsx", + "actualName": "FileUpload", + "exportName": "default" + } + }, + "components-hotkey": { + "id": "components-hotkey", + "name": "Hotkey", + "path": "./src/components/Hotkey/tests/Hotkey.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t⌘K\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t}\n\t\t\t\t\tinputAttributes={{ \"aria-label\": \"hotkey test\" }}\n\t\t\t\t/>\n\t\t\t\n\t\t\n\t\n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n ⌘K\n \n
\n);" + } + ], + "import": "import { Example, Hotkey, TextField, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Hotkey", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Hotkey/Hotkey.tsx", + "actualName": "Hotkey", + "exportName": "default" + } + }, + "components-link": { + "id": "components-link", + "name": "Link", + "path": "./src/components/Link/tests/Link.stories.tsx", + "stories": [ + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n Reshaped\n \n \n \n {}} variant=\"plain\">\n Link\n \n \n \n);" + }, + { + "name": "color", + "snippet": "const color = () => (\n \n \n Link\n \n \n Link\n \n \n Link\n \n \n Link\n \n \n Link\n \n \n);" + }, + { + "name": "icon", + "snippet": "const icon = () => (\n \n \n Link\n \n \n \n Link\n \n \n \n \n \n Instant delivery\n \n \n \n \n);" + }, + { + "name": "href", + "snippet": "const href = () => Trigger;" + }, + { + "name": "onClick", + "snippet": "const onClick = () => Trigger;" + }, + { + "name": "hrefOnClick", + "snippet": "const hrefOnClick = (args) => (\n {\n e.preventDefault();\n args.handleClick(e);\n }}\n href=\"https://reshaped.so\"\n >\n Trigger\n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n {}}>\n Trigger\n \n);" + }, + { + "name": "render", + "snippet": "const render = () =>
}>Trigger;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Trigger\n \n
\n);" + }, + { + "name": "testMultilineInText", + "snippet": "const testMultilineInText = () => (\n \n \n
\n Someone asked me to write this text that is boring to ready for everyone and to add \n this very very long link to it.\n
\n
\n
\n);" + } + ], + "import": "import { Example, Link, Text } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Link", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Link/Link.tsx", + "actualName": "Link", + "exportName": "default" + } + }, + "components-loader": { + "id": "components-loader", + "name": "Loader", + "path": "./src/components/Loader/tests/Loader.stories.tsx", + "stories": [ + { + "name": "size", + "snippet": "const size = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "color", + "snippet": "const color = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "ariaLabel", + "snippet": "const ariaLabel = () => ;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Example, Loader } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Loader", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Loader/Loader.tsx", + "actualName": "Loader", + "exportName": "default" + } + }, + "components-menuitem": { + "id": "components-menuitem", + "name": "MenuItem", + "path": "./src/components/MenuItem/tests/MenuItem.stories.tsx", + "stories": [ + { + "name": "size", + "snippet": "const size = () => (\n \n \n {}} endSlot={⌘K}>\n Menu item\n \n \n \n {}}>\n Menu item\n \n \n \n {}}>\n Menu item\n \n \n \n {}}>\n Menu item\n \n \n \n);" + }, + { + "name": "color", + "snippet": "const color = () => (\n \n \n \n Menu item\n \n \n \n \n Menu item\n \n \n \n \n Menu item\n \n \n \n);" + }, + { + "name": "selected", + "snippet": "const selected = () => (\n \n \n \n Menu item\n \n \n \n \n Menu item\n \n \n \n \n Menu item\n \n \n \n);" + }, + { + "name": "roundedCorners", + "snippet": "const roundedCorners = () => (\n \n \n \n Menu item\n \n \n\n \n \n Menu item\n \n \n \n);" + }, + { + "name": "slots", + "snippet": "const slots = () => (\n \n \n } endSlot={} selected>\n Menu item\n \n \n \n);" + }, + { + "name": "aligner", + "snippet": "const aligner = () => (\n \n \n \n Heading\n \n {}}>\n Menu item\n \n \n \n \n\n \n \n Heading\n \n Menu item\n \n \n \n\n \n \n Heading\n \n \n Menu item\n \n \n \n \n \n);" + }, + { + "name": "href", + "snippet": "const href = () => Trigger;" + }, + { + "name": "onClick", + "snippet": "const onClick = () => Trigger;" + }, + { + "name": "hrefOnClick", + "snippet": "const hrefOnClick = (args) => (\n {\n e.preventDefault();\n args.handleClick(e);\n }}\n href=\"https://reshaped.so\"\n >\n Trigger\n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n {}}>\n Trigger\n \n);" + }, + { + "name": "as", + "snippet": "const as = () => (\n \n \n {}}\n render={(props) =>
}\n attributes={{ \"data-testid\": \"render-el\" }}\n >\n Trigger\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Trigger\n \n
\n);" + }, + { + "name": "alignerClassName", + "snippet": "const alignerClassName = () => (\n
\n \n Trigger\n \n
\n);" + } + ], + "import": "import { Example, Hotkey, MenuItem, Placeholder, Text, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "MenuItem", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/MenuItem/MenuItem.tsx", + "actualName": "MenuItem", + "exportName": "default" + } + }, + "components-modal": { + "id": "components-modal", + "name": "Modal", + "path": "./src/components/Modal/tests/Modal.stories.tsx", + "stories": [ + { + "name": "position", + "snippet": "const position = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "padding", + "snippet": "const padding = () => (\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "overflow", + "snippet": "const overflow = () => (\n \n \n \n \n \n \n\n \n \n \n \n \n \n);" + }, + { + "name": "composition", + "snippet": "const composition = () => (\n \n \n \n \n \n);" + }, + { + "name": "overlay", + "snippet": "const overlay = () => (\n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "flags", + "snippet": "const flags = () => {\n return (\n \n \n \n \n \n );\n};" + }, + { + "name": "containerRef", + "snippet": "const containerRef = () => {\n const containerRef = React.useRef(null);\n const containerRef2 = React.useRef(null);\n const toggle = useToggle();\n const toggle2 = useToggle();\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "renderProps", + "snippet": "const renderProps = () => (\n \n Title\n Content\n \n);" + }, + { + "name": "handlers", + "snippet": "const handlers = () => {\n const overlayToggle = useToggle();\n\n return (\n <>\n \n {\n overlayToggle.deactivate();\n args.handleClose(closeArgs);\n }}\n onOpen={fn()}\n onAfterOpen={fn()}\n onAfterClose={fn()}>\n TitleContent\n \n \n );\n};" + }, + { + "name": "className", + "snippet": "const className = () => (\n \n Title\n Content\n \n);" + }, + { + "name": "edgeCases", + "snippet": "const edgeCases = () => {\n const menuModalToggle = useToggle();\n const menuModalToggleInner = useToggle();\n const scrollModalToggle = useToggle();\n const inputRef = React.useRef(null);\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {\n inputRef.current?.focus();\n }}\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Content\n \n \n \n \n \n \n \n {(attributes) => }\n \n \n Open dialog\n Item 2\n \n \n \n \n \n \n {(attributes) => }\n \n \n Item 1\n Item 2\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n \n \n \n \n );\n};" + }, + { + "name": "trapFocusEdgeCases", + "snippet": "const trapFocusEdgeCases = () => {\n const toggle = useToggle();\n\n return (\n \n \n \n \n \n \n \n Option 1\n \n \n Option 2\n \n \n \n \n \n \n );\n};" + } + ], + "import": "import {\n Button,\n Dismissible,\n DropdownMenu,\n Example,\n Modal,\n Placeholder,\n Radio,\n Switch,\n TextField,\n View,\n} from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Modal", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Modal/Modal.tsx", + "actualName": "Modal", + "exportName": "default" + } + }, + "components-numberfield": { + "id": "components-numberfield", + "name": "NumberField", + "path": "./src/components/NumberField/tests/NumberField.stories.tsx", + "stories": [ + { + "name": "variant", + "snippet": "const variant = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "size", + "snippet": "const size = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "disabled", + "snippet": "const disabled = () => ;" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => ;" + }, + { + "name": "value", + "snippet": "const value = () => ;" + }, + { + "name": "minMax", + "snippet": "const minMax = () => (\n \n);" + }, + { + "name": "step", + "snippet": "const step = () => (\n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + }, + { + "name": "formControl", + "snippet": "const formControl = () => (\n \n \n \n Label\n \n Helper\n \n \n\n \n \n Label\n \n Helper\n \n \n\n \n \n Label\n \n Error\n \n \n \n);" + }, + { + "name": "valueChanges", + "snippet": "const valueChanges = () => (\n \n \n \n \n \n);" + } + ], + "import": "import { Example, FormControl, NumberField } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "NumberField", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/NumberField/NumberField.tsx", + "actualName": "NumberField", + "exportName": "default" + } + }, + "components-pagination": { + "id": "components-pagination", + "name": "Pagination", + "path": "./src/components/Pagination/tests/Pagination.stories.tsx", + "stories": [ + { + "name": "truncate", + "snippet": "const truncate = () => {\n return (\n \n \n `Page ${args.page}`}\n />\n \n \n `Page ${args.page}`}\n />\n \n \n `Page ${args.page}`}\n />\n \n \n `Page ${args.page}`}\n />\n \n \n );\n};" + }, + { + "name": "render", + "snippet": "const render = () => (\n
\n `Page ${args.page}`}\n />\n
\n);" + }, + { + "name": "defaultPage", + "snippet": "const defaultPage = () =>
\n \n
;" + }, + { + "name": "page", + "snippet": "const page = () =>
\n \n
;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Example, Pagination } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Pagination", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Pagination/Pagination.tsx", + "actualName": "Pagination", + "exportName": "default" + } + }, + "components-pinfield": { + "id": "components-pinfield", + "name": "PinField", + "path": "./src/components/PinField/tests/PinField.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => ;" + }, + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n);" + }, + { + "name": "valueLength", + "snippet": "const valueLength = () => (\n \n);" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => ;" + }, + { + "name": "value", + "snippet": "const value = () => ;" + }, + { + "name": "pattern", + "snippet": "const pattern = () => ;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + }, + { + "name": "formControl", + "snippet": "const formControl = () => (\n \n Label\n \n \n);" + }, + { + "name": "keyboard", + "snippet": "const keyboard = () => ;" + } + ], + "import": "import { Example, FormControl, PinField } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "PinField", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/PinField/PinField.tsx", + "actualName": "PinField", + "exportName": "default" + } + }, + "components-popover": { + "id": "components-popover", + "name": "Popover", + "path": "./src/components/Popover/tests/Popover.stories.tsx", + "stories": [ + { + "name": "position", + "snippet": "const position = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "widthNumber", + "snippet": "const widthNumber = () => ;" + }, + { + "name": "widthFull", + "snippet": "const widthFull = () => ;" + }, + { + "name": "padding", + "snippet": "const padding = () => (\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "elevation", + "snippet": "const elevation = () => (\n \n \n \n \n \n);" + }, + { + "name": "defaultActive", + "snippet": "const defaultActive = () => \n \n {(attributes) => }\n \n Content\n;" + }, + { + "name": "active", + "snippet": "const active = () => \n \n {(attributes) => }\n \n Content\n;" + }, + { + "name": "activeFalse", + "snippet": "const activeFalse = () => \n \n {(attributes) => }\n \n Content\n;" + }, + { + "name": "dismissible", + "snippet": "const dismissible = () => \n \n {(attributes) => }\n \n \n Content\n \n \n;" + }, + { + "name": "autoFocus", + "snippet": "const autoFocus = () => (\n \n \n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n {(attributes) => }\n \n \n Content\n \n \n
\n);" + }, + { + "name": "testNested", + "snippet": "const testNested = () => (\n \n \n {(attributes) => }\n \n \n \n Popover content\n \n \n {(attributes) => }\n \n Hello\n \n \n \n \n);" + }, + { + "name": "testWithTooltip", + "snippet": "const testWithTooltip = () => (\n \n \n {(tooltipAttributes) => (\n \n \n {(attributes) => (\n \n Open\n \n )}\n \n \n \n Popover content\n \n \n \n \n )}\n \n \n);" + }, + { + "name": "testContentEditable", + "snippet": "const testContentEditable = () => {\n const [active, setActive] = useState(false);\n\n return (\n \n \n {(attributes) => }\n \n \n \n \n \n \n\n \n {\n setActive(e.currentTarget.innerText.startsWith(\"@\"));\n }}\n onKeyDown={(e) => {\n console.log(e.key);\n if (e.key === \"Enter\" && active) {\n e.preventDefault();\n e.currentTarget.innerText = \"@hello\";\n setActive(false);\n }\n }}\n />\n \n\n setActive(false)}\n originCoordinates={{ x: 300, y: 300 }}\n trapFocusMode=\"selection-menu\"\n >\n \n \n {}}>Action\n {}}>Close\n \n \n \n \n \n \n );\n};" + }, + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n {(attributes) => }\n \n \n \n \n \n \n \n);" + } + ], + "import": "import { Button, Example, MenuItem, Popover, ScrollArea, Tooltip, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Popover", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Popover/Popover.tsx", + "actualName": "Popover", + "exportName": "default" + } + }, + "components-progress": { + "id": "components-progress", + "name": "Progress", + "path": "./src/components/Progress/tests/Progress.stories.tsx", + "stories": [ + { + "name": "value", + "snippet": "const value = () => (\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "color", + "snippet": "const color = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "duration", + "snippet": "const duration = () => {\n const [active, setActive] = React.useState(false);\n\n const handleChange = () => {\n setActive((state) => !state);\n };\n\n return (\n \n \n \n \n \n \n\n \n \n \n \n );\n};" + }, + { + "name": "render", + "snippet": "const render = () => ;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Button, Example, Progress, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Progress", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Progress/Progress.tsx", + "actualName": "Progress", + "exportName": "default" + } + }, + "components-progressindicator": { + "id": "components-progressindicator", + "name": "ProgressIndicator", + "path": "./src/components/ProgressIndicator/tests/ProgressIndicator.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => {\n const [activeIndex, setActiveIndex] = React.useState(0);\n const total = 10;\n\n return (\n \n \n \n \n {\n setActiveIndex((prev) => Math.max(0, prev - 1));\n }}\n >\n Previous\n \n {\n setActiveIndex((prev) => Math.min(total - 1, prev + 1));\n }}\n >\n Next\n \n Index: {activeIndex}\n \n\n \n }\n >\n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "color", + "snippet": "const color = () => {\n return (\n \n \n \n \n \n \n }\n >\n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "ariaLabel", + "snippet": "const ariaLabel = () => ;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Button, Example, ProgressIndicator, Scrim, Text, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "ProgressIndicator", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/ProgressIndicator/ProgressIndicator.tsx", + "actualName": "ProgressIndicator", + "exportName": "default" + } + }, + "components-radio": { + "id": "components-radio", + "name": "Radio", + "path": "./src/components/Radio/tests/Radio.stories.tsx", + "stories": [ + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n Radio\n \n \n \n \n \n \n Radio\n \n \n \n \n \n \n Radio\n \n \n \n \n \n \n Radio\n \n \n \n \n);" + }, + { + "name": "error", + "snippet": "const error = () => (\n \n \n \n Radio\n \n \n \n);" + }, + { + "name": "render", + "snippet": "const render = () => (\n \n Content\n \n);" + }, + { + "name": "checked", + "snippet": "const checked = () => Content\n ;" + }, + { + "name": "checkedFalse", + "snippet": "const checkedFalse = () => Content\n ;" + }, + { + "name": "defaultChecked", + "snippet": "const defaultChecked = () => Content\n ;" + }, + { + "name": "defaultCheckedFalse", + "snippet": "const defaultCheckedFalse = () => Content\n ;" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n Content\n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Content\n \n
\n);" + } + ], + "import": "import { Example, Radio, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Radio", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Radio/Radio.tsx", + "actualName": "Radio", + "exportName": "default" + } + }, + "components-radiogroup": { + "id": "components-radiogroup", + "name": "RadioGroup", + "path": "./src/components/RadioGroup/tests/RadioGroup.stories.tsx", + "stories": [ + { + "name": "value", + "snippet": "const value = () => \n {/* checked should be ignored */}\n Content\n \n Content 2\n;" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => \n {/* checked should be ignored */}\n Content\n \n Content 2\n;" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n Content\n \n);" + } + ], + "import": "import { Radio, RadioGroup } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "RadioGroup", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/RadioGroup/RadioGroup.tsx", + "actualName": "RadioGroup", + "exportName": "default" + } + }, + "components-resizable": { + "id": "components-resizable", + "name": "Resizable", + "path": "./src/components/Resizable/tests/Resizable.stories.tsx", + "stories": [ + { + "name": "direction", + "snippet": "const direction = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n {/* Test that page doesn't scroll on dragging */}\n
\n \n );\n};" + }, + { + "name": "children", + "snippet": "const children = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n};" + }, + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "layout", + "snippet": "const layout = () => (\n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n \n \n \n \n \n \n
\n);" + } + ], + "import": "import { Button, Example, Resizable, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Resizable", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Resizable/Resizable.tsx", + "actualName": "Resizable", + "exportName": "default" + } + }, + "components-scrim": { + "id": "components-scrim", + "name": "Scrim", + "path": "./src/components/Scrim/tests/Scrim.stories.tsx", + "stories": [ + { + "name": "position", + "snippet": "const position = () => (\n \n \n }>Scrim\n \n\n \n }>\n Scrim\n \n \n\n \n }>\n Scrim\n \n \n\n \n }>\n Scrim\n \n \n\n \n }>\n Scrim\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Content\n \n
\n);" + }, + { + "name": "composition", + "snippet": "const composition = () => (\n \n \n
\n Text\n
\n
\n
\n);" + } + ], + "import": "import { Example, Placeholder, Scrim } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Scrim", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Scrim/Scrim.tsx", + "actualName": "Scrim", + "exportName": "default" + } + }, + "components-select": { + "id": "components-select", + "name": "Select", + "path": "./src/components/Select/tests/Select.stories.tsx", + "stories": [ + { + "name": "nativeRender", + "snippet": "const nativeRender = () => (\n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "customRender", + "snippet": "const customRender = () => (\n \n \n \n Dog\n Turtle\n \n \n \n \n \n Pigeon\n Parrot\n \n \n Whale\n Dolphin\n \n \n \n \n);" + }, + { + "name": "nativeHandlers", + "snippet": "const nativeHandlers = () => \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n;" + }, + { + "name": "customHandlers", + "snippet": "const customHandlers = () => \n \n \n Dog\n Turtle\n \n \n \n \n Dog\n Turtle\n \n \n \n \n Dog\n Turtle\n \n \n;" + }, + { + "name": "triggerOnly", + "snippet": "const triggerOnly = (args) => {\n const toggle = useToggle();\n const [value, setValue] = React.useState(\"Dog\");\n\n const handleClick: SelectProps[\"onClick\"] = (e) => {\n args.handleClick(e);\n toggle.toggle();\n };\n\n return (\n \n \n \n {value}\n \n \n
\n {\n setValue(\"Dog\");\n toggle.deactivate();\n }}\n attributes={{\n role: \"option\",\n }}\n >\n Dog\n \n {\n setValue(\"Turtle\");\n toggle.deactivate();\n }}\n >\n Turtle\n \n
\n \n
\n
\n );\n};" + }, + { + "name": "multiple", + "snippet": "const multiple = () => \n \n \n Dog\n Turtle\n \n \n;" + }, + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n \n \n \n\n \n \n Dog\n Turtle\n \n \n\n \n \n \n \n \n \n\n \n \n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n Dog\n Turtle\n \n \n \n \n Dog\n Turtle\n \n \n \n \n Dog\n Turtle\n \n \n \n \n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "startSlot", + "snippet": "const startSlot = () => (\n \n \n \n Dog\n Turtle\n \n \n\n \n }\n >\n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "renderValue", + "snippet": "const renderValue = () => {\n const options = [\n {\n value: \"1\",\n label: \"Title 1\",\n subtitle: \"Subtitle 1\",\n },\n {\n value: \"2\",\n label: \"Title 2\",\n subtitle: \"Subtitle 2\",\n },\n ];\n\n return (\n \n \n \n {options.map((option) => (\n \n {option.label}\n \n {option.subtitle}\n \n \n ))}\n \n \n\n \n \n {options.map((option) => (\n \n {option.label}\n \n {option.subtitle}\n \n \n ))}\n \n \n\n \n Title {args.value}}\n >\n {options.map((option) => (\n \n {option.label}\n \n {option.subtitle}\n \n \n ))}\n \n \n\n \n Titles {args.value.join(\", \")}}\n >\n {options.map((option) => (\n \n {option.label}\n \n {option.subtitle}\n \n \n ))}\n \n \n \n );\n};" + }, + { + "name": "error", + "snippet": "const error = () => (\n \n \n \n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n \n \n \n \n \n \n \n \n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n \n \n \n \n \n \n \n \n \n Dog\n Turtle\n \n \n \n);" + }, + { + "name": "fallback", + "snippet": "const fallback = () => (\n \n \n \n {[...Array(100)].map((_, index) => (\n \n Item {index + 1}\n \n ))}\n \n
\n \n {[...Array(100)].map((_, index) => (\n \n Item {index + 1}\n \n ))}\n \n
\n
\n);" + }, + { + "name": "formControl", + "snippet": "const formControl = () => (\n \n \n \n Animal\n \n Dog\n Turtle\n \n This field is required\n \n \n \n);" + }, + { + "name": "testComposition", + "snippet": "const testComposition = () => (\n \n \n \n \n Dog\n Turtle\n \n Hello\n \n \n \n);" + } + ], + "import": "import { Badge, Example, FormControl, MenuItem, Modal, Placeholder, Select, Text } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Select", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Select/Select.tsx", + "actualName": "Select", + "exportName": "default" + } + }, + "components-skeleton": { + "id": "components-skeleton", + "name": "Skeleton", + "path": "./src/components/Skeleton/tests/Skeleton.stories.tsx", + "stories": [ + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n);" + }, + { + "name": "radius", + "snippet": "const radius = () => (\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + } + ], + "import": "import { Example, Skeleton } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Skeleton", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Skeleton/Skeleton.tsx", + "actualName": "Skeleton", + "exportName": "default" + } + }, + "components-slider": { + "id": "components-slider", + "name": "Slider", + "path": "./src/components/Slider/tests/Slider.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n \n \n \n \n \n \n \n \n \n \n
\n \n);" + }, + { + "name": "orientation", + "snippet": "const orientation = () => (\n \n \n \n \n \n \n \n \n);" + }, + { + "name": "minMax", + "snippet": "const minMax = () => (\n \n \n \n \n \n \n \n max\">\n \n \n \n);" + }, + { + "name": "step", + "snippet": "const step = () => (\n \n \n \n \n\n \n \n \n\n \n \n \n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n \n \n \n\n \n \n \n \n);" + }, + { + "name": "renderValue", + "snippet": "const renderValue = () => (\n \n \n `$${args.value}`} />\n \n \n \n \n \n);" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => \n \n;" + }, + { + "name": "value", + "snippet": "const value = () => \n \n;" + }, + { + "name": "rangeDefaultValue", + "snippet": "const rangeDefaultValue = () => \n \n;" + }, + { + "name": "rangeValue", + "snippet": "const rangeValue = () => \n \n;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n
\n);" + }, + { + "name": "testForm", + "snippet": "const testForm = (args) => {\n const formRef = React.useRef(null);\n const [data, setData] = React.useState<[string, FormDataEntryValue][]>([]);\n\n const handleChange = (e: React.FormEvent) => {\n const formData = new FormData(e.currentTarget);\n const nextState = [...formData.entries()];\n\n args.handleFormChange({ formData: nextState });\n setData(nextState);\n };\n\n return (\n \n
\n \n \n\n {data.map((v) => v.join(\": \")).join(\", \")}\n
\n );\n};" + }, + { + "name": "testSwipe", + "snippet": "const testSwipe = () => {\n const toggle = useToggle(true);\n\n return (\n \n \n Modal\n \n \n \n );\n};" + } + ], + "import": "import { Example, Modal, Slider, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Slider", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Slider/Slider.tsx", + "actualName": "Slider", + "exportName": "default" + } + }, + "components-stepper": { + "id": "components-stepper", + "name": "Stepper", + "path": "./src/components/Stepper/tests/Stepper.stories.tsx", + "stories": [ + { + "name": "direction", + "snippet": "const direction = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "labelDisplay", + "snippet": "const labelDisplay = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "gap", + "snippet": "const gap = () => (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n
\n);" + }, + { + "name": "edgeCases", + "snippet": "const edgeCases = () => (\n \n \n \n \n \n);" + } + ], + "import": "import { Button, Example, Placeholder, Stepper, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Stepper", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Stepper/Stepper.tsx", + "actualName": "Stepper", + "exportName": "default" + } + }, + "components-switch": { + "id": "components-switch", + "name": "Switch", + "path": "./src/components/Switch/tests/Switch.stories.tsx", + "stories": [ + { + "name": "size", + "snippet": "const size = () => (\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\n);" + }, + { + "name": "label", + "snippet": "const label = () => (\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\n\t\t\n\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tWi-fi\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n);" + }, + { + "name": "defaultChecked", + "snippet": "const defaultChecked = () => Label\n ;" + }, + { + "name": "checked", + "snippet": "const checked = () => Label\n ;" + }, + { + "name": "disabled", + "snippet": "const disabled = () => (\n \n \n \n \n \n \n \n \n \n Switch\n \n \n \n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n Label\n \n
\n);" + } + ], + "import": "import { Example, Switch, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Switch", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Switch/Switch.tsx", + "actualName": "Switch", + "exportName": "default" + } + }, + "components-table": { + "id": "components-table", + "name": "Table", + "path": "./src/components/Table/tests/Table.stories.tsx", + "stories": [ + { + "name": "layout", + "snippet": "const layout = () => (\n \n \n \n \n \n Column 1\n Column 2\n \n \n \n \n Cell 1\n Cell 2\n \n \n
\n
\n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n Cell 3\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n Column 1\n Column 2\n Column 2\n \n \n \n \n \n Cell 2\n Cell 3\n \n
\n
\n \n \n \n \n Column 1\n \n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n
\n);" + }, + { + "name": "border", + "snippet": "const border = () => (\n \n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n
\n);" + }, + { + "name": "rows", + "snippet": "const rows = () => (\n \n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n Column 1\n Column 2\n \n {}}>\n Cell 1\n Cell 2\n \n
\n
\n
\n);" + }, + { + "name": "render", + "snippet": "const render = () => (\n \n \n \n Heading\n Heading\n \n \n \n \n Content\n Content\n \n \n
\n);" + }, + { + "name": "tbody", + "snippet": "const tbody = () => (\n \n \n Heading\n Heading\n \n
\n);" + }, + { + "name": "tabIndex", + "snippet": "const tabIndex = () => (\n \n \n \n \n {}}>\n \n \n {} }}>\n \n \n
\n);" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n \n
\n
\n);" + }, + { + "name": "edgeCases", + "snippet": "const edgeCases = () => (\n \n \n \n \n Column 1\n Column 2\n \n \n Cell 1\n Cell 2\n \n
\n
\n \n \n \n \n \n Column 1\n \n \n Column 2\n \n \n \n Cell 1\n Cell 2\n Cell 3\n \n \n Cell 1\n Cell 2\n Cell 3\n \n
\n
\n
\n \n \n \n Column 1\n Column 2\n Column 3\n Long heading title\n \n \n Cell 1\n Cell 2\n Cell 3\n Cell 4\n \n
\n
\n
\n);" + }, + { + "name": "examples", + "snippet": "const examples = () => (\n \n \n \n \n \n);" + } + ], + "import": "import { Card, Checkbox, Example, Table, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Table", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Table/Table.tsx", + "actualName": "Table", + "exportName": "default" + } + }, + "components-tabs": { + "id": "components-tabs", + "name": "Tabs", + "path": "./src/components/Tabs/tests/Tabs.stories.tsx", + "stories": [ + { + "name": "base", + "snippet": "const base = () => (\n \n \n Item 1\n Item 2\n \n\n \n Content 1\n \n \n Content 2\n \n \n);" + }, + { + "name": "variant", + "snippet": "const variant = () => (\n \n \n \n \n Long item 2\n Item 1\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n);" + }, + { + "name": "size", + "snippet": "const size = () => (\n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n\n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n);" + }, + { + "name": "direction", + "snippet": "const direction = () => (\n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n \n \n \n);" + }, + { + "name": "icon", + "snippet": "const icon = () => (\n \n \n \n \n \n Item 1\n \n \n Long item 2\n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "equalWidth", + "snippet": "const equalWidth = () => (\n \n \n \n \n \n Item 1\n \n \n Long item 2\n \n \n Very long item 3\n \n \n \n \n \n);" + }, + { + "name": "href", + "snippet": "const href = () => (\n \n \n \n \n \n Item 1\n \n \n Long item 2\n \n \n Very long item 3\n \n \n \n \n \n);" + }, + { + "name": "disabled", + "snippet": "const disabled = () => {\n return (\n \n \n \n \n Item 1\n \n Item 2\n \n Item 3\n \n\n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n\n \n \n \n \n Item 1\n \n \n Item 2\n \n \n Item 3\n \n \n\n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n \n );\n};" + }, + { + "name": "defaultValue", + "snippet": "const defaultValue = () => \n \n Item 1\n Item 2\n \n Content 1\n Content 2\n;" + }, + { + "name": "value", + "snippet": "const value = () => \n \n Item 1\n Item 2\n \n Content 1\n Content 2\n;" + }, + { + "name": "className", + "snippet": "const className = () => (\n
\n \n \n \n Item\n \n \n\n \n \n
\n);" + }, + { + "name": "testFocusableContent", + "snippet": "const testFocusableContent = () => (\n \n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n\n Tab 1\n Tab 2\n Tab 3\n \n \n \n\n \n \n \n \n Item 1\n Long item 2\n Very long item 3\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n);" + }, + { + "name": "testComposition", + "snippet": "const testComposition = () => (\n \n \n \n \n {[...Array(8)].map((_, i) => (\n \n Very long item {i}\n \n ))}\n \n\n {[...Array(8)].map((_, i) => (\n \n Tab {i}\n \n ))}\n \n \n \n \n \n Item 1\n Long item 2\n \n \n Item 3\n \n \n \n \n \n \n \n \n Item 1\n \n Long item 2\n Very long item 3\n \n \n \n \n);" + }, + { + "name": "testEdgeCaseDom", + "snippet": "const testEdgeCaseDom = () => {\n const [activeItem, setActiveItem] = React.useState(\"1\");\n const sectionsRef = React.useRef(null);\n\n return (\n \n \n \n \n setActiveItem(args.value)}>\n \n Item 1\n Item 2\n Item 3\n Item 4\n \n \n\n {\n setActiveItem(Math.min(4, Math.floor(args.y * 10) + 1).toString());\n }}\n >\n \n \n Section 1\n\n \n {[...Array(4)].map((_, i) => (\n \n ))}\n \n \n\n \n Section 2\n\n \n {[...Array(4)].map((_, i) => (\n \n ))}\n \n \n\n \n Section 3\n\n \n {[...Array(4)].map((_, i) => (\n \n ))}\n \n \n\n \n Section 4\n\n \n {[...Array(4)].map((_, i) => (\n \n ))}\n \n \n \n \n \n \n \n \n );\n};" + } + ], + "import": "import { Button, Example, ScrollArea, Tabs, Text, View } from \"reshaped\";", + "jsDocTags": {}, + "reactDocgen": { + "description": "", + "methods": [], + "displayName": "Tabs", + "definedInFile": "/Users/shilman/projects/design-systems/reshaped/src/components/Tabs/Tabs.tsx", + "actualName": "Tabs", + "exportName": "default" + } + }, + "components-textarea": { + "id": "components-textarea", + "name": "TextArea", + "path": "./src/components/TextArea/tests/TextArea.stories.tsx", + "stories": [ + { + "name": "variants", + "snippet": "const variants = () => (\n \n \n