Initial commit: Clean DSS implementation

Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,433 @@
"""
Storybook Story Generator
Generates Storybook stories from React components.
"""
import re
from pathlib import Path
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from enum import Enum
class StoryTemplate(str, Enum):
"""Available story templates."""
CSF3 = "csf3" # Component Story Format 3 (latest)
CSF2 = "csf2" # Component Story Format 2
MDX = "mdx" # MDX format
@dataclass
class PropInfo:
"""Information about a component prop."""
name: str
type: str = "unknown"
required: bool = False
default_value: Optional[str] = None
description: str = ""
options: List[str] = field(default_factory=list) # For enum/union types
@dataclass
class ComponentMeta:
"""Metadata about a component for story generation."""
name: str
path: str
props: List[PropInfo] = field(default_factory=list)
description: str = ""
has_children: bool = False
class StoryGenerator:
"""
Generates Storybook stories from component information.
"""
def __init__(self, root_path: str):
self.root = Path(root_path).resolve()
async def generate_story(
self,
component_path: str,
template: StoryTemplate = StoryTemplate.CSF3,
include_variants: bool = True,
output_path: Optional[str] = None,
) -> str:
"""
Generate a Storybook story for a component.
Args:
component_path: Path to the component file
template: Story template format
include_variants: Generate variant stories
output_path: Optional path to write the story file
Returns:
Generated story code
"""
# Parse component
meta = await self._parse_component(component_path)
# Generate story based on template
if template == StoryTemplate.CSF3:
story = self._generate_csf3(meta, include_variants)
elif template == StoryTemplate.CSF2:
story = self._generate_csf2(meta, include_variants)
else:
story = self._generate_mdx(meta, include_variants)
# Write to file if output path provided
if output_path:
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(story)
return story
async def _parse_component(self, component_path: str) -> ComponentMeta:
"""Parse a React component to extract metadata."""
path = self.root / component_path if not Path(component_path).is_absolute() else Path(component_path)
content = path.read_text(encoding="utf-8", errors="ignore")
component_name = path.stem
props = []
# Extract props from interface/type
# interface ButtonProps { variant?: 'primary' | 'secondary'; ... }
props_pattern = re.compile(
r'(?:interface|type)\s+\w*Props\s*(?:=\s*)?\{([^}]+)\}',
re.DOTALL
)
props_match = props_pattern.search(content)
if props_match:
props_content = props_match.group(1)
# Parse each prop line
for line in props_content.split('\n'):
line = line.strip()
if not line or line.startswith('//'):
continue
# Match: propName?: type; or propName: type;
prop_match = re.match(
r'(\w+)(\?)?:\s*([^;/]+)',
line
)
if prop_match:
prop_name = prop_match.group(1)
is_optional = prop_match.group(2) == '?'
prop_type = prop_match.group(3).strip()
# Extract options from union types
options = []
if '|' in prop_type:
# 'primary' | 'secondary' | 'ghost'
options = [
o.strip().strip("'\"")
for o in prop_type.split('|')
if o.strip().startswith(("'", '"'))
]
props.append(PropInfo(
name=prop_name,
type=prop_type,
required=not is_optional,
options=options,
))
# Check if component uses children
has_children = 'children' in content.lower() and (
'React.ReactNode' in content or
'ReactNode' in content or
'{children}' in content
)
# Extract component description from JSDoc
description = ""
jsdoc_match = re.search(r'/\*\*\s*\n\s*\*\s*([^\n*]+)', content)
if jsdoc_match:
description = jsdoc_match.group(1).strip()
return ComponentMeta(
name=component_name,
path=component_path,
props=props,
description=description,
has_children=has_children,
)
def _generate_csf3(self, meta: ComponentMeta, include_variants: bool) -> str:
"""Generate CSF3 format story."""
lines = [
f"import type {{ Meta, StoryObj }} from '@storybook/react';",
f"import {{ {meta.name} }} from './{meta.name}';",
"",
f"const meta: Meta<typeof {meta.name}> = {{",
f" title: 'Components/{meta.name}',",
f" component: {meta.name},",
" parameters: {",
" layout: 'centered',",
" },",
" tags: ['autodocs'],",
]
# Add argTypes for props with options
arg_types = []
for prop in meta.props:
if prop.options:
arg_types.append(
f" {prop.name}: {{\n"
f" options: {prop.options},\n"
f" control: {{ type: 'select' }},\n"
f" }},"
)
if arg_types:
lines.append(" argTypes: {")
lines.extend(arg_types)
lines.append(" },")
lines.extend([
"};",
"",
"export default meta;",
f"type Story = StoryObj<typeof {meta.name}>;",
"",
])
# Generate default story
default_args = self._get_default_args(meta)
lines.extend([
"export const Default: Story = {",
" args: {",
])
for key, value in default_args.items():
lines.append(f" {key}: {value},")
lines.extend([
" },",
"};",
])
# Generate variant stories
if include_variants:
variant_prop = next(
(p for p in meta.props if p.name == 'variant' and p.options),
None
)
if variant_prop:
for variant in variant_prop.options:
story_name = variant.title().replace('-', '').replace('_', '')
lines.extend([
"",
f"export const {story_name}: Story = {{",
" args: {",
f" ...Default.args,",
f" variant: '{variant}',",
" },",
"};",
])
# Size variants
size_prop = next(
(p for p in meta.props if p.name == 'size' and p.options),
None
)
if size_prop:
for size in size_prop.options:
story_name = f"Size{size.title()}"
lines.extend([
"",
f"export const {story_name}: Story = {{",
" args: {",
f" ...Default.args,",
f" size: '{size}',",
" },",
"};",
])
# Disabled state
disabled_prop = next(
(p for p in meta.props if p.name == 'disabled'),
None
)
if disabled_prop:
lines.extend([
"",
"export const Disabled: Story = {",
" args: {",
" ...Default.args,",
" disabled: true,",
" },",
"};",
])
return "\n".join(lines)
def _generate_csf2(self, meta: ComponentMeta, include_variants: bool) -> str:
"""Generate CSF2 format story."""
lines = [
f"import React from 'react';",
f"import {{ {meta.name} }} from './{meta.name}';",
"",
"export default {",
f" title: 'Components/{meta.name}',",
f" component: {meta.name},",
"};",
"",
f"const Template = (args) => <{meta.name} {{...args}} />;",
"",
"export const Default = Template.bind({});",
"Default.args = {",
]
default_args = self._get_default_args(meta)
for key, value in default_args.items():
lines.append(f" {key}: {value},")
lines.append("};")
# Generate variant stories
if include_variants:
variant_prop = next(
(p for p in meta.props if p.name == 'variant' and p.options),
None
)
if variant_prop:
for variant in variant_prop.options:
story_name = variant.title().replace('-', '').replace('_', '')
lines.extend([
"",
f"export const {story_name} = Template.bind({{}});",
f"{story_name}.args = {{",
f" ...Default.args,",
f" variant: '{variant}',",
"};",
])
return "\n".join(lines)
def _generate_mdx(self, meta: ComponentMeta, include_variants: bool) -> str:
"""Generate MDX format story."""
lines = [
f"import {{ Meta, Story, Canvas, ArgsTable }} from '@storybook/blocks';",
f"import {{ {meta.name} }} from './{meta.name}';",
"",
f"<Meta title=\"Components/{meta.name}\" component={{{meta.name}}} />",
"",
f"# {meta.name}",
"",
]
if meta.description:
lines.extend([meta.description, ""])
lines.extend([
"## Default",
"",
"<Canvas>",
f" <Story name=\"Default\">",
f" <{meta.name}",
])
default_args = self._get_default_args(meta)
for key, value in default_args.items():
lines.append(f" {key}={value}")
lines.extend([
f" />",
" </Story>",
"</Canvas>",
"",
"## Props",
"",
f"<ArgsTable of={{{meta.name}}} />",
])
return "\n".join(lines)
def _get_default_args(self, meta: ComponentMeta) -> Dict[str, str]:
"""Get default args for a component."""
args = {}
for prop in meta.props:
if prop.name == 'children' and meta.has_children:
args['children'] = f"'{meta.name}'"
elif prop.name == 'variant' and prop.options:
args['variant'] = f"'{prop.options[0]}'"
elif prop.name == 'size' and prop.options:
args['size'] = f"'{prop.options[0]}'"
elif prop.name == 'disabled':
args['disabled'] = 'false'
elif prop.name == 'onClick':
args['onClick'] = '() => console.log("clicked")'
elif prop.required and prop.default_value:
args[prop.name] = prop.default_value
# Ensure children for button-like components
if meta.has_children and 'children' not in args:
args['children'] = f"'{meta.name}'"
return args
async def generate_stories_for_directory(
self,
directory: str,
template: StoryTemplate = StoryTemplate.CSF3,
dry_run: bool = True,
) -> List[Dict[str, str]]:
"""
Generate stories for all components in a directory.
Args:
directory: Path to component directory
template: Story template format
dry_run: If True, only return what would be generated
Returns:
List of dicts with component path and generated story
"""
results = []
dir_path = self.root / directory
if not dir_path.exists():
return results
# Find component files
for pattern in ['*.tsx', '*.jsx']:
for comp_path in dir_path.glob(pattern):
# Skip story files, test files, index files
if any(x in comp_path.name.lower() for x in ['.stories.', '.test.', '.spec.', 'index.']):
continue
# Skip non-component files (not PascalCase)
if not comp_path.stem[0].isupper():
continue
try:
rel_path = str(comp_path.relative_to(self.root))
story = await self.generate_story(rel_path, template)
# Determine story output path
story_path = comp_path.with_suffix('.stories.tsx')
result = {
'component': rel_path,
'story_path': str(story_path.relative_to(self.root)),
'story': story,
}
if not dry_run:
story_path.write_text(story)
result['written'] = True
results.append(result)
except Exception as e:
results.append({
'component': str(comp_path),
'error': str(e),
})
return results