Design handoff
Skill markdavidgan/apple-dev-skills/platforms/claude/skills/design-handoff
Produce a current, labeled screenshot package of a whole app experience for an external design reviewer (Claude Design, a human designer, or any image-consuming tool), reusing the project's existing screenshot UITest target and fastlane snapshot lane. Use when you need an up-to-date visual handoff of an iOS app's UI states, refresh a stale handoff after UI changes, or bundle screenshots plus design context for upload. Has an authoritative simulator-capability guard so it never tries to capture on a machine that may not run the simulator.From its SKILL.md
npx -y skills add markdavidgan/apple-dev-skills --skill design-handoffAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.0 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
Design Handoff
Purpose: Turn an app's real UI states into a current, captioned screenshot package that an external reviewer can consume — without inventing a parallel screenshot mechanism, and without ever capturing on a machine that may not run the simulator. Trigger: You need to hand an app's UI to Claude Design / a designer / an image-consuming tool, and the committed screenshots are missing or stale.
This skill is the generic engine. A project binds it through a thin overlay skill (generated by overlay-sync) that supplies the app list, schemes, paths, and a design-context brief. The engine never hard-codes a project.
Core principle: reuse, never duplicate
Most iOS apps already have a screenshot path — a UITest target driven by fastlane snapshot (capture_ios_screenshots), a seeding mechanism for deterministic states, and stable accessibility identifiers. This skill drives that existing path. It does not create a second harness, a second seeding system, or a -STATE enum. If the app's deterministic states live in a seeder enum, the manifest references those enum cases by name. If a needed state is missing, you extend the existing seeder/UITest target — you do not start a parallel one. (See the "never keep multiple versions of a feature" rule that most Apple projects in this org enforce.)
Phases
/design-handoff <app> [phase] [--dest <dir>]
| Phase | Where it may run | Does |
|---|---|---|
prepare | any host (incl. capture-forbidden) | Reconcile the manifest against the app's screenshot UITest target — report/author the test methods that produce each manifest shot. No simulator. |
capture | capture-capable host only | Run the host guard, then the project's existing fastlane screenshot lane → raw PNGs. |
package | any host | Collect the raw PNGs into latest/, archive the previous set, write captions + design context + the reviewer prompt, stamp provenance. |
bundle | any host | Zip latest/ and copy it to --dest. |
all | capture-capable host | capture then package (then bundle if --dest is given). |
Default phase is package if raw screenshots already exist, otherwise the skill tells you to run capture on a capable host first.
Step 1 — Simulator-capability guard (REQUIRED before capture/all)
You cannot reliably probe whether the simulator will destabilize this machine. An explicit opt-out therefore always wins over any hardware probe. Run the shipped guard, which echoes one of forbidden | unavailable | insufficient | capable:
bash "<skill-dir>/scripts/host-guard.sh"
| Result | Action |
|---|---|
forbidden | A marker (.claude/NO_SIMULATOR.md, $AETHER_NO_SIMULATOR, ~/.config/no-simulator) opts this machine out. Stop. Never override. Tell the user to run capture on a capture-capable Mac, then package anywhere. |
unavailable | Not macOS, no Xcode, or no installed runtime. Cannot capture here. |
insufficient | Below the RAM/disk floor. Warn; prefer a capable host. |
capable | Proceed to capture. |
The marker is authoritative. If unsure, treat as
forbidden. This is the same ladderpreview-captureuses — one convention across the toolkit.
Step 2 — The manifest (<app screenshots dir>/shots.yaml)
Single source of truth for "which states matter for review." One entry per shot. The manifest maps each shot to the app's existing deterministic state mechanism — do not invent launch flags the app does not already read.
# Captions double as the reviewer's per-image context.
scheme: AppName-Screenshots # the existing fastlane snapshot scheme
seedMechanism: launchArgScenario # how this app stages state (documentation only)
shots:
- id: "01_TimerIdle" # MUST equal the snapshot("<id>") name in the UITest
title: "Timer — idle"
caption: "Resting dial at 25:00; first-run hint visible."
scenario: "freshInstall" # an existing seeder case, not a new one
nav: [] # accessibility-id taps after launch, in order
- id: "03_QuickCapture"
title: "Capture — typing"
caption: "Thought-parking sheet open with sample text."
scenario: "multipleCaptures"
nav: ["captureButton", "type:captureTextField:Email the team"]
devices: ["iPhone 17 Pro Max"] # informational; the Snapfile owns the real device list
# States worth adding later but not yet in the UITest target — never silently dropped:
wishlist:
- "Completion moment"
- "Session detail / ThoughtVerse"
A template lives at <skill-dir>/templates/shots.example.yaml.
Step 3 — prepare: reconcile manifest ↔ UITest target
- Read
shots.yamland the app's existing screenshot UITest file (the one theschemebuilds). - For every shot whose
idhas no matchingsnapshot("<id>")call, author one test method following the file's established pattern (seed thescenario, launch, applynav, wait on the relevant accessibility id, thensnapshot("<id>")). Reuse the existing seeding helper (e.g.TestDataSeeder/setupSnapshot) — do not introduce a new one. - Report a diff: shots added, updated, already present, and any manifest
idyou could not map (with why).prepareis idempotent — an unchanged manifest produces no edits. - Commit the UITest changes. Never run the UITests here (capture happens in its own phase, on a capable host).
If the app has no screenshot UITest target yet, say so and point the user at the project's UITest conventions; scaffold one only with explicit approval.
Step 4 — capture (capable host only)
After the guard passes, run the project's existing lane (the overlay supplies the exact command), e.g.:
cd <app dir> && bundle exec fastlane ios_screenshots
Raw PNGs land wherever the project's Snapfile/lane writes them (commonly fastlane/screenshots/<locale>/). Do not reimplement capture; if the project also has a Watch lane and the manifest has Watch shots, run that too. Never run plain UI tests as part of this — capture is a render pass, not a test pass.
Step 5 — package
Run the shipped packager, then write the prose:
node "<skill-dir>/scripts/package-handoff.mjs" \
--raw "<dir of captured PNGs>" \
--out "<app dir>/design-handoff" \
--scheme "<scheme>" \
--shots "<app screenshots dir>/shots.yaml"
The packager (deterministic file plumbing only):
- Archives the current
design-handoff/latest/intodesign-handoff/archive/<date>-<sha>/(gitignored history). - Copies the captured PNGs into
design-handoff/latest/. - Writes
latest/STAMP.txt— git SHA, marketing + build version (best-effort), capture date, scheme, image count — so a stale set can never again pass as current. - Emits
latest/manifest.json(id → caption) for downstream tools.
Then you write the prose into latest/ (the packager does not fabricate these):
manifest.md— each image mapped to its caption.<app>-design-context.md— a code-grounded brief (design tokens, per-screen intent). The overlay supplies the app's design language; ground every claim in the actual code, never invent palette/spacing.CLAUDE-DESIGN-PROMPT.md— the paste-in reviewer prompt + the explicit upload list + "point the reviewer at the scoped<app dir>/subdirectory, not the whole monorepo." Start from<skill-dir>/templates/CLAUDE-DESIGN-PROMPT.template.md.
Honesty rule: if some manifest shots were not captured (forbidden host, wishlist states), package lists them as missing rather than shipping a handoff that looks complete. Never claim visual parity you cannot see.
Step 6 — bundle (optional)
node "<skill-dir>/scripts/package-handoff.mjs" --zip "<app dir>/design-handoff/latest" --dest "<dir>"
Zips latest/ to <app>-handoff-<version>-<sha>.zip and copies it to --dest (default ~/Desktop/<project>-handoffs/). The zip is transient — gitignore it.
Output layout
<app dir>/design-handoff/
latest/ # committed, canonical upload target
01_TimerIdle.png ...
manifest.json # generated (id -> caption)
manifest.md # authored
<app>-design-context.md # authored, code-grounded
CLAUDE-DESIGN-PROMPT.md # authored
STAMP.txt # generated provenance
archive/<date>-<sha>/ # gitignored history
Recommended .gitignore: **/design-handoff/archive/ and *-handoff-*.zip.
Error handling
| Symptom | Cause | Fix |
|---|---|---|
Guard returns capable on a known-bad machine | Marker missing | Create .claude/NO_SIMULATOR.md; the marker is authoritative |
capture invoked on forbidden host | Wrong machine | Refuse; run capture on a capable Mac, package anywhere |
Manifest id not found in UITest | Test method missing | Run prepare; author the method against the existing pattern |
Missing PNG for a manifest id | Capture skipped/failed | package lists it as missing — do not hand-fill |
| STAMP older than HEAD | latest/ is stale | Re-run capture+package on a capable host |
Cross-references
- Generated per-project overlay + descriptor sync:
overlay-sync - Simulator-capability convention shared here:
preview-capture - Device selection / simctl:
ios-simulate - Why UI-test execution is gated:
ios-test
What ships with it: 5 files
14.0 KB alongside SKILL.md, 2 of them executable
scripts/
- host-guard.shruns1.7 KB
- package-handoff.mjsruns6.8 KB
templates/
- CLAUDE-DESIGN-PROMPT.template.md2.2 KB
- overlay-template.md1.4 KB
- shots.example.yaml1.8 KB