Design handoff
Skill markdavidgan/apple-dev-skills/platforms/cursor/skills/design-handoff
Apple platform development skills for Claude Code, Cursor, Kimi Code, Antigravity, Codex CLI, and Agy.
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
- 2 stars2 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.
What its author says it does
Copied from the file, not written here
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.
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