Skill: wangjipeng977/ai-image-to-code (version 2.0.3)

# AI Image To Code **Emoji:** 🖼️→💻 **Trigger:** User pastes a UI screenshot/image and wants code (HTML/CSS or React). ## What It Does Converts UI screenshots into working HTML/CSS or React + Tailwind components. Analyzes the layout structure, color palette, typography hierarchy, and spacing to produce faithful code reconstruction. ## Features - Vision-powered layout extraction (header, sidebar, main content, etc.) - Multi-format output: plain HTML/CSS (default) or React + Tailwind CSS - Mobile-first responsive (detects mobile screenshots → max-width: 375px) - Contextual placeholder content (e.g., "Price: $49.99" not lorem ipsum) ## Modes | Mode | Description | |------|-------------| | `/ai-image-to-code` | Convert UI image to HTML/CSS | | `/ai-image-to-code/react` | Output React functional component + Tailwind | | `/ai-image-to-code/describe` | Text description of layout, no code | ## How To Use ``` /ai-image-to-code ``` Paste a screenshot, ask to generate HTML/CSS. ``` /ai-image-to-code/react ``` Asks for React + Tailwind output instead. ``` /ai-image-to-code/describe ``` Just describe the layout, no code generation. ## Technical Notes - Uses MiniMax vision model to analyze screenshot - Detects dark mode and applies appropriate color schemes - Generates semantic HTML structure - Tailwind classes mapped from visual analysis --- *Last updated: 2026-05-28*

path: CHANGELOG.md
content: # Changelog
## [1.0.1] - 2026-05-18

### Minor update

- **Previous:** 1.0
- **Changed:** Updated skill content and quality



## [1.0] - 2026-05-18

### Added

- Initial release


path: CONTRIBUTING.md
content: # Contributing to `User Provides Screenshot`

Thank you for your interest in contributing! Please follow these steps to set up your development environment and submit changes.

## Development Setup

```bash
# 1. Fork the repository on GitHub

# 2. Clone your fork
git clone https://github.com/<your-username>/User Provides Screenshot.git
cd User Provides Screenshot

# 3. Install dependencies (if any)
pip install -r scripts/requirements.txt

# 4. Run the self-audit to verify quality
python scripts/audit_skill.py .
```

## Workflow

We use a standard feature branch workflow:

```bash
# 1. Create a new branch from main
git checkout -b feat/<your-feature-name>

# 2. Make your changes
#    - Follow the SKILL.md structure standards
#    - Keep SKILL.md body in English
#    - Do not hardcode API keys or secrets

# 3. Run the audit to check for issues
python scripts/audit_skill.py .

# 4. Commit your changes
git add .
git commit -m "feat(ai-image-to-code): add <brief description>"

# 5. Push to your fork
git push origin feat/<your-feature-name>

# 6. Open a Pull Request on GitHub
#    - Title: feat(ai-image-to-code): add <brief description>
#    - Description: What + Why + Testing
```

## Code Standards

- **SKILL.md**: Follow the YAML frontmatter standard (name, description, license, metadata)
- **Scripts**: Must include shebang, requirements.txt, and graceful error handling
- **Language**: SKILL.md body must be in English; reference docs in English
- **No secrets**: Never commit API keys, tokens, or credentials

## Quality Checklist

Before opening a PR, verify:

- [ ] `audit_skill.py` exits with code 0 or 2
- [ ] `validate_skills.py` exits with code 0
- [ ] README.md and README_zh.md are both present
- [ ] CONTRIBUTING.md is present
- [ ] .gitignore is present
- [ ] No hardcoded secrets anywhere in the codebase

## Reporting Issues

Please report issues via GitHub Issues with:

1. **What you expected to happen**
2. **What actually happened**
3. **Steps to reproduce**
4. **Environment** (OS, Python version, etc.)

## License

By contributing, you agree that your contributions will be licensed under the MIT License.

path: README.md
content: # AI Image To Code

[中文版](./README_zh.md)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.0-blue)](SKILL.md)

> Converts UI screenshots into HTML/CSS or React components — rebuilds designs as working code

## What Problem This Solves

User has a design mockup or screenshot and needs it as actual code they can run — not a description, not a Figma export, but working HTML/CSS or a React component that looks like the original.

**When triggered:** UI screenshot/image + code/generate/rebuild intent.

## Features

- **Vision-powered layout extraction** — analyzes screenshot for structure (header, sidebar, main content), color palette, typography hierarchy, and spacing
- **Multi-format output** — plain HTML/CSS (default) or React + Tailwind CSS (for component requests)
- **Mobile-first responsive** — detects mobile screenshots and outputs `max-width: 375px` containers
- **Placeholder content** — uses contextually appropriate text ("Price: $49.99" not generic lorem ipsum)

## Quick Start

```bash
# Via ClawHub
clawhub install ai-image-to-code

# Or manually
cp -r ai-image-to-code ~/.openclaw/skills/
```

### Usage

```
/ai-image-to-code
```

Paste screenshot, ask to generate HTML/CSS.

```
/ai-image-to-code/react
```

Asks for React + Tailwind output instead of plain HTML.

```
/ai-image-to-code/describe
```

Just want a text description of the layout first — no code generation.

## Modes

| Mode | Description |
|------|-------------|
| `/ai-image-to-code` | Converts UI image to HTML/CSS |
| `/ai-image-to-code/react` | Outputs React functional component with Tailwind |
| `/ai-image-to-code/describe` | Text description of layout, no code |

## Examples

| Input | Output |
|-------|--------|
| E-commerce product card | "Price: $49.99 — Add to Cart" contextually appropriate |
| Dark mode UI screenshot | Dark background, light text, correct contrast applied |
| Mobile app screen | `max-width: 375px` container, mobile-first |
| Complex dashboard | Grid layout with sidebar, header, main panels |

## Directory Structure

```
ai-image-to-code/
├── SKILL.md
├── LICENSE
├── README.md
├── README_zh.md
├── CONTRIBUTING.md
├── .gitignore
├── references/       # Color extraction, layout patterns, Tailwind mapping
└── tests/
```

## License

MIT License — see [LICENSE](LICENSE).

path: README_zh.md
content: # AI Image To Code

[English](./README.md)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![版本](https://img.shields.io/badge/version-1.0-blue)

> 将 UI 截图转换为 HTML/CSS 或 React 组件 — 以工作代码重建设计

## 解决什么问题

用户有设计模型或截图，需要将其转换为可以实际运行的代码——不是描述，不是 Figma 导出，而是看起来像原始设计的工作 HTML/CSS 或 React 组件。

**触发条件：** UI 截图/图片 + 代码/生成/重建意图。

## 功能特性

- **视觉驱动布局提取** — 分析截图的结构（头部、侧边栏、主内容）、配色、字体层次和间距
- **多格式输出** — 纯 HTML/CSS（默认）或 React + Tailwind CSS（用于组件请求）
- **移动端优先响应式** — 检测移动端截图并输出 `max-width: 375px` 容器
- **适配的占位符内容** — 使用上下文适当文本（"价格：￥49.99"而非通用占位符）

## 快速开始

```bash
# 通过 ClawHub 安装
clawhub install ai-image-to-code

# 或手动复制
cp -r ai-image-to-code ~/.openclaw/skills/
```

### 使用方法

```
/ai-image-to-code
```

粘贴截图，要求生成 HTML/CSS。

```
/ai-image-to-code/react
```

要求 React + Tailwind 输出，而不是纯 HTML。

```
/ai-image-to-code/describe
```

只想先获取布局的文本描述——不生成代码。

## 工作模式

| 模式 | 说明 |
|------|------|
| `/ai-image-to-code` | 将 UI 图片转换为 HTML/CSS |
| `/ai-image-to-code/react` | 输出 React 函数组件 + Tailwind |
| `/ai-image-to-code/describe` | 布局的文本描述，无代码 |

## 示例

| 输入 | 输出 |
|------|------|
| 电商产品卡片 | "价格：￥49.99 — 加入购物车" 上下文适当 |
| 深色模式 UI 截图 | 应用深色背景、浅色文字、正确对比度 |
| 移动端 App 截图 | `max-width: 375px` 容器，移动端优先 |
| 复杂仪表盘 | 网格布局：侧边栏、头部、主面板 |

## 目录结构

```
ai-image-to-code/
├── SKILL.md
├── LICENSE
├── README.md
├── README_zh.md
├── CONTRIBUTING.md
├── .gitignore
├── references/       # 颜色提取、布局模式、Tailwind 映射
└── tests/
```

## 许可证

MIT 许可证 — 详见 [LICENSE](LICENSE)。

path: references/index.md
content: # ai-image-to-code — References

Detailed documents for `user-provides-screenshot` skill.

TODO: Add reference files here as needed.


path: tests/test_skill.py
content: #!/usr/bin/env python3
"""
Auto-generated test suite for skill: user-provides-screenshot
Run with: python tests/test_skill.py
"""

import sys, os, re, yaml

def _p(name, passed, msg=''):
    emoji = "\u2705" if passed else "\u274c"
    result = "PASS" if passed else "FAIL"
    print(f"  [{emoji}] {result} -- {name}{msg}")
    return passed

def _skill_md():
    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'SKILL.md')

def _skill_dir():
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def _frontmatter():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    end = text.find('\n---', 3)
    if end == -1:
        return None
    try:
        return yaml.safe_load(text[3:end]) or {}
    except Exception:
        return None

def test_frontmatter_delimiters():
        with open(_skill_md(), "r", encoding="utf-8", errors="ignore") as f:
            text = f.read()
        return _p("frontmatter_delimiters", text.startswith("---"))

def test_frontmatter_name():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_name', False, ' -- no frontmatter')
        name = fm.get('name', '').strip()
        skill_dir = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
        return _p('frontmatter_name', name == skill_dir,
                 f" (got '{name}', expected '{skill_dir}')")

def test_frontmatter_name_is_kebab_case():
        fm = _frontmatter()
        if fm is None:
            return _p('name_is_kebab_case', False)
        name = fm.get('name', '')
        is_kebab = bool(re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name))
        return _p('name_is_kebab_case', is_kebab, f" (got '{name}')")

def test_frontmatter_description():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_description', False)
        return _p('frontmatter_description', bool(fm.get('description', '').strip()))

def test_description_has_trigger_conditions():
        fm = _frontmatter()
        if fm is None:
            return _p('description_is_trigger', False)
        desc = fm.get('description', '')
        has_trigger = ('use when' in desc.lower() or
                      re.search(r'\([0-9]+\)', desc) or
                      ('when the user' in desc.lower()))
        return _p('description_is_trigger', has_trigger,
                  ' (must be trigger condition, not capability statement)')

def test_description_length():
        fm = _frontmatter()
        if fm is None:
            return _p('description_length', False)
        desc = fm.get('description', '')
        length = len(desc.strip())
        ok = 50 <= length <= 500
        hint = 'optimal' if ok else f'{length} chars'
        return _p('description_length', ok, f' ({hint} -- optimal: 50-500)')

def test_frontmatter_license():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_license', False)
        lic = fm.get('license', '').strip().upper()
        return _p('frontmatter_license', lic == 'MIT' or 'MIT' in lic)

def test_frontmatter_metadata():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_metadata', False)
        meta = fm.get('metadata', {}) or {}
        has_version = bool(str(meta.get('version', '')).strip())
        has_category = bool(str(meta.get('category', '')).strip())
        return _p('frontmatter_metadata', has_version and has_category,
                  f' (version={has_version}, category={has_category})')

def test_has_modes():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_modes', '## Modes' in text or '## Mode' in text or '## Core Position' in text)

def test_modes_are_distinct():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        mode_blocks = re.findall(r'^#{1,3} [^#\n].+$', text, re.MULTILINE)
        distinct = len(mode_blocks)
        return _p('modes_distinct', distinct >= 2,
                  f' ({distinct} distinct sections -- need 2+ for multi-mode)')

def test_has_do_not():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_do_not', 'Do not' in text or 'do-not' in text or 'Must not' in text)

def test_do_not_section_has_content():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        do_not_match = re.search(r'(?i)##?\s*(do not|mandatory rules)[:\n](.+?)(?=##|\Z)', text, re.DOTALL)
        if not do_not_match:
            return _p('do_not_has_rules', False, ' (no Do not section found)')
        do_not_text = do_not_match.group(1)
        rules = re.findall(r'^\s*[-*]\s+\w', do_not_text, re.MULTILINE)
        return _p('do_not_has_rules', len(rules) >= 2,
                  f' ({len(rules)} rules -- need at least 2)')

def test_execution_steps_are_numbered():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        has_numbered = bool(re.search(r'(?:^|\n)\d+\.?\s+\w', text, re.MULTILINE))
        return _p('steps_numbered', has_numbered,
                  ' (execution steps must be numbered, not bullets or prose)')

def test_has_quality_bar():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_quality_bar', 'Quality Bar' in text or 'quality bar' in text)

def test_quality_bar_has_criteria():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        qb_match = re.search(r'(?i)##?\s*quality bar[:\n](.+?)(?=##|\Z)', text, re.DOTALL)
        if not qb_match:
            return _p('quality_bar_has_criteria', False, ' (no Quality Bar section)')
        qb_text = qb_match.group(1).lower()
        concrete_markers = ['must have', 'must be', 'must not', 'a good output', 'a bad output',
                            'is present', 'is valid', 'returns', 'provides', 'has a']
        has_concrete = any(m in qb_text for m in concrete_markers)
        return _p('quality_bar_has_criteria', has_concrete,
                  ' (Quality Bar must have concrete observable criteria)')

def test_has_good_bad_examples():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_good_bad_examples', 'Good' in text and 'Bad' in text)

def test_no_secrets():
        SECRET_PATTERNS = [
            r'sk-[A-Za-z0-9]{20,}',
            r'sk-[A-Za-z0-9][A-Za-z0-9-]{19,}',
            r'AKIA[A-Z0-9]{16}',
            r'ghp_[A-Za-z0-9]{36}',
            r"(?i)api[_-]?key\s*[=:]\s*['\"]?[A-Za-z0-9_-]{20,}",
        ]
        skill_path = os.path.dirname(os.path.abspath(__file__))
        passed = True
        for root, dirs, files in os.walk(skill_path):
            dirs[:] = [d for d in dirs if not d.startswith('.') and 'installed_skills' not in d]
            for fname in files:
                if fname.startswith('.'):
                    continue
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                except Exception:
                    continue
                for pat in SECRET_PATTERNS:
                    if re.search(pat, content):
                        rel = os.path.relpath(fpath, skill_path)
                        print(f'  \u274c FAIL -- secret found: {rel}')
                        passed = False
                        break
        return _p('no_secrets', passed)

def test_readme_zh_exists():
        return _p('readme_zh_exists', os.path.isfile(os.path.join(_skill_dir(), 'README_zh.md')))

def test_contributing_exists():
        return _p('contributing_exists', os.path.isfile(os.path.join(_skill_dir(), 'CONTRIBUTING.md')))

def test_gitignore_exists():
        return _p('gitignore_exists', os.path.isfile(os.path.join(_skill_dir(), '.gitignore')))

def test_tests_dir_not_empty():
        tests_dir = os.path.join(_skill_dir(), 'tests')
        if not os.path.isdir(tests_dir):
            return _p('tests_dir_not_empty', False, ' (tests/ dir missing)')
        has_tests = any(f.startswith('test_') and f.endswith('.py')
                        for f in os.listdir(tests_dir) if not f.startswith('.'))
        return _p('tests_dir_not_empty', has_tests)

def test_license_badge_in_readme():
        readme = os.path.join(_skill_dir(), 'README.md')
        if not os.path.isfile(readme):
            return _p('license_badge_in_readme', False, ' (README.md missing)')
        with open(readme, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read().lower()
        has_badge = 'license' in content and ('mit' in content or 'apache' in content or 'gpl' in content)
        return _p('license_badge_in_readme', has_badge)

def test_scripts_shebangs():
        scripts_dir = os.path.join(_skill_dir(), 'scripts')
        if not os.path.isdir(scripts_dir):
            return _p('scripts_shebangs_skipped', True, ' (no scripts/ dir)')
        passed = True
        for fname in os.listdir(scripts_dir):
            if fname.startswith('.'):
                continue
            if fname.endswith(('.py', '.sh')):
                fpath = os.path.join(scripts_dir, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        first = f.read(10)
                    if not first.startswith('#!'):
                        print(f'  \u274c FAIL -- missing shebang: scripts/{fname}')
                        passed = False
                except Exception:
                    pass
        return _p('scripts_have_shebangs', passed)

def test_skill_md_size():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
        line_count = content.count('\n') + 1
        if line_count <= 400:
            hint = '\u2705 good'
        elif line_count <= 800:
            hint = '\u26a0\ufe0f consider extracting to references/'
        else:
            hint = '\u274c too large'
        return _p('skill_md_size', line_count <= 800,
                  f' ({line_count} lines -- {hint})')

def _main():
    print()
    print("=" * 60)
    print(f"  🧪 TEST SUITE -- skill-factory")
    print("=" * 60)
    print()
    tests = [
        test_frontmatter_delimiters,
        test_frontmatter_name,
        test_frontmatter_name_is_kebab_case,
        test_frontmatter_description,
        test_description_has_trigger_conditions,
        test_description_length,
        test_frontmatter_license,
        test_frontmatter_metadata,
        test_has_modes,
        test_modes_are_distinct,
        test_has_do_not,
        test_do_not_section_has_content,
        test_execution_steps_are_numbered,
        test_has_quality_bar,
        test_quality_bar_has_criteria,
        test_has_good_bad_examples,
        test_no_secrets,
        test_readme_zh_exists,
        test_contributing_exists,
        test_gitignore_exists,
        test_tests_dir_not_empty,
        test_license_badge_in_readme,
        test_scripts_shebangs,
        test_skill_md_size,
    ]
    results = []
    for t in tests:
        try:
            results.append(t())
        except Exception as e:
            print(f"  \u274c FAIL -- {t.__name__} raised {e}")
            results.append(False)
    passed = sum(results)
    total = len(results)
    print()
    print("=" * 60)
    print(f"  \U0001f4ca RESULTS: {passed}/{total} passed")
    print("=" * 60)
    if passed == total:
        print("  \u2705 ALL TESTS PASSED")
    else:
        print(f'  \u274c {total - passed} test(s) FAILED -- fix before submission')
    print()
    return 0 if passed == total else 1

if __name__ == '__main__':
    sys.exit(_main())


def _main():
    _run_unit_tests()
    return 0


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (MEDIUM)