Scaffold a new component with story, tests, and optional visual mockup following SOTA patterns
Scaffold production-ready components with stories, tests, and optional AI-generated visual mockups.
/plugin marketplace add flight505/storybook-assistant-plugin/plugin install flight505-storybook-assistant@flight505/storybook-assistant-pluginWhen the user invokes /create-component, guide them through creating a new component with all necessary files: component implementation, Storybook story, tests, and optional visual mockup.
This command provides an interactive workflow for scaffolding production-ready components:
Run framework detection to understand the project setup:
bash ${CLAUDE_PLUGIN_ROOT}/scripts/detect-framework.sh
This provides:
Use AskUserQuestion to gather component requirements:
AskUserQuestion({
questions: [
{
question: "What type of component are you creating?",
header: "Component Type",
multiSelect: false,
options: [
{
label: "Button",
description: "Interactive button with variants (primary, secondary, etc.)"
},
{
label: "Input",
description: "Form input with validation and states"
},
{
label: "Card",
description: "Content container with optional header, footer, image"
},
{
label: "Modal / Dialog",
description: "Overlay dialog with backdrop and focus management"
},
{
label: "Table / DataGrid",
description: "Data table with sorting, filtering, pagination"
},
{
label: "Form",
description: "Form layout with validation and submission"
},
{
label: "Navigation (Menu/Tabs)",
description: "Navigation component with routing"
},
{
label: "Layout (Container/Grid)",
description: "Layout component for page structure"
},
{
label: "Custom",
description: "Generic component (will ask for props)"
}
]
},
{
question: "What should the component be named? (PascalCase, e.g., 'MyButton')",
header: "Component Name",
multiSelect: false,
options: [
// This will be a text input via "Other" option
{
label: "Enter custom name",
description: "Use PascalCase (e.g., MyButton, UserCard, DataTable)"
}
]
},
{
question: "What level of testing should I include?",
header: "Testing Level",
multiSelect: false,
options: [
{
label: "Full Testing (Recommended)",
description: "Component + Story + Interaction tests + A11y tests"
},
{
label: "Standard Testing",
description: "Component + Story + Interaction tests"
},
{
label: "Basic",
description: "Component + Story only"
},
{
label: "Minimal",
description: "Component only (no story or tests)"
}
]
},
{
question: "Should I generate a visual mockup for design reference?",
header: "Visual Mockup",
multiSelect: false,
options: [
{
label: "Yes - Generate AI mockup (Recommended)",
description: "Creates visual reference using NanoBanana (requires OPENROUTER_API_KEY)"
},
{
label: "No - Skip mockup",
description: "Generate component files only"
}
]
}
]
})
After receiving answers, process and prepare for generation:
import json
import os
# Extract answers
component_type = answers['Component Type']
component_name_raw = answers['Component Name']
testing_level_raw = answers['Testing Level']
generate_mockup_raw = answers['Visual Mockup']
# Clean component name (from "Other" text input)
component_name = component_name_raw.strip()
# Validate component name (PascalCase)
if not component_name[0].isupper():
component_name = component_name[0].upper() + component_name[1:]
# Map testing level
testing_level_map = {
"Full Testing (Recommended)": "full",
"Standard Testing": "standard",
"Basic": "basic",
"Minimal": "minimal"
}
testing_level = testing_level_map.get(testing_level_raw, "full")
# Determine if mockup should be generated
generate_mockup = "Yes" in generate_mockup_raw and os.getenv('OPENROUTER_API_KEY')
# Get component template based on type
component_template = get_component_template(component_type, framework)
Invoke the component scaffold script:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/component-scaffold/scripts/create_component.py \
--name "${COMPONENT_NAME}" \
--type "${COMPONENT_TYPE}" \
--framework "${FRAMEWORK}" \
--typescript \
--output "${COMPONENT_DIR}/${COMPONENT_NAME}.tsx"
This creates the component file with:
Use the existing story generation system:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/story-generation/scripts/generate_story.py \
"${COMPONENT_DIR}/${COMPONENT_NAME}.tsx" \
--level "${TESTING_LEVEL}" \
--output "${COMPONENT_DIR}/${COMPONENT_NAME}.stories.tsx"
If user requested mockup and OPENROUTER_API_KEY is available:
# Build context-aware prompt based on component type and project
mockup_prompt = f"""
Modern {component_type} component for {framework} application.
Component name: {component_name}
Style: Clean, professional, follows design system best practices
Include: {get_type_specific_elements(component_type)}
Color scheme: Modern, accessible (WCAG AA compliant)
Layout: Responsive, mobile-friendly
"""
# Generate mockup
python3 ${CLAUDE_PLUGIN_ROOT}/skills/visual-design/scripts/generate_mockup.py \
"${mockup_prompt}" \
--model "google/gemini-3.0-pro-image" \
--output "${COMPONENT_DIR}/mockups/${COMPONENT_NAME}.png"
Show the user what was created:
═══════════════════════════════════════════════
Component Created: ${COMPONENT_NAME}
═══════════════════════════════════════════════
Type: ${COMPONENT_TYPE}
Framework: ${FRAMEWORK}
Files Created:
✓ ${COMPONENT_DIR}/${COMPONENT_NAME}.tsx
✓ ${COMPONENT_DIR}/${COMPONENT_NAME}.stories.tsx
${MOCKUP_CREATED ? `✓ ${COMPONENT_DIR}/mockups/${COMPONENT_NAME}.png` : ''}
Component Features:
✓ TypeScript interfaces with proper types
✓ Accessibility attributes (ARIA labels, roles)
✓ ${PROPS_COUNT} props with sensible defaults
✓ JSDoc documentation
${TESTING_LEVEL === 'full' ? '✓ Interaction tests with play functions' : ''}
${TESTING_LEVEL === 'full' ? '✓ Accessibility tests with axe-core' : ''}
Next Steps:
1. Review component: ${COMPONENT_DIR}/${COMPONENT_NAME}.tsx
2. Customize props and styling as needed
3. Run Storybook: npm run storybook
4. View your component in the browser
${TESTING_LEVEL !== 'minimal' ? '5. Run tests: npm run test-storybook' : ''}
Features:
Props Generated:
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
onClick?: () => void;
children: React.ReactNode;
}
Features:
Props Generated:
interface InputProps {
label: string;
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
value?: string;
error?: string;
helperText?: string;
required?: boolean;
disabled?: boolean;
onChange?: (value: string) => void;
}
Features:
Props Generated:
interface CardProps {
variant?: 'elevated' | 'outlined' | 'flat';
image?: string;
imageAlt?: string;
header?: React.ReactNode;
footer?: React.ReactNode;
onClick?: () => void;
children: React.ReactNode;
}
Features:
Props Generated:
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
size?: 'small' | 'medium' | 'large' | 'fullscreen';
closeOnBackdropClick?: boolean;
closeOnEsc?: boolean;
children: React.ReactNode;
}
Features:
Props Generated:
interface TableColumn<T> {
key: keyof T;
header: string;
sortable?: boolean;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface TableProps<T> {
data: T[];
columns: TableColumn<T>[];
sortable?: boolean;
selectable?: boolean;
pagination?: boolean;
pageSize?: number;
loading?: boolean;
emptyMessage?: string;
onRowClick?: (row: T) => void;
}
For "Custom" type, ask follow-up questions:
AskUserQuestion({
questions: [
{
question: "What props does your component need? (comma-separated, e.g., 'title, description, onClick')",
header: "Component Props",
multiSelect: false,
options: [
{
label: "Enter prop names",
description: "List props separated by commas"
}
]
},
{
question: "Should this component accept children?",
header: "Children Support",
multiSelect: false,
options: [
{
label: "Yes - Component wraps content",
description: "Add children prop (React.ReactNode)"
},
{
label: "No - Self-contained component",
description: "No children prop"
}
]
}
]
})
import React from 'react';
import './{{COMPONENT_NAME}}.css';
export interface {{COMPONENT_NAME}}Props {
{{PROPS}}
}
/**
* {{COMPONENT_DESCRIPTION}}
*/
export function {{COMPONENT_NAME}}({
{{PROP_DESTRUCTURING}}
}: {{COMPONENT_NAME}}Props) {
{{COMPONENT_LOGIC}}
return (
<div className="{{COMPONENT_CLASS}}" {{ATTRIBUTES}}>
{{COMPONENT_CONTENT}}
</div>
);
}
{{COMPONENT_NAME}}.displayName = '{{COMPONENT_NAME}}';
<script setup lang="ts">
interface {{COMPONENT_NAME}}Props {
{{PROPS}}
}
const props = defineProps<{{COMPONENT_NAME}}Props>();
{{COMPONENT_LOGIC}}
</script>
<template>
<div class="{{COMPONENT_CLASS}}" {{ATTRIBUTES}}>
{{COMPONENT_CONTENT}}
</div>
</template>
<style scoped>
.{{COMPONENT_CLASS}} {
{{BASE_STYLES}}
}
</style>
<script lang="ts">
export let {{PROPS}};
{{COMPONENT_LOGIC}}
</script>
<div class="{{COMPONENT_CLASS}}" {{ATTRIBUTES}}>
{{COMPONENT_CONTENT}}
</div>
<style>
.{{COMPONENT_CLASS}} {
{{BASE_STYLES}}
}
</style>
The script provides sensible defaults based on component type:
| Type | Default Props | Variants | Features |
|---|---|---|---|
| Button | variant, size, disabled, loading, onClick, children | 4 variants, 3 sizes | Icon support, loading state |
| Input | label, type, value, error, onChange | 4 types | Validation, helper text |
| Card | variant, image, header, footer, children | 3 variants | Clickable, image support |
| Modal | isOpen, onClose, title, size, children | 4 sizes | Focus trap, ESC/backdrop close |
| Table | data, columns, sortable, pagination | - | Sorting, selection, pagination |
| Form | onSubmit, loading, error | - | Validation, error handling |
| Navigation | items, activeItem, onChange | - | Routing support |
| Layout | children, spacing, direction | - | Flex/Grid layout |
Component Already Exists:
⚠ Component already exists: src/components/Button.tsx
Options:
1. Choose a different name
2. Overwrite existing (use --force flag)
3. Cancel operation
Invalid Component Name:
✗ Invalid component name: "my-button"
Component names must:
• Start with uppercase letter (PascalCase)
• Contain only letters and numbers
• Not contain spaces or special characters
Examples: MyButton, UserCard, DataTable
Framework Detection Failed:
✗ Could not detect project framework
Please specify framework manually:
/create-component --framework react
/create-component --framework vue
/create-component --framework svelte
No Component Directory:
⚠ Could not find component directory
Suggested directories:
• src/components
• src/lib/components
• components
Create directory? (y/n)
# Custom props
/create-component --name MyButton --type button --props "label:string,onClick:function,disabled:boolean"
# Specify output directory
/create-component --name UserCard --type card --output src/features/user/components
# Generate component with custom variants
/create-component --name StatusBadge --type custom --variants "success,warning,error,info"
If project uses Tauri, automatically include IPC mocking in stories:
// Auto-generated in story file
export const WithTauriAPI: Story = {
args: { /* ... */ },
decorators: [
(Story) => {
if (typeof window !== 'undefined') {
window.__TAURI__ = {
invoke: async (cmd) => ({ success: true }),
};
}
return <Story />;
},
],
};
If project uses Electron, include IPC mocking:
// Auto-generated in story file
export const WithElectronAPI: Story = {
args: { /* ... */ },
decorators: [
(Story) => {
if (typeof window !== 'undefined') {
window.api = {
// Mock electron API
};
}
return <Story />;
},
],
};