From Heimdall
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hmd:designmatchThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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%.
Do NOT use for: unit logic, redux state shape, navigation graph correctness — those are not visual.
From the RN project root:
designmatch init "<claude-design-url>" --app-dir . --port-all
This single command does the full bootstrap:
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..designmatch/canonical/ by intercepting every network response, then flips config.json kind from url → local-dir (original URL preserved for re-fetch).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).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):
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.
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:
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.<div> → <View>; <span> / <p> / <h*> → <Text>; <img> → <Image>; <button> → <Pressable>className / Tailwind → StyleSheet.create()normalize(n)fontWeight on bold-family text → Platform.OS gate (anti-pattern #2)<svg> → react-native-svg primitivesdesignmatch iterate <ScreenName> --platform android --device <id>
Renders canonical, captures device, diffs, opens composite. Pass when SSIM ≥ 0.95 OR pixel-diff ≤ 5%.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.
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.
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
Trigger: 5× long-press AppLogo within 4s → flip AsyncStorage dm_visual_qa → RNRestart.restart().
Boot path: primeVisualQaFlag() reads AsyncStorage → applyVisualQaState(dispatch) seeds:
{ onboarded: true, verified: true, locale: 'en-US', name: 'Visual QA' }{ id: 'vqa-1', title: 'Test Item', category: 'sample', detail: 'vqa-detail' } + setSelectedItem('vqa-1'){ balance: 1000, ledger: [], applyCapPct: 50 }{ 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).
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> → waitForSelectorwindow.__APP_READY__ truthyFull-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.
Dual metric:
Resize-to-match via sharp if PNG sizes differ (document in top-of-file comment).
Outputs:
diff.png — pixelmatch overlaycomposite.png — 3-up horizontal: canonical | native | diff, 2px black separatorsmetrics.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]
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:
mkdir -p $OUT_DIR$OUT_DIR/canonical.pngadb -s <id> exec-out screencap -p > $OUT_DIR/native.png (exec-out avoids CRLF mangling)xcrun simctl io <id> screenshot $OUT_DIR/native.pngidevicescreenshot $OUT_DIR/native.png if on PATHvisual-diff.js → capture exit code as PASS/FAILopen (mac) / xdg-open (linux) the compositeset -euo pipefail. Validate node, adb/xcrun per platform. Clear error messages.
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).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.
--no-verify if hooks broken in worktree).node_modules + live device.Platform.OS pattern — don't assume inference from canonical jsx.normalize() wrapping — they otherwise inline literal px and bloat the diff.| 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) |
Platform.OS gate on fontWeight → Android synthesizes bold → diff bloats.normalize() → fails on non-base-414 devices.exec-out on adb → CRLF mangles PNG → unreadable native.png.__VQA_STATE__ after bundle eval → redux already booted → seed ignored.Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub randomittin/heimdall --plugin hmd