Skip to content
Back to skills

Designmatch

ASecurity

Use when matching a React Native screen to a Claude Design HTML canonical at ≥95% visual parity on real Android/iOS hardware — closes the loop with a VQA stub mode, Playwright canonical renderer at 1080×2444, and a dual-metric pixelmatch + SSIM diff harness producing composite triptychs.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added June 13, 2026
ai-agentsgobashreactnodegit

Works with

  • cli

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned June 13, 2026

npx -y skills add randomittin/heimdall --skill designmatch --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Designmatch?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Designmatch
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/randomittin-designmatch/badge)](https://www.skillsdirectory.com/skills/randomittin-designmatch)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: designmatch
description: Use when matching a React Native screen to a Claude Design HTML canonical at ≥95% visual parity on real Android/iOS hardware — closes the loop with a VQA stub mode, Playwright canonical renderer at 1080×2444, and a dual-metric pixelmatch + SSIM diff harness producing composite triptychs.
---

# designmatch

## Overview

Match RN screen → HTML canonical at ≥95% visual parity. Closed loop: VQA stub seeds redux → Playwright renders canonical PNG → adb/xcrun captures native PNG → pixelmatch + ssim.js scores diff → composite triptych for eyeball review → iterate.

Origin: a production React Native app. A single screen's SSIM climbed 35% → 55% over ~30 commits of eyeballed pixel-tweaking before this loop replaced it.

Pass gate: **SSIM ≥ 0.95 OR pixelDiffPct ≤ 5%**.

## When to Use

- RN screen drift from Claude Design HTML canonical
- Visual parity audit before ship
- Per-screen iteration loop on real device
- Closing the design ↔ code feedback gap

Do NOT use for: unit logic, redux state shape, navigation graph correctness — those are not visual.

## Quick Start — URL in, ready-to-translate out

From the RN project root:

```bash
designmatch init "<claude-design-url>" --app-dir . --port-all
```

This single command does the full bootstrap:

1. **App side**: copies `assets/visual-qa.ts` into the app (`src/lib/visual-qa.ts` by default; auto-detects `src/utils/`, `src/`, `app/`, or root), writes default `.designmatch/state.vqa.json`, updates `.gitignore`.
2. **Fetch**: Playwright headless downloads the entire canonical bundle (HTML + JSX + assets) to `.designmatch/canonical/` by intercepting every network response, then flips `config.json` kind from `url` → `local-dir` (original URL preserved for re-fetch).
3. **Port-all**: discovers every `screen-*.jsx|tsx` / `*Screen.jsx|tsx` in the bundle and writes each to `src/screens/<Name>.tsx` preceded by the TRANSLATION GUIDE (web → RN idiom map).
4. **Wire**: prints the `App.tsx` snippet (primeVisualQaFlag / applyVisualQaState / overrideFeatureFlags / VqaBadge / long-press handler).

Slash command equivalent (inside a Heimdall session): `/hmd:designmatch <url-or-path>`.

Auth: if the canonical URL is behind login, add `--headed` so Chromium launches visibly for interactive auth — the fetch picks up after sign-in.

Granular subcommands (when you want pieces, not the one-shot):

```bash
designmatch wire --app-dir .                       # app side only (no canonical)
designmatch fetch --app-dir . [--headed]           # download URL bundle
designmatch port <ScreenName> --out src/screens/<ScreenName>.tsx
designmatch port-all --out-dir src/screens         # port every screen found
designmatch action-types                           # print ACTION_TYPES starter
designmatch iterate Home --platform android --device emulator-5554
```

Peer deps the app must have: `@react-native-async-storage/async-storage`, `react-native-restart`. Dev deps for the harness: `playwright pixelmatch pngjs ssim.js sharp`.

## Build Path (port-first, mandatory)

**Methodology: port the canonical source, do NOT eyeball pixels.** The HTML/JSX in the canonical bundle is the spec; the PNG is the verification gate. Eyeballing pixels re-derives layout / spacing / colors that already exist in the source — drift, token bloat, the multi-commit grind documented in the anti-patterns reference.

Per-screen flow:

1. **Port** — `designmatch port <ScreenName> --out src/screens/<ScreenName>.tsx`
   Emits the canonical JSX preceded by a TRANSLATION GUIDE (web → RN idiom map). Optional `--guide-only` prints just the guide; `--no-guide` skips it.
2. **Translate** — apply the guide top-down:
   - `<div>` → `<View>`; `<span>` / `<p>` / `<h*>` → `<Text>`; `<img>` → `<Image>`; `<button>` → `<Pressable>`
   - `className` / Tailwind → `StyleSheet.create()`
   - All px literals → `normalize(n)`
   - `fontWeight` on bold-family text → `Platform.OS` gate (anti-pattern #2)
   - `<svg>` → `react-native-svg` primitives
   - Keep variable names + structure identical to canonical.
3. **Verify** — `designmatch iterate <ScreenName> --platform android --device <id>`
   Renders canonical, captures device, diffs, opens composite. Pass when **SSIM ≥ 0.95 OR pixel-diff ≤ 5%**.
4. **Refine** — only adjust translation deltas the diff surfaces. **Do NOT freelance pixel adjustments.**

If the canonical is registered as a URL (not yet downloaded), `designmatch port` and `designmatch port-all` auto-run `designmatch fetch` first — transparent. Add `--headed` if the URL is behind login. To skip auto-fetch on init, pass `--no-fetch`.

**Why mandatory:** rebuilding by eyeballing PNGs is anti-pattern #9. PNGs are the gate, not the build input.

## Architecture

```
canonical HTML ──Playwright──► canonical.png ─┐
                                              ├──diff──► metrics.json + composite.png ──► iterate
device (adb/xcrun) ──capture──► native.png ───┘
```

Canonical viewport locked to **1080×2444**. Orientation locked. State seeded via `window.__VQA_STATE__` before bundle eval.

## Layout

```
skills/designmatch/
├── SKILL.md
├── scripts/
│   ├── render-canonical.js      # Playwright renderer
│   ├── visual-diff.js           # pixelmatch + ssim.js + composite
│   └── iterate-screen.sh        # per-screen loop
├── assets/
│   └── visual-qa.ts             # RN VQA stub helper (drop-in)
└── references/
    ├── anti-patterns.md         # 9-item checklist (incl. port-first rule)
    └── canonical-values.md      # typography + spacing cheat-sheet
```

## VQA Stub Mode (assets/visual-qa.ts)

Trigger: **5× long-press AppLogo within 4s** → flip AsyncStorage `dm_visual_qa` → `RNRestart.restart()`.

Boot path: `primeVisualQaFlag()` reads AsyncStorage → `applyVisualQaState(dispatch)` seeds:

- **user**: `{ onboarded: true, verified: true, locale: 'en-US', name: 'Visual QA' }`
- **items**: 1 saved `{ id: 'vqa-1', title: 'Test Item', category: 'sample', detail: 'vqa-detail' }` + `setSelectedItem('vqa-1')`
- **wallet**: `{ balance: 1000, ledger: [], applyCapPct: 50 }`
- **session**: `{ region: 'primary', channel: 'default' }`

`overrideFeatureFlags(isFeatureEnabled)` → force-enables flag-gated UI when VQA on.

Visible indicator: **red "VQA" pill badge top-right** (safe-area inset).

Peer deps (consumer): `react`, `react-native`, `@react-native-async-storage/async-storage`.
Optional injected dep: `react-native-restart` (passed to `toggleVqaAndRestart`).

## Renderer (scripts/render-canonical.js)

Node + Playwright (chromium). Viewport `{ width: 1080, height: 2444, deviceScaleFactor: 1 }`.

Inject `window.__VQA_STATE__` via `page.addInitScript()` **BEFORE** bundle eval → redux seeds from it. Optional `window.__VQA_SCREEN__` for routing.

Serve bundle dir via local HTTP (pure node `http` + `fs`) → no extra deps.

Wait strategy:
- `--wait <ms>` → timeout
- `--wait <selector>` → `waitForSelector`
- default → poll `window.__APP_READY__` truthy

Full-page screenshot 1080×2444 → `--out canonical.png`.

```
node render-canonical.js --html <App.html> --state <state.json> --out <canonical.png> [--screen <Name>] [--wait <ms|selector>]
```

Exit 0 + `{"ok":true,...}` stdout on success. Nonzero + error JSON on failure.

## Diff Harness (scripts/visual-diff.js)

Dual metric:
- **pixelmatch** → perceptual pixel diff (catches color/position drift)
- **ssim.js** → structural similarity (catches structural matches under accumulated minor pixel diffs)

Resize-to-match via `sharp` if PNG sizes differ (document in top-of-file comment).

Outputs:
- `diff.png` — pixelmatch overlay
- `composite.png` — 3-up horizontal: canonical | native | diff, 2px black separators
- `metrics.json` — `{ ssim, pixelDiffCount, totalPixels, pixelDiffPct, width, height, canonical, native, timestamp }`

Stdout (terse): `SSIM 0.823 | diff 4.2% | composite: <path>`.

Pass: **SSIM ≥ 0.95 OR pixelDiffPct ≤ 5** → exit 0. Else exit 1.

```
node visual-diff.js --canonical <c.png> --native <n.png> --out-dir <dir> [--threshold 0.1]
```

## Iteration Loop (scripts/iterate-screen.sh)

```
iterate-screen.sh <ScreenName> [--platform android|ios] [--device <id>] [--bundle <App.html>] [--state <state.json>] [--out <dir>]
```

Defaults: `OUT_DIR=./.designmatch/<ScreenName>`. `BUNDLE_HTML` / `VQA_STATE` from env.

Steps:
1. `mkdir -p $OUT_DIR`
2. render canonical → `$OUT_DIR/canonical.png`
3. capture native:
   - android → `adb -s <id> exec-out screencap -p > $OUT_DIR/native.png` (`exec-out` avoids CRLF mangling)
   - ios sim → `xcrun simctl io <id> screenshot $OUT_DIR/native.png`
   - ios real → fall back to `idevicescreenshot $OUT_DIR/native.png` if on PATH
4. diff via `visual-diff.js` → capture exit code as PASS/FAIL
5. `open` (mac) / `xdg-open` (linux) the composite
6. print metrics + pass/fail + next-step hint

`set -euo pipefail`. Validate `node`, `adb`/`xcrun` per platform. Clear error messages.

## Patterns to Enforce

- **Typography Platform gate**: `Platform.OS === 'android' ? {} : { fontWeight: 'N' }` — keeps Android on family-name bold (e.g. `Bricolage-Bold`) instead of synthesized weight that drifts from canonical.
- **`normalize()` always-on** wrapper for px values (width-relative RN scaler, base 414).
- Default Typography variant resolves to body family → **headings MUST specify display variant**.
- **Single safe-area**: Screen wrapper OR inner container, never both.
- **Header choice**: native stack header OR in-screen PushHeader, never both.
- **Custom `tabBarStyle`** when expand-to-label animation is needed.

See `references/anti-patterns.md` for the 8-item ❌/✅ checklist.
See `references/canonical-values.md` for fonts + spacing cheat-sheet.

## Agent Dispatch Guidance

- One agent per file or per logical concern → avoid file-overlap conflicts.
- Each agent commits independently (`--no-verify` if hooks broken in worktree).
- Avoid worktree isolation when agents need full `node_modules` + live device.
- Explicitly send each agent the `Platform.OS` pattern — don't assume inference from canonical jsx.
- Explicitly tell each agent to use `normalize()` wrapping — they otherwise inline literal px and bloat the diff.

## Scoring & Success

- Report SSIM (0–1) + pixel-diff % per screen.
- Target: **SSIM ≥ 0.95 OR pixel-diff ≤ 5%**.
- Baseline (a real screen, pre-loop): 35% → 55% over ~30 eyeballed commits.

## Quick Reference

| Concern | Answer |
|---|---|
| Canonical viewport | 1080×2444, deviceScaleFactor 1 |
| Pass gate | SSIM ≥ 0.95 OR pixelDiff ≤ 5% |
| State injection | `window.__VQA_STATE__` via `addInitScript` |
| Android capture | `adb -s <id> exec-out screencap -p` |
| iOS sim capture | `xcrun simctl io <id> screenshot` |
| iOS real capture | `idevicescreenshot` |
| VQA toggle | 5× long-press AppLogo in 4s |
| Storage key | `dm_visual_qa` |
| Composite layout | canonical \| native \| diff (2px black sep) |

## Common Mistakes

- Skipping `Platform.OS` gate on fontWeight → Android synthesizes bold → diff bloats.
- Inlining literal px instead of `normalize()` → fails on non-base-414 devices.
- Double safe-area (Screen + inner) → top padding doubles → header position drifts.
- Native stack header + in-screen PushHeader → two headers stack.
- Forgetting `exec-out` on adb → CRLF mangles PNG → unreadable native.png.
- Injecting `__VQA_STATE__` after bundle eval → redux already booted → seed ignored.

Files in this skill

  • SKILL.md11.4 KB
  • assets/visual-qa.ts8.6 KB
  • references/anti-patterns.md6.4 KB
  • references/canonical-values.md3 KB
  • scripts/fetch-canonical.js5.1 KB
  • scripts/iterate-screen.sh6.4 KB
  • scripts/render-canonical.js8.1 KB
  • scripts/visual-diff.js8.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.