From 378761762558e05021ec1aacab5dc39abc5966e3 Mon Sep 17 00:00:00 2001 From: cabana8471 Date: Sat, 31 Jan 2026 02:12:24 +0100 Subject: [PATCH 1/3] feat: add MCP servers for UI component libraries Add support for UI component library selection during project creation: - Add Phase 3b (UI library) and Phase 3c (visual style) to create-spec - Add app_spec_parser.py for shared UI config parsing - Add design_tokens.py with style presets (neobrutalism, glassmorphism, retro) - Update client.py with UI_MCP_TOOLS and dynamic MCP server configuration - Add UILibrarySelector and VisualStyleSelector React components - Generate design tokens on spec creation completion - Add 36 unit tests for UI configuration parsing Supported libraries: shadcn-ui (React), ark-ui (multi-framework) Libraries with MCP get rapid component generation via npx servers. Co-Authored-By: Claude Opus 4.5 --- .claude/commands/create-spec.md | 72 +++- .claude/templates/app_spec.template.txt | 38 ++ .claude/templates/coding_prompt.template.md | 23 + .../templates/initializer_prompt.template.md | 21 + CLAUDE.md | 64 +++ app_spec_parser.py | 184 ++++++++ client.py | 108 +++++ design_tokens.py | 209 +++++++++ server/services/spec_chat_session.py | 20 + test_ui_config.py | 400 ++++++++++++++++++ ui/public/previews/README.md | 32 ++ ui/src/components/UILibrarySelector.tsx | 181 ++++++++ ui/src/components/VisualStyleSelector.tsx | 232 ++++++++++ 13 files changed, 1583 insertions(+), 1 deletion(-) create mode 100644 app_spec_parser.py create mode 100644 design_tokens.py create mode 100644 test_ui_config.py create mode 100644 ui/public/previews/README.md create mode 100644 ui/src/components/UILibrarySelector.tsx create mode 100644 ui/src/components/VisualStyleSelector.tsx diff --git a/.claude/commands/create-spec.md b/.claude/commands/create-spec.md index f0555d24..bea48b7e 100644 --- a/.claude/commands/create-spec.md +++ b/.claude/commands/create-spec.md @@ -95,7 +95,64 @@ Ask the user about their involvement preference: **For Detailed Mode users**, ask specific tech questions about frontend, backend, database, etc. -### Phase 3b: Database Requirements (MANDATORY) +### Phase 3b: UI Component Library (OPTIONAL) + +Ask the user which UI component library they prefer. This step helps configure automated component generation. + +> "Would you like to use a UI component library for faster development? +> +> 1. **shadcn/ui (Recommended for React)** - Beautiful, accessible components with Radix primitives +> - Copy-paste approach (you own the code) +> - MCP-enabled: Agent can generate components automatically +> +> 2. **Ark UI (Recommended for multi-framework)** - Headless primitives for any framework +> - Works with React, Vue, Solid, Svelte +> - MCP-enabled: Agent can generate components automatically +> +> 3. **Radix UI** - Low-level headless primitives +> - Requires manual styling +> - Uses frontend-design skill for component creation +> +> 4. **None/Custom** - Build components from scratch +> - Full control, more development time +> - Uses frontend-design skill for component creation +> +> 5. **Skip this question** - Use default (no library)" + +**Framework validation:** If user chooses shadcn/ui but their framework isn't React, warn them and suggest Ark UI instead. + +### Phase 3c: Visual Style (OPTIONAL) + +Ask the user which visual style they prefer. This step configures design tokens for consistent styling. + +> "What visual style do you prefer for your app? +> +> 1. **Modern/Clean (Recommended)** - Minimal, professional design +> - Uses library defaults, no custom tokens needed +> +> 2. **Neobrutalism** - Bold colors, hard shadows, no border-radius +> - High contrast, playful aesthetic +> - Generates design tokens: 4px borders, offset shadows +> +> 3. **Glassmorphism** - Frosted glass effects, blur, transparency +> - Subtle gradients, floating elements +> - Generates design tokens: backdrop-blur, low opacity backgrounds +> +> 4. **Retro/Arcade** - Pixel-art inspired, vibrant neons +> - 8-bit aesthetic, chunky borders +> - Generates design tokens: sharp corners, neon gradients +> +> 5. **Custom** - Define your own design tokens +> - Maximum flexibility +> +> 6. **Skip this question** - Use default (Modern/Clean)" + +**Behavior:** +- If user skips or chooses "Modern/Clean", no design tokens file is generated +- Other styles generate `.autocoder/design-tokens.json` with appropriate presets +- The coding agent will apply these tokens when building components + +### Phase 3d: Database Requirements (MANDATORY) **Always ask this question regardless of mode:** @@ -457,6 +514,19 @@ Create a new file using this XML structure: [Font preferences] + + + + [shadcn-ui | ark-ui | radix-ui | none] + [react | vue | solid | svelte] + [true if shadcn-ui or ark-ui, false otherwise] + + + + + + [.autocoder/design-tokens.json if style != default, empty otherwise] + diff --git a/.claude/templates/app_spec.template.txt b/.claude/templates/app_spec.template.txt index ebdfb856..ad3632bb 100644 --- a/.claude/templates/app_spec.template.txt +++ b/.claude/templates/app_spec.template.txt @@ -216,6 +216,44 @@ - Loading spinners - Skeleton loaders + + + + none + react + false + + + + + + + diff --git a/.claude/templates/coding_prompt.template.md b/.claude/templates/coding_prompt.template.md index c8d3ba6b..df5243d1 100644 --- a/.claude/templates/coding_prompt.template.md +++ b/.claude/templates/coding_prompt.template.md @@ -111,6 +111,7 @@ Use browser automation tools: **Complete ALL applicable checks before marking any feature as passing:** +- **UI & Design:** Used MCP tools for component generation when available (`mcp__ui_components__*`); components follow the project's visual style (check `app_spec.txt`); if design tokens exist (`.autoforge/design-tokens.json`), styling matches; components are accessible (ARIA labels, keyboard nav); responsive at desktop (1920px), tablet (768px), mobile (375px) - **Security:** Feature respects role permissions; unauthenticated access blocked; API checks auth (401/403); no cross-user data leaks via URL manipulation - **Real Data:** Create unique test data via UI, verify it appears, refresh to confirm persistence, delete and verify removal. No unexplained data (indicates mocks). Dashboard counts reflect real numbers - **Mock Data Grep:** Run STEP 5.6 grep checks - no hits in src/ (excluding tests). No globalThis, devStore, or dev-store patterns @@ -198,6 +199,28 @@ Test like a human user with mouse and keyboard. Use `browser_console_messages` t --- +## UI COMPONENT GENERATION + +When building UI components, check if MCP tools are available for component generation: + +- `mcp__ui_components__list_components` - See all available components in the library +- `mcp__ui_components__get_example` - Get implementation code for a component +- `mcp__ui_components__styling_guide` - Understand the styling approach + +**If these tools are available**, use them to generate components quickly instead of building from scratch. + +**If these tools are NOT available**, use the frontend-design skill for component creation: +- Invoke `/frontend-design` for complex UI components +- Follow the project's visual style from `app_spec.txt` +- Check `.autoforge/design-tokens.json` for style tokens if they exist + +**Visual Style Application:** +1. Read the `` section in `app_spec.txt` +2. If a design tokens file exists, apply those values to your CSS/Tailwind +3. Maintain consistency across all components + +--- + ## FEATURE TOOL USAGE RULES (CRITICAL - DO NOT VIOLATE) The feature tools exist to reduce token usage. **DO NOT make exploratory queries.** diff --git a/.claude/templates/initializer_prompt.template.md b/.claude/templates/initializer_prompt.template.md index bb914e2d..2c5822b8 100644 --- a/.claude/templates/initializer_prompt.template.md +++ b/.claude/templates/initializer_prompt.template.md @@ -272,6 +272,27 @@ The feature_list.json **MUST** include tests from ALL 20 categories. Minimum cou --- +## UI COMPONENT LIBRARY + +Check the `` section in `app_spec.txt` for the UI library configuration: + +- **library**: The component library to use (shadcn-ui, ark-ui, radix-ui, or none) +- **framework**: The frontend framework (react, vue, solid, svelte) +- **has_mcp**: Whether MCP tools are available for component generation + +If `has_mcp` is `true`, the coding agent will have access to MCP tools for rapid component generation. Factor this into feature descriptions where relevant. + +## VISUAL STYLE + +Check the `` section in `app_spec.txt` for styling configuration: + +- **style**: The visual aesthetic (default, neobrutalism, glassmorphism, retro, custom) +- **design_tokens_path**: Path to custom design tokens JSON if applicable + +For non-default styles, style-specific tests may be included (e.g., "Button has 4px border and offset shadow" for neobrutalism). + +--- + ## ABSOLUTE PROHIBITION: NO MOCK DATA The feature_list.json must include tests that **actively verify real data** and **detect mock data patterns**. diff --git a/CLAUDE.md b/CLAUDE.md index e0f9ea3e..b95cf2ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -287,6 +287,7 @@ Projects can be stored in any directory (registered in `~/.autoforge/registry.db - `.autoforge/features.db` - SQLite database with feature test cases - `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances - `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional) +- `.autoforge/design-tokens.json` - Visual style design tokens (generated for non-default styles) - `.autoforge/.gitignore` - Ignores runtime files - `CLAUDE.md` - Stays at project root (SDK convention) - `app_spec.txt` - Root copy for agent template compatibility @@ -398,6 +399,69 @@ blocked_commands: - `examples/org_config.yaml` - Org config example (all commented by default) - `examples/README.md` - Comprehensive guide with use cases, testing, and troubleshooting +### UI Component MCP Servers + +The agent can use MCP servers for rapid UI component generation when a compatible library is configured in `app_spec.txt`. + +**Supported Libraries:** +- `shadcn-ui` - Beautiful, accessible React components (MCP enabled) +- `ark-ui` - Headless primitives for React, Vue, Solid, Svelte (MCP enabled) +- `radix-ui` - Low-level headless primitives (no MCP, uses frontend-design skill) +- `none` - Custom components (no MCP, uses frontend-design skill) + +**Configuration in app_spec.txt:** +```xml + + shadcn-ui + react + true + +``` + +**MCP Tools Available:** +- `mcp__ui_components__list_components` - List available components +- `mcp__ui_components__get_example` - Get component implementation code +- `mcp__ui_components__styling_guide` - Get styling documentation + +**Environment Variables:** +- `DISABLE_UI_MCP=true` - Disable UI MCP server (for troubleshooting) +- `MCP_SHADCN_VERSION=1.0.0` - Pin shadcn MCP server version +- `MCP_ARK_VERSION=0.1.0` - Pin Ark UI MCP server version +- `GITHUB_PERSONAL_ACCESS_TOKEN` - GitHub token for better rate limits (optional) + +### Visual Styles and Design Tokens + +Projects can specify a visual style that generates design tokens for consistent styling. + +**Available Styles:** +- `default` - Clean, minimal design (no tokens generated) +- `neobrutalism` - Bold colors, hard shadows, 4px borders, no border-radius +- `glassmorphism` - Frosted glass effects, blur, transparency +- `retro` - Pixel-art inspired, vibrant neons, 8-bit aesthetic +- `custom` - User-defined tokens + +**Configuration in app_spec.txt:** +```xml + + + .autoforge/design-tokens.json + +``` + +**Design Tokens File (generated for non-default styles):** +```json +{ + "borders": {"width": "4px", "radius": "0"}, + "shadows": {"default": "4px 4px 0 0 currentColor"}, + "colors": {"primary": "#ff6b6b", "secondary": "#4ecdc4"} +} +``` + +**Files:** +- `app_spec_parser.py` - Shared parser for UI config from app_spec.txt +- `design_tokens.py` - Design token generation and style presets +- `test_ui_config.py` - Unit tests for UI configuration + ### Vertex AI Configuration (Optional) Run coding agents via Google Cloud Vertex AI: diff --git a/app_spec_parser.py b/app_spec_parser.py new file mode 100644 index 00000000..4b9e12a7 --- /dev/null +++ b/app_spec_parser.py @@ -0,0 +1,184 @@ +""" +App Spec Parser +=============== + +Shared utilities for parsing app_spec.txt sections. +Used by client.py, prompts.py, and design_tokens.py to avoid code duplication. +""" + +import re +from pathlib import Path +from typing import TypedDict + + +class UIComponentsConfig(TypedDict, total=False): + """Configuration for UI component library.""" + library: str # shadcn-ui, ark-ui, radix-ui, none + framework: str # react, vue, solid, svelte + has_mcp: bool # Whether MCP server is available + + +class VisualStyleConfig(TypedDict, total=False): + """Configuration for visual style.""" + style: str # default, neobrutalism, glassmorphism, retro, custom + design_tokens_path: str # Path to custom design tokens JSON + + +class UIConfig(TypedDict, total=False): + """Combined UI configuration.""" + library: str + framework: str + has_mcp: bool + style: str + design_tokens_path: str + + +def parse_section(content: str, section_name: str) -> str | None: + """ + Parse an XML section from app_spec.txt content. + + Args: + content: The full app_spec.txt content + section_name: The XML tag name to extract (e.g., "ui_components") + + Returns: + The content inside the section tags, or None if not found. + """ + pattern = rf"<{section_name}[^>]*>(.*?)" + match = re.search(pattern, content, re.DOTALL) + return match.group(1).strip() if match else None + + +def parse_xml_value(content: str, tag_name: str) -> str | None: + """ + Parse a single XML value from content. + + Args: + content: XML content to search + tag_name: The tag name to extract value from + + Returns: + The value inside the tags, or None if not found. + """ + pattern = rf"<{tag_name}[^>]*>(.*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + value = match.group(1).strip() + # Filter out XML comments + if value and not value.startswith("" + result = parse_xml_value(content, "library") + assert result is None + + +# ============================================================================= +# Test: parse_ui_components +# ============================================================================= + +class TestParseUIComponents: + """Tests for parse_ui_components function.""" + + def test_parse_complete_ui_components(self): + content = """ + + shadcn-ui + react + true + + """ + result = parse_ui_components(content) + assert result["library"] == "shadcn-ui" + assert result["framework"] == "react" + assert result["has_mcp"] is True + + def test_parse_ui_components_false_mcp(self): + content = """ + + radix-ui + react + false + + """ + result = parse_ui_components(content) + assert result["library"] == "radix-ui" + assert result["has_mcp"] is False + + def test_parse_ui_components_defaults(self): + content = "content" + result = parse_ui_components(content) + assert result["library"] == "none" + assert result["framework"] == "react" + assert result["has_mcp"] is False + + def test_parse_ark_ui(self): + content = """ + + ark-ui + vue + true + + """ + result = parse_ui_components(content) + assert result["library"] == "ark-ui" + assert result["framework"] == "vue" + assert result["has_mcp"] is True + + +# ============================================================================= +# Test: parse_visual_style +# ============================================================================= + +class TestParseVisualStyle: + """Tests for parse_visual_style function.""" + + def test_parse_complete_visual_style(self): + content = """ + + + .autocoder/design-tokens.json + + """ + result = parse_visual_style(content) + assert result["style"] == "neobrutalism" + assert result["design_tokens_path"] == ".autocoder/design-tokens.json" + + def test_parse_visual_style_default(self): + content = """ + + + + """ + result = parse_visual_style(content) + assert result["style"] == "default" + assert result["design_tokens_path"] == "" + + def test_parse_visual_style_missing(self): + content = "content" + result = parse_visual_style(content) + assert result["style"] == "default" + assert result["design_tokens_path"] == "" + + +# ============================================================================= +# Test: parse_ui_config +# ============================================================================= + +class TestParseUIConfig: + """Tests for parse_ui_config function.""" + + def test_parse_complete_config(self): + content = """ + + shadcn-ui + react + true + + + + .autocoder/design-tokens.json + + """ + result = parse_ui_config(content) + assert result["library"] == "shadcn-ui" + assert result["framework"] == "react" + assert result["has_mcp"] is True + assert result["style"] == "neobrutalism" + assert result["design_tokens_path"] == ".autocoder/design-tokens.json" + + def test_parse_empty_config(self): + content = "" + result = parse_ui_config(content) + assert result["library"] == "none" + assert result["framework"] == "react" + assert result["has_mcp"] is False + assert result["style"] == "default" + + +# ============================================================================= +# Test: get_ui_config_from_spec +# ============================================================================= + +class TestGetUIConfigFromSpec: + """Tests for get_ui_config_from_spec function.""" + + def test_get_config_from_existing_spec(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = Path(tmpdir) + prompts_dir = project_dir / "prompts" + prompts_dir.mkdir() + + spec_content = """ + + + ark-ui + solid + true + + + + + + """ + (prompts_dir / "app_spec.txt").write_text(spec_content) + + result = get_ui_config_from_spec(project_dir) + assert result is not None + assert result["library"] == "ark-ui" + assert result["framework"] == "solid" + assert result["has_mcp"] is True + assert result["style"] == "glassmorphism" + + def test_get_config_missing_spec(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = Path(tmpdir) + result = get_ui_config_from_spec(project_dir) + assert result is None + + +# ============================================================================= +# Test: STYLE_PRESETS +# ============================================================================= + +class TestStylePresets: + """Tests for style presets in design_tokens.py.""" + + def test_neobrutalism_preset_exists(self): + assert "neobrutalism" in STYLE_PRESETS + preset = STYLE_PRESETS["neobrutalism"] + assert preset["borders"]["width"] == "4px" + assert preset["borders"]["radius"] == "0" + assert "4px 4px 0 0" in preset["shadows"]["default"] + + def test_glassmorphism_preset_exists(self): + assert "glassmorphism" in STYLE_PRESETS + preset = STYLE_PRESETS["glassmorphism"] + assert preset["borders"]["radius"] == "16px" + assert preset["effects"]["backdropBlur"] == "12px" + + def test_retro_preset_exists(self): + assert "retro" in STYLE_PRESETS + preset = STYLE_PRESETS["retro"] + assert preset["colors"]["primary"] == "#ff00ff" + assert preset["colors"]["secondary"] == "#00ffff" + assert "Press Start 2P" in preset["typography"]["fontFamily"] + + +# ============================================================================= +# Test: get_style_preset +# ============================================================================= + +class TestGetStylePreset: + """Tests for get_style_preset function.""" + + def test_get_neobrutalism_preset(self): + preset = get_style_preset("neobrutalism") + assert preset is not None + assert "borders" in preset + assert "shadows" in preset + + def test_get_default_preset(self): + preset = get_style_preset("default") + assert preset is None + + def test_get_unknown_preset(self): + preset = get_style_preset("unknown_style") + assert preset is None + + +# ============================================================================= +# Test: generate_design_tokens +# ============================================================================= + +class TestGenerateDesignTokens: + """Tests for generate_design_tokens function.""" + + def test_generate_neobrutalism_tokens(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = Path(tmpdir) + result = generate_design_tokens(project_dir, "neobrutalism") + + assert result is not None + assert result.exists() + assert result.name == "design-tokens.json" + + tokens = json.loads(result.read_text()) + assert tokens["borders"]["width"] == "4px" + + def test_generate_default_tokens(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = Path(tmpdir) + result = generate_design_tokens(project_dir, "default") + assert result is None + + def test_generate_custom_tokens(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_dir = Path(tmpdir) + result = generate_design_tokens(project_dir, "custom") + assert result is None + + +# ============================================================================= +# Test: validate_visual_style +# ============================================================================= + +class TestValidateVisualStyle: + """Tests for validate_visual_style function.""" + + def test_valid_styles(self): + assert validate_visual_style("default") is True + assert validate_visual_style("neobrutalism") is True + assert validate_visual_style("glassmorphism") is True + assert validate_visual_style("retro") is True + assert validate_visual_style("custom") is True + + def test_invalid_styles(self): + assert validate_visual_style("unknown") is False + assert validate_visual_style("") is False + assert validate_visual_style("NEOBRUTALISM") is False # Case sensitive + + +# ============================================================================= +# Test: Constants +# ============================================================================= + +class TestConstants: + """Tests for module constants.""" + + def test_valid_ui_libraries(self): + assert "shadcn-ui" in VALID_UI_LIBRARIES + assert "ark-ui" in VALID_UI_LIBRARIES + assert "radix-ui" in VALID_UI_LIBRARIES + assert "none" in VALID_UI_LIBRARIES + + def test_mcp_supported_libraries(self): + assert "shadcn-ui" in MCP_SUPPORTED_LIBRARIES + assert "ark-ui" in MCP_SUPPORTED_LIBRARIES + assert "radix-ui" not in MCP_SUPPORTED_LIBRARIES + assert "none" not in MCP_SUPPORTED_LIBRARIES + + def test_valid_visual_styles(self): + assert "default" in VALID_VISUAL_STYLES + assert "neobrutalism" in VALID_VISUAL_STYLES + assert "glassmorphism" in VALID_VISUAL_STYLES + assert "retro" in VALID_VISUAL_STYLES + assert "custom" in VALID_VISUAL_STYLES + + def test_valid_frameworks(self): + assert "react" in VALID_FRAMEWORKS + assert "vue" in VALID_FRAMEWORKS + assert "solid" in VALID_FRAMEWORKS + assert "svelte" in VALID_FRAMEWORKS + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/ui/public/previews/README.md b/ui/public/previews/README.md new file mode 100644 index 00000000..93eac8f7 --- /dev/null +++ b/ui/public/previews/README.md @@ -0,0 +1,32 @@ +# UI Preview Images + +This directory contains preview images for UI library and visual style selectors. + +## Required Images + +### UI Libraries +- `shadcn-button.png` - shadcn/ui button component preview (400x300) +- `shadcn-card.png` - shadcn/ui card component preview (400x300) +- `shadcn-dialog.png` - shadcn/ui dialog component preview (400x300) +- `ark-accordion.png` - Ark UI accordion component preview (400x300) +- `ark-combobox.png` - Ark UI combobox component preview (400x300) +- `ark-tooltip.png` - Ark UI tooltip component preview (400x300) +- `radix-primitives.png` - Radix UI primitives preview (400x300) + +### Visual Styles +- `style-default.png` - Modern/Clean style preview (400x300) +- `style-neobrutalism.png` - Neobrutalism style preview (400x300) +- `style-glassmorphism.png` - Glassmorphism style preview (400x300) +- `style-retro.png` - Retro/Arcade style preview (400x300) + +## Image Guidelines + +- Format: PNG with transparency where appropriate +- Size: 400x300 pixels +- Show component or style clearly +- Use consistent backgrounds +- Optimize for web (compress without quality loss) + +## Fallback Behavior + +If an image is not found, the selector components will display a placeholder or icon instead. diff --git a/ui/src/components/UILibrarySelector.tsx b/ui/src/components/UILibrarySelector.tsx new file mode 100644 index 00000000..11804454 --- /dev/null +++ b/ui/src/components/UILibrarySelector.tsx @@ -0,0 +1,181 @@ +/** + * UI Library Selector Component + * + * Allows users to select a UI component library during project creation. + * Shows preview images and MCP availability badges. + */ + +import { useState } from 'react' +import { Check, Zap, Code2, Sparkles, Box } from 'lucide-react' +import { cn } from '@/lib/utils' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' + +export interface UILibrary { + id: string + name: string + description: string + frameworks: string[] + hasMcp: boolean + isRecommended?: boolean + previewImage?: string +} + +const UI_LIBRARIES: UILibrary[] = [ + { + id: 'shadcn-ui', + name: 'shadcn/ui', + description: 'Beautiful, accessible React components with Radix primitives', + frameworks: ['react'], + hasMcp: true, + isRecommended: true, + }, + { + id: 'ark-ui', + name: 'Ark UI', + description: 'Headless primitives for any framework', + frameworks: ['react', 'vue', 'solid', 'svelte'], + hasMcp: true, + }, + { + id: 'radix-ui', + name: 'Radix UI', + description: 'Low-level headless primitives', + frameworks: ['react'], + hasMcp: false, + }, + { + id: 'none', + name: 'Custom/None', + description: 'Build components from scratch', + frameworks: ['react', 'vue', 'solid', 'svelte', 'vanilla'], + hasMcp: false, + }, +] + +interface UILibrarySelectorProps { + value: string + framework: string + onChange: (libraryId: string) => void + className?: string +} + +export function UILibrarySelector({ + value, + framework, + onChange, + className, +}: UILibrarySelectorProps) { + const [hoveredId, setHoveredId] = useState(null) + + const getLibraryIcon = (id: string) => { + switch (id) { + case 'shadcn-ui': + return + case 'ark-ui': + return + case 'radix-ui': + return + case 'none': + return + default: + return + } + } + + const isCompatible = (lib: UILibrary) => { + return lib.frameworks.includes(framework) || lib.id === 'none' + } + + return ( +
+ {UI_LIBRARIES.map((lib) => { + const selected = value === lib.id + const compatible = isCompatible(lib) + // hoveredId is tracked for potential future hover preview effects + const _hovered = hoveredId === lib.id + void _hovered // Suppress unused variable warning + + return ( + compatible && onChange(lib.id)} + onMouseEnter={() => setHoveredId(lib.id)} + onMouseLeave={() => setHoveredId(null)} + role="option" + aria-selected={selected} + aria-disabled={!compatible} + aria-label={`Select ${lib.name} component library`} + tabIndex={compatible ? 0 : -1} + onKeyDown={(e) => { + if (compatible && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault() + onChange(lib.id) + } + }} + > + +
+
+ {getLibraryIcon(lib.id)} + {lib.name} +
+ {selected && ( +
+ +
+ )} +
+
+ {lib.hasMcp && ( + + + MCP Enabled + + )} + {lib.isRecommended && framework === 'react' && ( + + Recommended + + )} + {!compatible && ( + + Not compatible with {framework} + + )} +
+
+ + + {lib.description} + + {lib.frameworks.length > 0 && lib.id !== 'none' && ( +
+ {lib.frameworks.map((fw) => ( + + {fw} + + ))} +
+ )} +
+
+ ) + })} +
+ ) +} + +export { UI_LIBRARIES } diff --git a/ui/src/components/VisualStyleSelector.tsx b/ui/src/components/VisualStyleSelector.tsx new file mode 100644 index 00000000..bc54085b --- /dev/null +++ b/ui/src/components/VisualStyleSelector.tsx @@ -0,0 +1,232 @@ +/** + * Visual Style Selector Component + * + * Allows users to select a visual style during project creation. + * Shows color swatches and style previews. + */ + +import { useState } from 'react' +import { Check, Paintbrush, Sparkles, Layers, Gamepad2, Settings2 } from 'lucide-react' +import { cn } from '@/lib/utils' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' + +export interface VisualStyle { + id: string + name: string + description: string + colors: string[] + generatesTokens: boolean + previewImage?: string +} + +const VISUAL_STYLES: VisualStyle[] = [ + { + id: 'default', + name: 'Modern/Clean', + description: 'Minimal, professional design following library defaults', + colors: ['#3b82f6', '#64748b', '#ffffff'], + generatesTokens: false, + }, + { + id: 'neobrutalism', + name: 'Neobrutalism', + description: 'Bold colors, hard shadows, no border-radius', + colors: ['#ff6b6b', '#4ecdc4', '#000000'], + generatesTokens: true, + }, + { + id: 'glassmorphism', + name: 'Glassmorphism', + description: 'Frosted glass effects, blur, transparency', + colors: ['#8b5cf6', '#06b6d4', '#f472b6'], + generatesTokens: true, + }, + { + id: 'retro', + name: 'Retro/Arcade', + description: 'Pixel-art inspired, vibrant neons, 8-bit aesthetic', + colors: ['#ff00ff', '#00ffff', '#ffff00'], + generatesTokens: true, + }, + { + id: 'custom', + name: 'Custom', + description: 'Define your own design tokens', + colors: [], + generatesTokens: true, + }, +] + +interface VisualStyleSelectorProps { + value: string + onChange: (styleId: string) => void + className?: string +} + +export function VisualStyleSelector({ + value, + onChange, + className, +}: VisualStyleSelectorProps) { + const [hoveredId, setHoveredId] = useState(null) + + const getStyleIcon = (id: string) => { + switch (id) { + case 'default': + return + case 'neobrutalism': + return + case 'glassmorphism': + return + case 'retro': + return + case 'custom': + return + default: + return + } + } + + const renderColorSwatches = (colors: string[]) => { + if (colors.length === 0) return null + + return ( +
+ {colors.map((color, index) => ( +
+ ))} +
+ ) + } + + const renderStylePreview = (style: VisualStyle) => { + switch (style.id) { + case 'neobrutalism': + return ( +
+
+ Button Preview +
+
+ ) + case 'glassmorphism': + return ( +
+
+ Button Preview +
+
+ ) + case 'retro': + return ( +
+
+ Button Preview +
+
+ ) + default: + return null + } + } + + return ( +
+ {VISUAL_STYLES.map((style) => { + const selected = value === style.id + // hoveredId is tracked for potential future hover preview effects + const _hovered = hoveredId === style.id + void _hovered // Suppress unused variable warning + + return ( + onChange(style.id)} + onMouseEnter={() => setHoveredId(style.id)} + onMouseLeave={() => setHoveredId(null)} + role="option" + aria-selected={selected} + aria-label={`Select ${style.name} visual style`} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onChange(style.id) + } + }} + > + +
+
+ {getStyleIcon(style.id)} + {style.name} +
+ {selected && ( +
+ +
+ )} +
+ {style.generatesTokens && style.id !== 'custom' && ( + + Generates design tokens + + )} + {style.id === 'default' && ( + + Recommended + + )} +
+ + + {style.description} + + {renderColorSwatches(style.colors)} + {renderStylePreview(style)} + +
+ ) + })} +
+ ) +} + +export { VISUAL_STYLES } From 865d55ee0089258cb6f049626bda13c0016f7adc Mon Sep 17 00:00:00 2001 From: cabana8471 Date: Sat, 31 Jan 2026 02:29:11 +0100 Subject: [PATCH 2/3] fix: address CodeRabbit review issues for PR #141 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache ui_config in client.py to avoid 3 redundant file reads - Fix env:None in MCP config by omitting key when empty - Add integration tests for generate_design_tokens_from_spec - Fix Phase 3b → 3d reference in create-spec.md - Remove duplicate ROOT_DIR in spec_chat_session.py - Add ARIA listbox role to UILibrarySelector and VisualStyleSelector - Remove redundant isCompatible check in UILibrarySelector - Add OSError handling in design_tokens.py - Move constants to top of app_spec_parser.py - Improve XML comment filtering with regex pattern Co-Authored-By: Claude Opus 4.5 --- .claude/commands/create-spec.md | 2 +- app_spec_parser.py | 65 +++++++++---- client.py | 106 +++++++++++----------- design_tokens.py | 11 ++- test_ui_config.py | 62 +++++++++++++ ui/src/components/UILibrarySelector.tsx | 9 +- ui/src/components/VisualStyleSelector.tsx | 6 +- 7 files changed, 185 insertions(+), 76 deletions(-) diff --git a/.claude/commands/create-spec.md b/.claude/commands/create-spec.md index bea48b7e..e7a11249 100644 --- a/.claude/commands/create-spec.md +++ b/.claude/commands/create-spec.md @@ -293,7 +293,7 @@ These are just reference points - your actual count should come from the require **MANDATORY: Infrastructure Features** -If the app requires a database (Phase 3b answer was "Yes" or "Not sure"), you MUST include 5 Infrastructure features (indices 0-4): +If the app requires a database (Phase 3d answer was "Yes" or "Not sure"), you MUST include 5 Infrastructure features (indices 0-4): 1. Database connection established 2. Database schema applied correctly 3. Data persists across server restart diff --git a/app_spec_parser.py b/app_spec_parser.py index 4b9e12a7..97ba7eaf 100644 --- a/app_spec_parser.py +++ b/app_spec_parser.py @@ -4,12 +4,41 @@ Shared utilities for parsing app_spec.txt sections. Used by client.py, prompts.py, and design_tokens.py to avoid code duplication. + +This module provides functions to: +- Extract XML sections from app_spec.txt +- Parse UI component library configuration +- Parse visual style configuration +- Combine configurations for the coding agent """ import re from pathlib import Path from typing import TypedDict +# ============================================================================= +# Constants +# ============================================================================= + +# Valid UI library options +VALID_UI_LIBRARIES = {"shadcn-ui", "ark-ui", "radix-ui", "none"} + +# Libraries that have MCP server support +MCP_SUPPORTED_LIBRARIES = {"shadcn-ui", "ark-ui"} + +# Valid visual style options +VALID_VISUAL_STYLES = {"default", "neobrutalism", "glassmorphism", "retro", "custom"} + +# Valid frameworks +VALID_FRAMEWORKS = {"react", "vue", "solid", "svelte"} + +# Regex pattern to match XML comments () +XML_COMMENT_PATTERN = re.compile(r"", re.DOTALL) + +# ============================================================================= +# Type Definitions +# ============================================================================= + class UIComponentsConfig(TypedDict, total=False): """Configuration for UI component library.""" @@ -33,6 +62,11 @@ class UIConfig(TypedDict, total=False): design_tokens_path: str +# ============================================================================= +# Parsing Functions +# ============================================================================= + + def parse_section(content: str, section_name: str) -> str | None: """ Parse an XML section from app_spec.txt content. @@ -53,19 +87,29 @@ def parse_xml_value(content: str, tag_name: str) -> str | None: """ Parse a single XML value from content. + Extracts the text content from an XML tag, filtering out any XML comments. + Returns None if the tag is not found or contains only whitespace/comments. + Args: content: XML content to search tag_name: The tag name to extract value from Returns: - The value inside the tags, or None if not found. + The value inside the tags, or None if not found or empty. + + Example: + >>> parse_xml_value("shadcn-ui", "library") + 'shadcn-ui' + >>> parse_xml_value("", "library") + None """ pattern = rf"<{tag_name}[^>]*>(.*?)" match = re.search(pattern, content, re.DOTALL) if match: - value = match.group(1).strip() - # Filter out XML comments - if value and not value.startswith("