agentleFS
randomittinrandomittin/heimdall/skills/designmatch/SKILL.md

designmatch

Skill5 starsChanged 38 days agoMITView on GitHub

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.

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

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

    line 23
  2. Very specific:

    All px literals → normalize(n)

    line 68
  3. Distinctive:

    Keep variable names + structure identical to canonical.

    line 71
  4. Very specific:

    If the canonical is registered as a URL (not yet downloaded), designmatch port and designmatch port-all auto-run designmatch fetch first — …

    line 76
  5. Very specific:

    default → poll window.__APP_READY__ truthy

    line 135
  6. Distinctive:

    Default Typography variant resolves to body family → headings MUST specify display variant.

    line 191
  7. Very specific:

    Custom tabBarStyle: when expand-to-label animation is needed.

    line 194
  8. Very specific:

    Avoid worktree isolation when agents need full node_modules + live device.

    line 203

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

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):

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

ConcernAnswer
Canonical viewport1080×2444, deviceScaleFactor 1
Pass gateSSIM ≥ 0.95 OR pixelDiff ≤ 5%
State injectionwindow.__VQA_STATE__ via addInitScript
Android captureadb -s <id> exec-out screencap -p
iOS sim capturexcrun simctl io <id> screenshot
iOS real captureidevicescreenshot
VQA toggle5× long-press AppLogo in 4s
Storage keydm_visual_qa
Composite layoutcanonical | 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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.