Skills / Design

build-visual

Generate a custom FSE block theme from structured HTML/CSS design exports or screenshot images - extracts design tokens, maps HTML to Gutenberg blocks, bundles Google Fonts, and produces an installable theme with custom-{slug} prefix

Source Shaisolaris/solaris-dev-shopLicense MITSafety scan Clear

No fetch-and-execute, destructive, credential, exfiltration or rules-override patterns in 1 files, checked on Oct 20, 2026. This is not a guarantee of safety. How the scan works.

npx degit Shaisolaris/solaris-dev-shop/employees/specialized/wordpress-master/diagnostics/build-visual .claude/skills/build-visual

12 alternatives · Best design skills · Author? Prove this skill

SKILL.md
# Build Visual Skill

Translates a visual design input (HTML/CSS export directory or screenshot image) into a complete, installable WordPress FSE block theme with a `custom-{slug}` naming prefix. This skill is the core engine for Phase 14's visual input mode - it reads a design, extracts tokens, scaffolds the FSE theme directory, downloads and bundles Google Fonts, activates the theme via WP-CLI, and generates a visual-specific SETUP.md.

**Critical sequencing:** This skill runs AFTER `build-mcp` Section 2 (MCP adapter activated, database re-exported) and BEFORE `build-git` Section 3 (dynamic .gitignore update adds custom-{slug} to tracked paths).

This skill expects the following variables to already be set by the calling command:

- `BUILD_DIR` - absolute path to the build directory (set by build-scaffold Section 2)
- `WP` - the WP-CLI command prefix (e.g. `wp --path=$BUILD_DIR` or the Docker equivalent, set by build-scaffold Section 4)
- `VISUAL_PATH` - path to the design input (directory or image file, set by COMMAND.md Section 1)
- `SLUG` - build slug (set by COMMAND.md Section 1)
- `SITE_TITLE` - site title (set by COMMAND.md Section 1)
- `PLUGIN_DIR` - absolute path to the CoWork plugin directory

## Section 1: Input Detection and Design Parsing

Detect whether `VISUAL_PATH` is a directory (HTML/CSS export) or an image file (screenshot), then parse the design to prepare for token extraction.

### 1a: Input Type Detection

```bash
# VISUAL_PATH set by COMMAND.md Section 1 argument parsing
THEME_SLUG="custom-${SLUG}"
THEME_DIR="$BUILD_DIR/wp-content/themes/${THEME_SLUG}"

if [ -d "$VISUAL_PATH" ]; then
  # Check if directory actually contains HTML/CSS files
  HTML_COUNT=$(find "$VISUAL_PATH" -maxdepth 3 -name "*.html" -o -name "*.css" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$HTML_COUNT" -gt 0 ]; then
    VISUAL_MODE="html-css"
    echo "[Build] Visual input: HTML/CSS export directory ($VISUAL_PATH) - $HTML_COUNT files found"
  else
    # Directory contains no HTML/CSS - check for images (Canva/Miro image-only exports)
    IMG_FILE=$(find "$VISUAL_PATH" -maxdepth 3 \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.webp" \) 2>/dev/null | sort -k5 -rn 2>/dev/null | head -1)
    if [ -n "$IMG_FILE" ]; then
      VISUAL_MODE="screenshot"
      VISUAL_PATH="$IMG_FILE"
      echo "[Build] Visual input: directory contained only images - switching to screenshot mode ($VISUAL_PATH)"
    else
      echo ""
      echo "ERROR: --visual directory contains no HTML, CSS, or image files: $VISUAL_PATH"
      echo ""
      exit 1
    fi
  fi
elif [ -f "$VISUAL_PATH" ]; then
  # Check file extension for supported image formats
  EXT=$(echo "${VISUAL_PATH##*.}" | tr '[:upper:]' '[:lower:]')
  case "$EXT" in
    png|jpg|jpeg|gif|webp|bmp|tiff)
      VISUAL_MODE="screenshot"
      echo "[Build] Visual input: Screenshot image ($VISUAL_PATH)"
      ;;
    *)
      echo ""
      echo "ERROR: --visual path must be a directory (HTML/CSS export) or an image file (png, jpg, jpeg, gif, webp, bmp, tiff)"
      echo ""
      exit 1
      ;;
  esac
else
  echo ""
  echo "ERROR: --visual path does not exist: $VISUAL_PATH"
  echo ""
  exit 1
fi

echo "[Build] VISUAL_MODE=$VISUAL_MODE, THEME_SLUG=$THEME_SLUG"
```

### 1b: HTML/CSS Path - File Inventory and Image Collection

When `VISUAL_MODE="html-css"`, collect all design files for Section 2 processing. Copy images to a staging area for later placement in `assets/images/`.

```bash
if [ "$VISUAL_MODE" = "html-css" ]; then
  # Collect CSS files for token extraction
  CSS_FILES=$(find "$VISUAL_PATH" -maxdepth 5 -name "*.css" 2>/dev/null)
  HTML_FILES=$(find "$VISUAL_PATH" -maxdepth 5 -name "*.html" 2>/dev/null)

  # Stage exported images for assets/images/ placement (Section 3g)
  IMAGES_STAGE="/tmp/visual_images_${SLUG}_$$"
  mkdir -p "$IMAGES_STAGE"
  find "$VISUAL_PATH" -maxdepth 5 \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \
    -o -name "*.gif" -o -name "*.webp" -o -name "*.svg" \) 2>/dev/null \
    | while read img; do cp "$img" "$IMAGES_STAGE/" 2>/dev/null; done

  IMG_STAGE_COUNT=$(ls "$IMAGES_STAGE" 2>/dev/null | wc -l | tr -d ' ')
  echo "[Build] Design files staged: $(echo "$CSS_FILES" | wc -l | tr -d ' ') CSS, $(echo "$HTML_FILES" | wc -l | tr -d ' ') HTML, $IMG_STAGE_COUNT images"
fi
```

### 1c: Screenshot Path - the coding agent Multimodal Vision Analysis

When `VISUAL_MODE="screenshot"`, the coding agent reads the image file using multimodal vision capability and performs design interpretation. This is an in-context the coding agent judgment step - no external tool needed.

the coding agent reads `$VISUAL_PATH` as an image and identifies:

1. **3-6 dominant/brand colors** - output as hex values (e.g. `#2D3748`)
2. **Heading font name + body font name** - or the closest Google Font matches if proprietary
3. **Approximate section boundaries** - header region, hero area, main content sections, footer region
4. **Image placeholder areas** - with approximate aspect ratios (e.g. hero 16:9, portrait 1:1, landscape 4:3)
5. **Navigation labels** - any visible menu text (Home, About, Services, Contact, etc.)

the coding agent outputs a structured JSON block:

```json
{
  "colors": ["#2D3748", "#4A5568", "#E53E3E", "#F7FAFC", "#1A202C"],
  "fonts": ["Playfair Display", "DM Sans"],
  "sections": {
    "header_height_approx": "80px",
    "hero_present": true,
    "footer_present": true
  },
  "image_areas": [
    {"name": "hero", "width": 1200, "height": 630, "ratio": "16:9"},
    {"name": "about", "width": 800, "height": 600, "ratio": "4:3"}
  ],
  "nav_labels": ["Home", "About", "Services", "Contact"]
}
```

This JSON is consumed identically to the HTML/CSS extraction output in Section 2.

Set output: `VISUAL_MODE` ("html-css" or "screenshot"), `THEME_SLUG` ("custom-${SLUG}"), `THEME_DIR` (absolute path to theme directory).

---

## Section 2: Design Token Extraction

Extract color palette, font families, and spacing values from the design. Both input paths produce identical output JSON for downstream consumption.

### 2a: HTML/CSS Path - Python CSS Parser

Run the Python CSS token extractor against all CSS files from the design export:

```bash
TOKENS_FILE="/tmp/design_tokens_${SLUG}_$$.json"

python3 << 'PYEOF'
import re, json, sys, os, glob

export_dir = os.environ.get('VISUAL_PATH', '')
tokens_file = os.environ.get('TOKENS_FILE', '/tmp/design_tokens.json')

css_files = glob.glob(os.path.join(export_dir, '**/*.css'), recursive=True)

colors = set()
fonts = set()

# Color extraction patterns
COLOR_PATTERNS = [
    r'#[0-9a-fA-F]{6}\b',                                 # 6-digit hex
    r'#[0-9a-fA-F]{3}\b',                                 # 3-digit hex
    r'rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)',              # rgb()
    r'hsl\(\s*\d+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*\)',      # hsl()
]

# Font-family extraction
FONT_PATTERN = r'font-family\s*:\s*[\'"]?([^,;\'"{}]+)[\'"]?'

# Generic families to skip
SKIP_FONTS = {'inherit', 'initial', 'unset', 'sans-serif', 'serif', 'monospace',
              'cursive', 'fantasy', 'system-ui', '-apple-system', 'blinkmacsystemfont',
              'segoe ui', 'roboto', 'helvetica neue', 'arial', 'times new roman'}

for css_file in css_files:
    with open(css_file, 'r', errors='ignore') as f:
        css = f.read()
    for pat in COLOR_PATTERNS:
        colors.update(re.findall(pat, css, re.IGNORECASE))
    for m in re.finditer(FONT_PATTERN, css, re.IGNORECASE):
        font = m.group(1).strip().strip("'\"").strip()
        if font and font.lower() not in SKIP_FONTS and len(font) > 2:
            fonts.add(font)

result = {
    'colors': list(colors)[:12],
    'fonts': list(fonts)[:4],
}
with open(tokens_file, 'w') as f:
    json.dump(result, f, indent=2)
print(f"[Build] Tokens extracted: {len(result['colors'])} colors, {len(result['fonts'])} fonts")
PYEOF
```

### 2b: Screenshot Path - Use the coding agent's Multimodal Output

When `VISUAL_MODE="screenshot"`, the token JSON was already produced in Section 1c. Write it to `$TOKENS_FILE` from the coding agent's structured output.

### 2c: Font Mapping and Google Font Resolution

the coding agent reads the extracted `fonts` array from `$TOKENS_FILE` and determines:

- **Google Font match**: If the font name is a known Google Font (e.g. `Playfair Display`, `DM Sans`, `Inter`, `Roboto`), use directly.
- **Proprietary substitution**: If the font is proprietary (Adobe Fonts, paid typefaces) or unrecognisable, find the closest Google Font match by visual characteristics (serif/sans-serif, weight, x-height, personality).

the coding agent sets `FONT_MAP` as a JSON mapping of original → Google Font slug:

```bash
# Example FONT_MAP (the coding agent sets based on extraction)
FONT_MAP='{
  "Playfair Display": "playfair-display",
  "DM Sans": "dm-sans"
}'

# Track substitutions for SETUP.md
# Format: "Original Name → Google Font Substitute"
FONT_SUBSTITUTIONS=()
# the coding agent appends entries if a proprietary font was substituted:
# FONT_SUBSTITUTIONS+=("Aktiv Grotesk → Inter")
```

### 2d: Color Palette Construction

the coding agent reads the extracted colors and assigns semantic slugs:

```
primary   → dominant brand color (most prominent, used for CTA/headings)
secondary → supporting brand color (complementary to primary)
accent    → action/highlight color (buttons, links, hover states)
light     → background or near-white color
dark      → text or near-black color
custom-1 through custom-7 → additional extracted colors (if any)
```

Convert spacing values from px to rem (divide by 16). Limit palette to 12 colors maximum.

Set output: `EXTRACTED_COLORS` (array of `{slug, color, name}` objects), `EXTRACTED_FONTS` (array of font family names), `FONT_MAP` (mapping JSON), `FONT_SUBSTITUTIONS` (array of substitution strings).

---

## Section 3: Theme Scaffolding (Write All Files)

Create the complete FSE theme directory structure and write all theme files based on extracted tokens and design interpretation.

### 3a: Directory Structure Creation

```bash
echo "[Build] Creating FSE theme directory: $THEME_DIR"
mkdir -p "$THEME_DIR/assets/fonts"
mkdir -p "$THEME_DIR/assets/images"
mkdir -p "$THEME_DIR/templates"
mkdir -p "$THEME_DIR/parts"
mkdir -p "$THEME_DIR/patterns"
```

Expected final structure:
```
$BUILD_DIR/wp-content/themes/custom-${SLUG}/
  style.css
  theme.json
  functions.php
  assets/
    fonts/          (populated in Section 4)
    images/         (exported images or generated placeholders)
  templates/
    index.html
    front-page.html
    single.html
    page.html
    archive.html
    search.html
    404.html
  parts/
    header.html
    footer.html
  patterns/
    front-page.php
```

### 3b: style.css - WordPress Theme Header

Write `$THEME_DIR/style.css` with the WordPress theme file header and responsive CSS:

```css
/*
Theme Name:        Custom {Slug}
Theme URI:
Author:            WP CoWork Builder
Author URI:
Description:       Custom FSE block theme generated from design export. Built with WP CoWork Plugin.
Version:           1.0.0
Requires at least: 6.6
Tested up to:      6.7
Requires PHP:      8.0
License:           GNU General Public License v2 or later
License URI:       http://www.gnu.org/licenses/gpl-2.0.html
Text Domain:       custom-{slug}
*/

/* Responsive inference - stacks desktop columns for mobile */
@media (max-width: 768px) {
  .wp-block-columns {
    flex-direction: column;
  }
  .wp-block-column {
    width: 100% !important;
  }
  /* Scale display headings */
  h1.wp-block-heading { font-size: clamp(1.75rem, 5vw, 2.5rem); }
  h2.wp-block-heading { font-size: clamp(1.5rem, 4vw, 2rem); }
  h3.wp-block-heading { font-size: clamp(1.25rem, 3vw, 1.75rem); }
}

/* Required: prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

Replace `{Slug}` and `{slug}` with the actual slug values (e.g. `Custom Figma Portfolio`, `custom-figma-portfolio`).

### 3c: theme.json - Design Token Encoding

Write `$THEME_DIR/theme.json` using the v3 schema. Populate `settings.color.palette` with `EXTRACTED_COLORS`, populate `settings.typography.fontFamilies` with `EXTRACTED_FONTS` (using `file:./assets/fonts/` src paths - actual filenames updated in Section 4 after download).

```json
{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "layout": {
      "contentSize": "800px",
      "wideSize": "1280px"
    },
    "color": {
      "palette": [
        { "slug": "primary",   "color": "#2D3748", "name": "Primary"   },
        { "slug": "secondary", "color": "#4A5568", "name": "Secondary" },
        { "slug": "accent",    "color": "#E53E3E", "name": "Accent"    },
        { "slug": "light",     "color": "#F7FAFC", "name": "Light"     },
        { "slug": "dark",      "color": "#1A202C", "name": "Dark"      }
      ],
      "defaultPalette": false,
      "defaultGradients": false
    },
    "typography": {
      "fontFamilies": [
        {
          "name": "Heading Font",
          "slug": "heading",
          "fontFamily": "Playfair Display, Georgia, serif",
          "fontFace": [
            {
              "fontFamily": "Playfair Display",
              "fontWeight": "400",
              "fontStyle": "normal",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/playfair-display-v36-latin-regular.woff2"]
            },
            {
              "fontFamily": "Playfair Display",
              "fontWeight": "700",
              "fontStyle": "normal",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/playfair-display-v36-latin-700.woff2"]
            }
          ]
        },
        {
          "name": "Body Font",
          "slug": "body",
          "fontFamily": "DM Sans, system-ui, sans-serif",
          "fontFace": [
            {
              "fontFamily": "DM Sans",
              "fontWeight": "400",
              "fontStyle": "normal",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/dm-sans-v15-latin-regular.woff2"]
            },
            {
              "fontFamily": "DM Sans",
              "fontWeight": "700",
              "fontStyle": "normal",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/dm-sans-v15-latin-700.woff2"]
            }
          ]
        }
      ],
      "fontSizes": [
        { "slug": "small",  "size": "0.875rem",                 "name": "Small"   },
        { "slug": "medium", "size": "1rem",                     "name": "Medium"  },
        { "slug": "large",  "size": "1.25rem",                  "name": "Large"   },
        { "slug": "xlarge", "size": "1.75rem",                  "name": "X-Large" },
        { "slug": "xxlarge","size": "2.25rem",                  "name": "2X-Large"},
        { "slug": "huge",   "size": "clamp(2.5rem, 4vw, 3.5rem)", "name": "Huge" }
      ]
    },
    "spacing": {
      "units": ["px", "em", "rem", "%"],
      "spacingSizes": [
        { "slug": "xs",  "size": "0.5rem", "name": "XS"  },
        { "slug": "sm",  "size": "1rem",   "name": "SM"  },
        { "slug": "md",  "size": "2rem",   "name": "MD"  },
        { "slug": "lg",  "size": "3rem",   "name": "LG"  },
        { "slug": "xl",  "size": "5rem",   "name": "XL"  },
        { "slug": "xxl", "size": "8rem",   "name": "XXL" }
      ]
    }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--light)",
      "text": "var(--wp--preset--color--dark)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--body)",
      "fontSize": "var(--wp--preset--font-size--medium)",
      "lineHeight": "1.6"
    },
    "elements": {
      "heading": {
        "typography": {
          "fontFamily": "var(--wp--preset--font-family--heading)",
          "lineHeight": "1.2"
        }
      },
      "link": {
        "color": { "text": "var(--wp--preset--color--accent)" }
      },
      "button": {
        "color": {
          "background": "var(--wp--preset--color--accent)",
          "text": "var(--wp--preset--color--light)"
        },
        "border": { "radius": "4px" }
      }
    }
  },
  "templateParts": [
    { "slug": "header", "title": "Header", "area": "header" },
    { "slug": "footer", "title": "Footer", "area": "footer" }
  ]
}
```

the coding agent replaces the example color and font values above with the actual `EXTRACTED_COLORS` and `EXTRACTED_FONTS` values from Section 2. Spacing values from the design CSS are converted from px to rem (divide by 16) before encoding.

**CRITICAL: Validate JSON immediately after writing:**

```bash
python3 -c "import json; json.load(open('$THEME_DIR/theme.json'))" \
  && echo "[Build] theme.json validated: OK" \
  || echo "[Build] WARNING: theme.json JSON validation failed - check for syntax errors"
```

### 3d: functions.php - Pattern Registration

Write `$THEME_DIR/functions.php` with minimal PHP: register block pattern category for the theme's custom patterns. No font CDN enqueuing - fonts are bundled via `theme.json fontFace`, no external CDN requests.

```php
<?php
/**
 * Custom {Slug} Theme Functions
 * Theme: custom-{slug}
 * Generated by WP CoWork Plugin (visual build)
 */

if ( ! function_exists( 'custom_{slug}_setup' ) ) {
    function custom_{slug}_setup() {
        // Register theme's block pattern category
        if ( function_exists( 'register_block_pattern_category' ) ) {
            register_block_pattern_category(
                'custom-{slug}',
                array( 'label' => __( 'Custom {Slug} Patterns', 'custom-{slug}' ) )
            );
        }
    }
}
add_action( 'after_setup_theme', 'custom_{slug}_setup' );
```

Replace `{slug}` and `{Slug}` with the actual slug values throughout (e.g. `custom_figma_portfolio_setup`, `Figma Portfolio`).

### 3d-ii: patterns/ - Front-Page Starter Pattern

Write `$THEME_DIR/patterns/front-page.php`. This is the primary design translation as a reusable block pattern. the coding agent interprets the full design and outputs the complete front-page layout using WordPress block comment syntax.

```php
<?php
/**
 * Title: Front Page
 * Slug: custom-{slug}/front-page
 * Categories: custom-{slug}
 * Block Types: core/post-content
 */
?>
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">

<!-- wp:cover {"url":"","dimRatio":40,"align":"full","style":{"color":{"duotone":"unset"}}} -->
<div class="wp-block-cover alignfull"><span aria-hidden="true" class="wp-block-cover__background has-background-dim-40 has-background-dim"></span>
<div class="wp-block-cover__inner-container">
<!-- wp:heading {"level":1,"textAlign":"center","style":{"typography":{"fontFamily":"var(--wp--preset--font-family--heading)"}}} -->
<h1 class="wp-block-heading has-text-align-center">Welcome to {Site Title}</h1>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Your site tagline goes here - describe what makes this site special.</p>
<!-- /wp:paragraph -->

<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons">
<!-- wp:button {"backgroundColor":"accent","textColor":"light"} -->
<div class="wp-block-button"><a class="wp-block-button__link has-accent-background-color has-light-color has-background has-text-color wp-element-button">Get Started</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
</div>
<!-- /wp:cover -->

</div>
<!-- /wp:group -->
```

the coding agent replaces the hero placeholder with the actual design interpretation. Additional pattern sections (services, about, testimonials, CTA) are added based on what the coding agent identifies in the design.

### 3e: templates/ - 7 WordPress Block Templates

the coding agent interprets the design and generates 7 HTML template fi

Shown under the source repo’s MIT license. Read the full folder on GitHub.