App Store & Google Play Screenshots Generator
Overview
Scaffold a pre-built Next.js + ShadCN editor that lets the user design and export App Store and Google Play screenshots as advertisements (not UI showcases). The editor handles all the heavy lifting:
- Connected live preview at the canvas's true resolution (scaled to fit)
- Drag-to-reorder screens, inline text editing, layout switcher per screen
- Cross-screen mockups: phone/device frames, captions, and layered elements can be moved across adjacent screens, then exported as clipped crops
- Drop-target screenshot picker (file → saved to
public/screenshots/uploaded/<hash>.png)
- Auto-save to
app-store-screenshots.json at the project root (git-trackable) + localStorage mirror
- Easy iOS ↔ Mac ↔ Android platform switch — separate slide decks live side by side
- One-click bulk PNG export at every Apple/Google-required resolution via
html-to-image
- Light/dark variant toggle per slide, a toolbar theme picker (one palette preset per named style), locale select
- A Copy ideas menu next to the headline field with formulas for hero, differentiator, feature, proof, and closer slides
- Per-slide custom background colors (caption colours stay readable automatically), a live screenshot font menu, and importing licensed WOFF2/WOFF/TTF/OTF fonts
- Image overlay elements (logos, badges, photos) with drag/resize/rotation/layering controls and directional edge fades
- Toolbar Undo/Redo (
⌘Z / ⇧⌘Z) over the last 50 edits of the session
- Guided in-place migration for older projects created by this skill; passive and explicit migrations keep legacy decks isolated until the user intentionally opts into connected canvas
Supported devices out of the box:
- iPhone (portrait) — Apple App Store
- iPad (portrait) — Apple App Store
- iPhone Duo outer and inner displays, each in portrait and landscape (four decks) — Apple App Store; see "iPhone Duo" under Step 5
- App Store creative assets: universal 5244×2950, product page header 3840×1646, search results 3:2 — Apple App Store; see "App Store creative assets" under Step 5
- Apple TV (landscape, 4K + HD) — Apple App Store
- Apple Watch (portrait, every Ultra/Series size) — Apple App Store
- CarPlay (landscape head unit) — uploaded into the iPhone slot; see "Apple TV, Apple Watch and CarPlay" under Step 5
- Mac (16:10 landscape, own Mac tab) — Mac App Store (
2880×1800, 2560×1600, 1440×900, 1280×800); see "Mac" under Step 5
- Android Phone (portrait) — Google Play
- Android Tablet 7" (portrait + landscape) — Google Play
- Android Tablet 10" (portrait + landscape) — Google Play
- Feature Graphic (1024×500 banner) — Google Play store listing header
Core Principle
Screenshots are advertisements, not documentation. Every screenshot sells one idea. If you're showing UI, you're doing it wrong — you're selling a feeling, an outcome, or killing a pain point. Use this skill's interactive editor to iterate on copy and layout fast; do not hand-craft the page from scratch.
What This Skill Does
- Copies a pre-built template from
template/ (co-located with this SKILL.md) into the user's working directory.
- Installs dependencies with the user's package manager.
- Drops the user's screenshots into
public/screenshots/... and their app icon into public/.
- (Optionally) prefills
app-store-screenshots.json with the user's app name, starting copy, screenshots, and connected-canvas preference so the first preview is meaningful.
- Starts the dev server and tells the user to open the editor in the browser.
You should NOT write page.tsx, device frames, or export logic by hand. They live in the template.
Step 0: Probe for Existing Screenshot Projects
Before asking the new-project questions in Step 1, always inspect the current working directory for an existing app-store-screenshots implementation.
Run lightweight probes:
test -f package.json && sed -n '1,220p' package.json
test -f app-store-screenshots.json && sed -n '1,120p' app-store-screenshots.json
rg -n "app-store-screenshots|html-to-image|toPng|ScreenshotEditor|DeckCanvas|connectedCanvas|EXPORT_SIZES|mockup.png|PHONE_SCREEN" package.json src app public 2>/dev/null
find public -maxdepth 4 \( -path "*/screenshots*" -o -name "mockup.png" -o -name "app-icon.png" \) -print 2>/dev/null
Treat the project as an older implementation when any of these are true:
app-store-screenshots.json exists but has no schemaVersion, has schemaVersion < 2, or lacks connectedCanvas.
src/components/editor/screenshot-editor.tsx exists but the editor does not reference DeckCanvas or connectedCanvas.
src/app/page.tsx contains a previous all-in-one generator (html-to-image, toPng, EXPORT_SIZES, PHONE_SCREEN, hardcoded slide arrays/themes).
- The repo contains the old screenshot asset layout (
public/mockup.png, public/screenshots...) plus a screenshot generator package setup.
If an older implementation is detected, ask exactly one question before doing anything else:
I found an older App Store screenshots project here. Do you want me to migrate this existing project to the new connected-canvas editor?
- Yes — migrate the existing project to the new editor
- No — set up or modify a project another way
If the user chooses Yes, do not ask the Step 1 questionnaire. Run the migration path below using the files already in the repo. If the user chooses No, continue to Step 1.
Migration Path (When User Says Yes)
The goal is an in-place UI/template upgrade, not a redesign. Preserve the user's existing app name, copy, screenshot paths, app icon, uploaded assets, locales, and device decks wherever they already exist. Replace the old UI implementation with the current template. Keep legacy decks in isolated export mode unless the project already explicitly opted into connected canvas.
Migration rules:
- Do not ask further product/design questions. The user already has a project. Infer from existing files and report any non-blocking gaps at the end.
- Never delete user assets. Preserve
public/screenshots/, public/app-icon.png, uploaded screenshots, and any existing app-store-screenshots.json.
- Preserve recoverability. If the worktree is not clean, do not revert unrelated changes. Before overwriting template files, copy replaced project-state/assets/code snapshots to a temporary backup outside the repo (for example
/tmp/app-store-screenshots-migration-<timestamp>/) and mention the path in the final response.
- Prefer structured migration. Read and write
app-store-screenshots.json with JSON tooling. Do not regex-edit JSON.
- Set
schemaVersion: 2 and keep legacy connectedCanvas safe. If the existing project already has an explicit boolean connectedCanvas, preserve it. If the project is pre-v2 or lacks the flag, write "connectedCanvas": false so offscreen/clipped legacy mockups do not leak into neighboring exports. New projects still default to connected canvas.
- Keep screenshots pointed at existing files. Do not rename screenshot files unless the old project already depended on numeric names and the migration needs them. Existing static paths are fine.
- Handle custom themes without asking. If the old project references a custom
themeId, merge the matching theme object into the new src/lib/constants.ts when it can be found. If it cannot be recovered, leave the themeId in project JSON; the editor will fall back to clean-light and warn, and you should note that a custom theme needs manual restoration.
- Merge package metadata when possible. The template's dependencies and scripts must win for the screenshot editor, but preserve unrelated existing
dependencies, devDependencies, and useful scripts unless they directly conflict.
- Do not import template sample decks into real migrations. If the old project already has decks or screenshots, use the template for UI/code only. Keep template sample screenshots/decks out of the migrated project so the user's app does not inherit unrelated example content.
- Use a disposable copy for dogfooding. If the user asks to test or review the migration instead of actually migrating their project, copy the app to a temp directory or worktree and run the migration there. Only touch the real checkout when the user explicitly asks for the real migration and answers Yes.
Recommended migration sequence:
# 1. Snapshot useful old files outside the repo.
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/tmp/app-store-screenshots-migration-$STAMP"
mkdir -p "$BACKUP_DIR"
cp -R app-store-screenshots.json public src package.json tailwind.config.ts next.config.mjs "$BACKUP_DIR/" 2>/dev/null || true
# 2. Preserve project state and assets that must survive template copy.
PRESERVE_DIR="$BACKUP_DIR/preserve"
mkdir -p "$PRESERVE_DIR"
cp app-store-screenshots.json "$PRESERVE_DIR/" 2>/dev/null || true
cp -R public/screenshots "$PRESERVE_DIR/screenshots" 2>/dev/null || true
cp public/app-icon.png "$PRESERVE_DIR/app-icon.png" 2>/dev/null || true
# 3. Copy the current template over the old UI implementation.
cp -R "<SKILL_DIR>/template/." "$PWD/"
cp app-store-screenshots.json "$BACKUP_DIR/template-app-store-screenshots.json" 2>/dev/null || true
# 4. Restore preserved user state/assets over template samples.
cp "$PRESERVE_DIR/app-store-screenshots.json" app-store-screenshots.json 2>/dev/null || true
mkdir -p public
if [ -d "$PRESERVE_DIR/screenshots" ]; then
mkdir -p "$BACKUP_DIR/template-samples/public"
mv public/screenshots "$BACKUP_DIR/template-samples/public/screenshots" 2>/dev/null || true
cp -R "$PRESERVE_DIR/screenshots" public/screenshots
else
mkdir -p public/screenshots
fi
cp "$PRESERVE_DIR/app-icon.png" public/app-icon.png 2>/dev/null || true
After copying, upgrade or create app-store-screenshots.json. If an existing project file exists, coerce it in place. If no project file exists but old slide data is embedded in src/lib/defaults.ts or src/app/page.tsx, extract it best-effort into the template's project JSON before falling back to starter slides. Prefer old arrays or objects named slides, screens, features, defaultSlides, appName, tagline, theme, and screenshot paths. If the old implementation only has image files, sort public/screenshots/** by path and seed slides from those files.
Use a small JSON script like this for the final project-state coercion:
BACKUP_DIR="$BACKUP_DIR" node <<'NODE'
const fs = require("fs");
const path = require("path");
const PROJECT_FILE = "app-store-screenshots.json";
const DEFAULT_LOCALE = "en";
const DEVICE_KEYS = ["iphone", "ipad", "duo-outer", "duo-outer-landscape", "duo-inner", "duo-inner-landscape", "tvos", "watchos", "carplay", "mac", "android", "android-7", "android-10", "feature-graphic", "creative-universal", "creative-header", "creative-search"];
const LAYOUTS = ["hero", "device-bottom", "device-top", "two-devices", "no-device", "split-landscape", "feature-graphic"];
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return null;
}
}
const templateState =
readJson(path.join(process.env.BACKUP_DIR || "", "template-app-store-screenshots.json")) ||
readJson(PROJECT_FILE) ||
{};
const existingState = readJson(PROJECT_FILE) || {};
const hasExplicitConnectedCanvas = typeof existingState.connectedCanvas === "boolean";
const existingDecks =
existingState.slidesByDevice && typeof existingState.slidesByDevice === "object" && !Array.isArray(existingState.slidesByDevice)
? existingState.slidesByDevice
: {};
const hasExistingDecks = Object.keys(existingDecks).length > 0;
const state = {
...templateState,
...existingState,
slidesByDevice: hasExistingDecks ? existingDecks : templateState.slidesByDevice || {},
};
const legacySlides =
Array.isArray(existingState.slides) ? existingState.slides :
Array.isArray(existingState.screens) ? existingState.screens :
Array.isArray(existingState.features) ? existingState.features :
null;
if (legacySlides && !hasExistingDecks) {
state.slidesByDevice = {
iphone: legacySlides,
};
}
function localized(value) {
if (typeof value === "string") return { [DEFAULT_LOCALE]: value };
if (value && typeof value === "object" && !Array.isArray(value)) {
return Object.fromEntries(Object.entries(value).filter(([, text]) => typeof text === "string"));
}
return {};
}
function cleanTransform(value) {
if (!value || typeof value !== "object") return undefined;
const { x, y, width, height, rotation, zIndex } = value;
if (![x, y, width, height].every((n) => typeof n === "number" && Number.isFinite(n))) return undefined;
return {
x,
y,
width: Math.max(1, width),
height: Math.max(1, height),
...(typeof rotation === "number" && Number.isFinite(rotation) ? { rotation } : {}),
...(typeof zIndex === "number" && Number.isFinite(zIndex) ? { zIndex } : {}),
};
}
function firstString(...values) {
return values.find((value) => typeof value === "string") || "";
}
// The editor refuses to load a deck with empty or repeated screen ids.
function uniqueId(value, used) {
let id = typeof value === "string" && value.trim() ? value : "";
while (!id || used.has(id)) id = `migrated-${Math.random().toString(36).slice(2, 10)}`;
used.add(id);
return id;
}
function migrateSlide(slide, used) {
if (!slide || typeof slide !== "object" || Array.isArray(slide)) return null;
const transforms = {};
const rawTransforms = slide.transforms && typeof slide.transforms === "object" ? slide.transforms : {};
for (const [id, transform] of Object.entries(rawTransforms)) {
const cleaned = cleanTransform(transform);
if (["caption", "device", "deviceSecondary", "callout"].includes(id) && cleaned) transforms[id] = cleaned;
}
const textIds = new Set();
const textElements = Array.isArray(slide.textElements)
? slide.textElements
.map((element) => {
if (!element || typeof element !== "object" || Array.isArray(element)) return null;
const transform = cleanTransform(element.transform);
if (!transform) return null;
return {
...element,
id: uniqueId(element.id, textIds),
text: localized(element.text),
transform,
fontSize: Number.isFinite(element.fontSize) && element.fontSize > 0 ? element.fontSize : undefined,
fontWeight: Number.isFinite(element.fontWeight) && element.fontWeight > 0 ? element.fontWeight : undefined,
};
})
.filter(Boolean)
: undefined;
const imageIds = new Set();
const imageElements = Array.isArray(slide.imageElements)
? slide.imageElements.map((element) => {
if (!element || typeof element !== "object" || Array.isArray(element) || typeof element.src !== "string") return null;
const transform = cleanTransform(element.transform);
return transform ? { ...element, id: uniqueId(element.id, imageIds), transform } : null;
}).filter(Boolean)
: undefined;
return {
...slide,
id: uniqueId(slide.id, used),
layout: LAYOUTS.includes(slide.layout) ? slide.layout : "device-bottom",
label: localized(slide.label),
headline: localized(slide.headline || slide.title || slide.caption || slide.copy),
screenshot: firstString(slide.screenshot, slide.image, slide.src, slide.path),
screenshotSecondary: typeof slide.screenshotSecondary === "string" ? slide.screenshotSecondary : undefined,
inverted: typeof slide.inverted === "boolean" ? slide.inverted : undefined,
...(Object.keys(transforms).length ? { transforms } : { transforms: undefined }),
...(textElements && textElements.length ? { textElements } : { textElements: undefined }),
...(imageElements && imageElements.length ? { imageElements } : { imageElements: undefined }),
// The editor clamps magnifier values on load; only a non-object would be rejected.
callout: slide.callout && typeof slide.callout === "object" && !Array.isArray(slide.callout) ? slide.callout : undefined,
};
}
state.schemaVersion = 2;
state.connectedCanvas = hasExplicitConnectedCanvas ? existingState.connectedCanvas : false;
// Unique codes like "en", "pt-BR", "zh_Hans"; anything else makes the editor refuse the file.
const LOCALE_CODE = /^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/;
state.locales = Array.isArray(state.locales)
? [...new Set(state.locales.filter((locale) => typeof locale === "string" && LOCALE_CODE.test(locale)))]
: [];
if (!state.locales.length) state.locales = [DEFAULT_LOCALE];
state.locale = state.locales.includes(state.locale) ? state.locale : state.locales[0];
state.device = DEVICE_KEYS.includes(state.device) ? state.device : "iphone";
if (state.orientation !== "portrait" && state.orientation !== "landscape") delete state.orientation;
for (const key of ["appName", "themeId", "appIcon"]) {
if (state[key] !== undefined && typeof state[key] !== "string") delete state[key];
}
// The feature graphic only shows an icon that `appIcon` points at.
if (!state.appIcon && fs.existsSync(path.join("public", "app-icon.png"))) state.appIcon = "/app-icon.png";
if (state.slidesByDevice && typeof state.slidesByDevice === "object") {
for (const [device, slides] of Object.entries(state.slidesByDevice)) {
// The editor only accepts known device decks; the backup keeps the original.
if (!DEVICE_KEYS.includes(device)) {
delete state.slidesByDevice[device];
continue;
}
const used = new Set();
state.slidesByDevice[device] = Array.isArray(slides) ? slides.map((slide) => migrateSlide(slide, used)).filter(Boolean) : [];
}
}
if (!state.slidesByDevice[state.device]) {
const firstDeviceWithSlides = DEVICE_KEYS.find((device) => state.slidesByDevice[device]?.length);
if (firstDeviceWithSlides) state.device = firstDeviceWithSlides;
}
fs.writeFileSync(PROJECT_FILE, JSON.stringify(state, null, 2) + "\n");
NODE
If package.json existed before the template copy, merge it after the project-state coercion instead of leaving a blind overwrite. Keep the template's dev, build, and start scripts and all editor dependencies, then add any old non-conflicting scripts and dependencies from the backed-up package.json.
BACKUP_DIR="$BACKUP_DIR" node <<'NODE'
const fs = require("fs");
const path = require("path");
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return null;
}
}
const oldPkg = readJson(path.join(process.env.BACKUP_DIR || "", "package.json"));
const templatePkg = readJson("package.json");
if (oldPkg && templatePkg) {
const merged = {
...oldPkg,
...templatePkg,
scripts: {
...(oldPkg.scripts || {}),
...(templatePkg.scripts || {}),
},
dependencies: {
...(oldPkg.dependencies || {}),
...(templatePkg.dependencies || {}),
},
devDependencies: {
...(oldPkg.devDependencies || {}),
...(templatePkg.devDependencies || {}),
},
};
fs.writeFileSync("package.json", JSON.stringify(merged, null, 2) + "\n");
}
NODE
Then install/update dependencies and verify:
bun install # or pnpm install / yarn / npm install
set -o pipefail
bun run build 2>&1 | tee "$BACKUP_DIR/build.log" # or the detected package-manager equivalent
Start the dev server and verify in the browser:
- The toolbar shows Isolated for migrated pre-v2 decks, unless the project file already explicitly had
"connectedCanvas": true.
- Existing screens, copy, screenshot paths, and app icon are present.
- Referenced screenshot files exist for every configured locale, or the final report lists the missing paths.
- Device decks retained from the old project do not silently become template placeholders. If a retained deck has empty screenshots or lacks active-locale copy, report it as a follow-up instead of removing it.
- A bundle export succeeds for the active device.
app-store-screenshots.json contains "schemaVersion": 2 and a boolean "connectedCanvas" value.
Step 1: Gather Input (Before Scaffolding)
Ask the user these. Do not proceed until you have answers:
Required
- App screenshots — "Do you already have screenshots of the devices?"
- App icon — "Where is your app icon PNG?"
- App name — "What's the app called?"
- Feature list — "List your app's features in priority order. What's the #1 thing your app does?"
- Style direction — "What style do you want? You can either (a) pick one of the named deep-spec styles, or (b) describe your own vibe in your own words (warm/organic, dark/moody, clean/minimal, bold/colorful, plus any reference apps you like) and I'll build a custom palette. The template also ships with palette presets in the toolbar theme picker: the generic
clean-light, dark-bold, warm-editorial, ocean-fresh, and bloom-roast, plus one preset per named style (same id as the style slug). The named deep specs live in style-prompts/ — see style-prompts.md for the full index. Currently available: Retro Rubberhose Mascot, Moody Curated Dating, Paper Sticker Skeuomorphic, Dreamy Pastel Couples, Hand-Drawn Editorial Tasks, Glossy 3D K-Beauty Creator, Liquid Glass Aurora, Swiss Grid Bold, Neon Athletic Night, Magazine Cover Editorial, Candy Pop Social, Soft Clay Wellness, Midnight Glow Pro, Risograph Zine, Bento Keynote Grid, Toybox Primary, Quiet Japandi, Vintage Travel Poster. If the user names one of these — or describes something that clearly matches one — read style-prompts/_QUALITY_BAR.md first, then the matching deep spec file, and apply its entire spec (palette, gradients, shadows, rotations, per-slide breakdown). If the user describes a fully custom style, fall back to the General Visual Design Principles below and pick the closest deep spec as a starting reference."
Optional
- Target stores — Apple App Store, Mac App Store, Google Play, or a mix? Determines which platform decks to seed.
- iPad / Mac / Android tablet screenshots — If yes, what sizes and orientations?
- Apple TV / Apple Watch / CarPlay — Does the app have a tvOS or watchOS app, or CarPlay support? Each gets its own deck.
- iPhone Duo — Does the app support iPhone Duo? If so, read
iphone-duo.md before planning. Ask: what does the app do differently on the inner display (two panes, sidebar, wider grid)? Which displays and orientations have their own layout? Do they have simulator captures from each display at the exact sizes? Apple's bezels for iPhone, iPad, Watch, TV, Mac and iPhone Duo ship with the template (see Step 5); ask only whether they want a different finish.
- App Store creative assets — Do they want a product page header / search results asset? Default to one universal 5244×2950 asset; offer separate header and search assets only if they need different compositions. Ask for a short brand line for each; don't reuse a screenshot headline.
- Feature Graphic — Want a 1024×500 Play Store banner too?
- Localized screenshots — Languages? (e.g. en, de, es, pt, ja, ar, he)
- Number of slides — Apple allows up to 10, Google Play up to 8.
- Brand colors / font — If they want a custom theme beyond the built-in presets.
- Additional instructions — Anything specific.
IMPORTANT: If the user gives instructions at any point, follow them. They override skill defaults.
Step 2: Scaffold the Template
Detect Package Manager
Priority: bun > pnpm > yarn > npm.
which bun && echo bun || which pnpm && echo pnpm || which yarn && echo yarn || echo npm
Copy the Template
The template lives at <this skill dir>/template/ — when the skill is installed, the whole folder is already on disk. Copy its contents (NOT the folder itself) into the user's working directory. The trailing /. copies dotfiles like .gitignore too.
# Replace <SKILL_DIR> with the absolute path to this skill (the directory containing SKILL.md).
cp -R "<SKILL_DIR>/template/." "$PWD/"
If the target directory already has a package.json, ask the user before overwriting during a new scaffold. If Step 0 detected an old implementation and the user chose Yes, do not ask this again; follow the migration path, preserve recoverability with the backup directory, and merge package metadata after the template copy.
Install Dependencies
bun install # or pnpm install / yarn / npm install
Drop the User's Assets
Move the user's screenshots into the layout the template expects:
public/
├── app-icon.png # ← user's app icon
├── mockup.png # ← already copied by the template (iPhone bezel)
├── frames/ # ← Apple's bezels, bundled; swap a file to change finish (Step 5)
└── screenshots/
├── apple/
│ ├── iphone/{locale}/01.png … N.png
│ ├── ipad/{locale}/01.png … N.png
│ ├── duo-outer/{portrait|landscape}/{locale}/01.png … N.png # iPhone Duo outer display
│ ├── duo-inner/{portrait|landscape}/{locale}/01.png … N.png # iPhone Duo inner display
│ ├── tvos/{locale}/01.png … N.png # Apple TV, 16:9
│ ├── watchos/{locale}/01.png … N.png # Apple Watch
│ ├── carplay/{locale}/01.png … N.png # CarPlay head-unit captures
│ └── mac/{locale}/01.png … N.png # Mac, 16:10
└── android/
├── phone/{locale}/01.png … N.png
├── tablet-7/{portrait|landscape}/{locale}/...
└── tablet-10/{portrait|landscape}/{locale}/...
The starter project state lives in app-store-screenshots.json, not src/lib/defaults.ts. If the user names their screenshots differently, either rename them or update the relevant slide screenshot fields in app-store-screenshots.json so the initial deck points at the right files. The user can also drag-drop files directly into the editor at runtime — those uploads are written to public/screenshots/uploaded/<hash>.png when the dev server is running.
(Optional) Seed Initial Copy
If the user provided headlines, edit app-store-screenshots.json to set:
appName
themeId (one of "clean-light" | "dark-bold" | "warm-editorial" | "ocean-fresh" | "bloom-roast", a named style slug such as "swiss-grid-bold" when the user picked that style, or add a matching entry to THEMES in src/lib/constants.ts). Themes may set accentAlt for the label color on inverted slides.
appIcon — public path of the app icon (e.g. "/app-icon.png" after copying it to public/app-icon.png). The Play Store feature graphic shows it; blank uses the app's initial. The icon can also be picked in the feature-graphic inspector.
connectedCanvas (true for new connected decks; migrated legacy decks should stay false until the user opts in)
- Starter slides per device with the user's
label + headline + screenshot paths
- Optional
scene for the whole project: { backdrop: "gradient"|"solid"|"aurora"|"spotlight"|"grid"|"dots"|"lines", span: boolean, decoration: "blobs"|"rings"|"sparkles"|"none", shadow: 0–100, glow: 0–100, tilt: -30–30, headlineWeight: 300–900, headlineCase: "as-typed"|"upper", captionAlign: "auto"|"left"|"center" }. Omit it for the classic gradient + blobs look. Pick values that match the chosen style (e.g. spotlight + glow for dark pro styles, lines + left-aligned serif for editorial); the user can refine it in the editor's Scene popover or compare whole looks in Style Lab.
- Optional per-slide
callout: { focusX: 0–1, focusY: 0–1, zoom: 1.5–5, shape: "circle"|"rounded" } to magnify one detail of the primary screenshot. It sits over the device's upper right unless transforms.callout places it.
- Optional per-slide
typography: { labelScale, headlineScale, appNameScale } (0.5–2, default 1) when one headline is much longer or shorter than the rest of the deck. appNameScale only applies to the feature graphic, where headlineScale sizes the tagline.
Otherwise, leave the defaults — the user can rewrite copy in the editor.
Start the Dev Server
bun dev # → http://localhost:3000
Tell the user to open the URL and start editing. The editor auto-saves to app-store-screenshots.json at the project root (plus a localStorage mirror for instant paint). Uploaded screenshots land in public/screenshots/uploaded/<hash>.png. Both are git-trackable — committing them means another machine can git clone and resume the exact deck.
Step 3: Coach the User on Copy
Inside the editor the user will write headlines themselves, but they often need guidance. Apply these rules when reviewing their copy or generating suggestions.
Read copy-ideas.md before drafting headlines. It has formulas per deck slot (hero, differentiator, feature, proof, closer), ready lines for 13 app categories, eyebrow labels, a weak-to-better table, four deck arcs, and localization notes. When you propose copy, give three options per slide (paint a moment / state an outcome / kill a pain), then rewrite the chosen one in the selected style's voice. The editor's inspector has a matching Copy ideas menu next to the headline field (src/lib/copy-ideas.ts) so users can drop in a formula and replace the bracketed words themselves.
The Iron Rules
- One idea per headline. Never join two things with "and."
- Short, common words. 1-2 syllables. No jargon unless it's domain-specific.
- 3-5 words per line. Must be readable at thumbnail size in the App Store.
- Line breaks are intentional. Newlines in the textarea map directly to visible breaks.
Three Approaches
| Type |
What it does |
Example |
| Paint a moment |
You picture yourself doing it |
"Check your coffee without opening the app." |
| State an outcome |
What your life looks like after |
"A home for every coffee you buy." |
| Kill a pain |
Name a problem and destroy it |
"Never waste a great bag of coffee." |
Bad-to-Better
| Weak |
Better |
Why |
| Track habits and stay motivated |
Keep your streak alive |
one idea, faster to parse |
| Organize tasks with AI summaries |
Turn notes into next steps |
outcome-first, less jargon |
| Save recipes with tags and favorites |
Find dinner fast |
sells the benefit, not the UI |
Narrative Arc
The user's slide deck should follow a rough arc (skip slots that don't fit):
| Slot |
Purpose |
| #1 |
Hero / Main Benefit — the ONLY slide most people see |
| #2 |
Differentiator — what makes the app unique |
| #3 |
Ecosystem — widgets, watch, extensions (skip if N/A) |
| #4+ |
Core Features — one per slide, most important first |
| 2nd-to-last |
Trust Signal — "made for people who [X]" |
| Last |
More Features — pills listing extras (skip if few features) |
Layout Variation
Vary the layout field across slides. The editor exposes:
hero — centered headline + bottom-anchored device
device-bottom — same composition, smaller headline
device-top — flipped, device above caption (good contrast slide)
two-devices — back + front phones layered
no-device — big standalone headline (use sparingly)
split-landscape — caption left + device right (tablet landscape and Mac)
feature-graphic — Play Store banner (1024×500)
Never repeat the same layout twice in a row. Use 1-2 inverted (dark) slides for visual rhythm.
On iPhone Duo decks, two-devices is Folded + open: the second device is the same phone in its other state, drawn in that display's frame. Its second capture comes from the other display.
iPhone Duo Decks
Read iphone-duo.md before writing copy or seeding any Duo deck. It holds the research behind these rules, with sources. In short:
- Plan the decks. Lead with the inner display in landscape when the app has a real two-pane or wide layout. Add an outer portrait deck next. Make inner portrait and outer landscape decks only if the app has a real layout for them.
- Show what only an open Duo can do. At least one inner screenshot shows two panes, a sidebar or a wider grid. A screenshot that could have come from a normal iPhone wastes the Duo slot.
- Inner landscape: a one-line headline (about 30 characters or fewer) across the top, with the device large below. Pick captures where one pane's action and the other pane's result sit in the same frame.
- Outer portrait: a short, wide canvas. Use two short lines at most, large type, and one idea per screen. It can reuse the iPhone story.
- Folded + open once per deck, for the moment that carries across states ("Close it for today. Open it for the week.").
- Write copy about what the user gets, not the hardware. Localise every deck and check that each landscape line still fits on one line.
Cross-Screen / Cross-Canvas Composition
Use the connected canvas as a design tool during Step 3, after the narrative arc and layout rhythm are chosen and before final export. For most decks with 5+ slides, plan one tasteful cross-screen moment by default. For 8-10 slide decks, use at most two. For short, formal, or compliance-heavy decks, zero is fine. The goal is "these screenshots belong together," not "one giant poster chopped into pieces."
Good cross-screen patterns:
- An oversized phone, tablet, or screenshot mosaic bridges two adjacent screens by 10-30% of its width, while each exported crop still reads as a complete ad.
- A background horizon, photo, gradient, doodle path, waveform, starfield, sticker trail, or map route continues across the seam.
- A mascot, 3D object, floating chip, or notification peeks from one screen into the next as a secondary visual, not the whole message.
- Related ideas form a pair: problem → solution, before → after, overview → detail, plan → result.
- The seam passes through negative space, a soft shadow, a simple object body, or a non-critical decorative area.
Bad cross-screen patterns:
- Splitting headlines, app names, prices, legal text, ratings, CTAs, or critical UI across a seam.
- Centering one giant phone on the seam so each crop shows only a half-device and no clear benefit.
- Using cross-screen movement on every slide; it becomes a gimmick and makes the deck harder to scan.
- Cutting through faces, mascot eyes, key chart numbers, product claims, or app-store-required information.
- Requiring the viewer to understand the carousel as one uninterrupted poster. Every exported PNG must still pass the one-second standalone test.
- Letting shadows, stickers, or partial objects look accidentally clipped. If it crosses a boundary, make the bleed deliberate with scale, shadow, rotation, or continuation.
Placement rules:
- Use adjacent screens only unless a deliberate 3-screen panorama is the entire concept.
- Keep all text fully inside a single exported screen with safe margins.
- Let 10-30% of a non-critical visual cross the seam; go beyond 40% only for backgrounds, paths, or abstract decoration.
- If adjacent screens have different background colors, bridge them with a shared object, matching shadow direction, or a designed transition band.
- Review both views: the zoomed-out connected canvas must look cohesive, and each individual export must still sell one idea.
Visual Design Principles
These rules are derived from studying the best app store screenshots in the wild (Superlist, Headspace, CRED, (Not Boring) Camera, Arc Search, Linktree, Gentler Streak, etc.). They apply regardless of which style preset the user picks. Style-specific tokens (fonts, palette, accents) live in style-prompts.md — point the user there.
1. The background is a designed surface — never white
Plain white is the amateur tell. Every great deck uses a deliberate surface: a saturated color block, a warm cream/off-white (#F4F1EC-ish), a dark navy/near-black, or a gradient. The background can shift per slide (Headspace, Linktree do this), but it must read as intentional, not default.
2. Headlines dominate
The headline occupies roughly the top 30–40% of the canvas — much bigger than a typical web hero. If a person can't read it at thumbnail size with no zoom, redesign.
3. Mixed emphasis inside the headline
Almost every great headline has one word styled differently from the rest — a contrast color, an italic script, a heavier weight, or a hand-drawn underline. Examples:
- Superlist: "The one app that fits your whole day" (script + coral)
- Headspace: "Stress less" (
less orange against black)
- Arc Search: "Fastest way to search. Cleanest way to browse." (purple / navy)
Flat single-color headlines look weaker. Pick one emphasis word per slide.
4. Decorative accents are the rule, not the exception
Top decks layer at least one of these on most slides:
- Hand-drawn squiggles, arrows, scribbles (Superlist)
- Sparkles / glow (Gentler Streak, Arc)
- Label badges on the visual ("SUPER RAW", "Cinematic", "LUT")
- Floating widget chips with real stats ("$3,630 earned", "11,175 steps") — these tell the story without copy
- Award lockups on the hero only (Apple Design Award, Webby, star count)
A bare phone on a bare bg with a bare headline is the default-skill output. Add one accent.
5. Phone framing is a deliberate choice — vary it across the deck
Three common framings, each carries a different feeling:
- Bezelless / minimal frame — maximizes UI legibility, modern (Arc, Linktree, Gentler)
- Tilted floating phone with soft shadow — product / advertorial feel (Superlist, CRED hero)
- Full device with visible bezel, dead-center — editorial, premium (CRED, NB Camera)
Mix at least two framings across the deck.
6. Proof anchors the hero, nothing else
Award badges, press quotes, star counts, install counts — concentrate them on slide 1 only. Spreading them dilutes both the proof and the rest of the slides. NB Camera does this perfectly: Verge quote + Apple Design Award + 15,000+ stars all on the cover, none after.
7. Density inside the phone, sparsity outside
The screenshot inside the phone can (and should) be a real, dense product capture — actual lists, dashboards, charts, conversations. The space outside the phone is the opposite: one headline, one visual, one optional sub-line, one optional badge. Don't add bullet lists, multi-line paragraphs, or competing logos around the device.
8. Break the phone parade
Every 2–3 slides, drop the phone and use a different hero element to keep visual rhythm:
- 3D rendered product object (NB Camera's stylized camera)
- Photographic still (NB Camera slide 2)
- Real human / lifestyle photo (Linktree)
- Mascot illustration (Headspace's mascot, Gentler Streak's character)
- Typographic feature wall (Superlist's last slide)
- Phone grid mosaic (Linktree's "Trusted by 70M+" final slide)
9. Last slide pattern
The closer is almost always one of two things:
- Feature wall — a vertical list of one-word f
…(truncated)
1---2name: app-store-screenshots3description: Use when building App Store or Google Play screenshot pages, generating exportable marketing screenshots for iOS, macOS, and/or Android apps, or scaffolding a screenshot editor with Next.js. Triggers on app store, mac app store, play store, screenshots, marketing assets, html-to-image, phone mockup, mac mockup, android screenshots, feature graphic.4metadata:5 source: https://skillmd.com/skills/parthjadhav/app-store-screenshots6---78# App Store & Google Play Screenshots Generator910## Overview1112Scaffold a pre-built Next.js + ShadCN editor that lets the user design and export App Store **and** Google Play screenshots as **advertisements** (not UI showcases). The editor handles all the heavy lifting:1314- Connected live preview at the canvas's true resolution (scaled to fit)15- Drag-to-reorder screens, inline text editing, layout switcher per screen16- Cross-screen mockups: phone/device frames, captions, and layered elements can be moved across adjacent screens, then exported as clipped crops17- Drop-target screenshot picker (file → saved to `public/screenshots/uploaded/<hash>.png`)18- Auto-save to **`app-store-screenshots.json`** at the project root (git-trackable) + `localStorage` mirror19- Easy iOS ↔ Mac ↔ Android platform switch — separate slide decks live side by side20- One-click bulk PNG export at every Apple/Google-required resolution via `html-to-image`21- Light/dark variant toggle per slide, a toolbar theme picker (one palette preset per named style), locale select22- A **Copy ideas** menu next to the headline field with formulas for hero, differentiator, feature, proof, and closer slides23- Per-slide custom background colors (caption colours stay readable automatically), a live screenshot font menu, and importing licensed WOFF2/WOFF/TTF/OTF fonts24- Image overlay elements (logos, badges, photos) with drag/resize/rotation/layering controls and directional edge fades25- Toolbar Undo/Redo (`⌘Z` / `⇧⌘Z`) over the last 50 edits of the session26- Guided in-place migration for older projects created by this skill; passive and explicit migrations keep legacy decks isolated until the user intentionally opts into connected canvas2728Supported devices out of the box:29- **iPhone** (portrait) — Apple App Store30- **iPad** (portrait) — Apple App Store31- **iPhone Duo** outer and inner displays, each in portrait and landscape (four decks) — Apple App Store; see "iPhone Duo" under Step 532- **App Store creative assets**: universal 5244×2950, product page header 3840×1646, search results 3:2 — Apple App Store; see "App Store creative assets" under Step 533- **Apple TV** (landscape, 4K + HD) — Apple App Store34- **Apple Watch** (portrait, every Ultra/Series size) — Apple App Store35- **CarPlay** (landscape head unit) — uploaded into the **iPhone** slot; see "Apple TV, Apple Watch and CarPlay" under Step 536- **Mac** (16:10 landscape, own **Mac** tab) — Mac App Store (`2880×1800`, `2560×1600`, `1440×900`, `1280×800`); see "Mac" under Step 537- **Android Phone** (portrait) — Google Play38- **Android Tablet 7"** (portrait + landscape) — Google Play39- **Android Tablet 10"** (portrait + landscape) — Google Play40- **Feature Graphic** (1024×500 banner) — Google Play store listing header4142## Core Principle4344**Screenshots are advertisements, not documentation.** Every screenshot sells one idea. If you're showing UI, you're doing it wrong — you're selling a *feeling*, an *outcome*, or killing a *pain point*. Use this skill's interactive editor to iterate on copy and layout fast; do not hand-craft the page from scratch.4546## What This Skill Does47481. **Copies a pre-built template** from `template/` (co-located with this `SKILL.md`) into the user's working directory.492. Installs dependencies with the user's package manager.503. Drops the user's screenshots into `public/screenshots/...` and their app icon into `public/`.514. (Optionally) prefills `app-store-screenshots.json` with the user's app name, starting copy, screenshots, and connected-canvas preference so the first preview is meaningful.525. Starts the dev server and tells the user to open the editor in the browser.5354You should NOT write `page.tsx`, device frames, or export logic by hand. They live in the template.5556## Step 0: Probe for Existing Screenshot Projects5758Before asking the new-project questions in Step 1, always inspect the current working directory for an existing app-store-screenshots implementation.5960Run lightweight probes:6162```bash63test -f package.json && sed -n '1,220p' package.json64test -f app-store-screenshots.json && sed -n '1,120p' app-store-screenshots.json65rg -n "app-store-screenshots|html-to-image|toPng|ScreenshotEditor|DeckCanvas|connectedCanvas|EXPORT_SIZES|mockup.png|PHONE_SCREEN" package.json src app public 2>/dev/null66find public -maxdepth 4 \( -path "*/screenshots*" -o -name "mockup.png" -o -name "app-icon.png" \) -print 2>/dev/null67```6869Treat the project as an older implementation when any of these are true:7071- `app-store-screenshots.json` exists but has no `schemaVersion`, has `schemaVersion < 2`, or lacks `connectedCanvas`.72- `src/components/editor/screenshot-editor.tsx` exists but the editor does not reference `DeckCanvas` or `connectedCanvas`.73- `src/app/page.tsx` contains a previous all-in-one generator (`html-to-image`, `toPng`, `EXPORT_SIZES`, `PHONE_SCREEN`, hardcoded slide arrays/themes).74- The repo contains the old screenshot asset layout (`public/mockup.png`, `public/screenshots...`) plus a screenshot generator package setup.7576If an older implementation is detected, ask exactly one question before doing anything else:7778> I found an older App Store screenshots project here. Do you want me to migrate this existing project to the new connected-canvas editor?79>80> 1. Yes — migrate the existing project to the new editor81> 2. No — set up or modify a project another way8283If the user chooses **Yes**, do **not** ask the Step 1 questionnaire. Run the migration path below using the files already in the repo. If the user chooses **No**, continue to Step 1.8485### Migration Path (When User Says Yes)8687The goal is an in-place UI/template upgrade, not a redesign. Preserve the user's existing app name, copy, screenshot paths, app icon, uploaded assets, locales, and device decks wherever they already exist. Replace the old UI implementation with the current template. Keep legacy decks in isolated export mode unless the project already explicitly opted into connected canvas.8889Migration rules:90911. **Do not ask further product/design questions.** The user already has a project. Infer from existing files and report any non-blocking gaps at the end.922. **Never delete user assets.** Preserve `public/screenshots/`, `public/app-icon.png`, uploaded screenshots, and any existing `app-store-screenshots.json`.933. **Preserve recoverability.** If the worktree is not clean, do not revert unrelated changes. Before overwriting template files, copy replaced project-state/assets/code snapshots to a temporary backup outside the repo (for example `/tmp/app-store-screenshots-migration-<timestamp>/`) and mention the path in the final response.944. **Prefer structured migration.** Read and write `app-store-screenshots.json` with JSON tooling. Do not regex-edit JSON.955. **Set `schemaVersion: 2` and keep legacy `connectedCanvas` safe.** If the existing project already has an explicit boolean `connectedCanvas`, preserve it. If the project is pre-v2 or lacks the flag, write `"connectedCanvas": false` so offscreen/clipped legacy mockups do not leak into neighboring exports. New projects still default to connected canvas.966. **Keep screenshots pointed at existing files.** Do not rename screenshot files unless the old project already depended on numeric names and the migration needs them. Existing static paths are fine.977. **Handle custom themes without asking.** If the old project references a custom `themeId`, merge the matching theme object into the new `src/lib/constants.ts` when it can be found. If it cannot be recovered, leave the `themeId` in project JSON; the editor will fall back to `clean-light` and warn, and you should note that a custom theme needs manual restoration.988. **Merge package metadata when possible.** The template's dependencies and scripts must win for the screenshot editor, but preserve unrelated existing `dependencies`, `devDependencies`, and useful scripts unless they directly conflict.999. **Do not import template sample decks into real migrations.** If the old project already has decks or screenshots, use the template for UI/code only. Keep template sample screenshots/decks out of the migrated project so the user's app does not inherit unrelated example content.10010. **Use a disposable copy for dogfooding.** If the user asks to test or review the migration instead of actually migrating their project, copy the app to a temp directory or worktree and run the migration there. Only touch the real checkout when the user explicitly asks for the real migration and answers **Yes**.101102Recommended migration sequence:103104```bash105# 1. Snapshot useful old files outside the repo.106STAMP=$(date +%Y%m%d-%H%M%S)107BACKUP_DIR="/tmp/app-store-screenshots-migration-$STAMP"108mkdir -p "$BACKUP_DIR"109cp -R app-store-screenshots.json public src package.json tailwind.config.ts next.config.mjs "$BACKUP_DIR/" 2>/dev/null || true110111# 2. Preserve project state and assets that must survive template copy.112PRESERVE_DIR="$BACKUP_DIR/preserve"113mkdir -p "$PRESERVE_DIR"114cp app-store-screenshots.json "$PRESERVE_DIR/" 2>/dev/null || true115cp -R public/screenshots "$PRESERVE_DIR/screenshots" 2>/dev/null || true116cp public/app-icon.png "$PRESERVE_DIR/app-icon.png" 2>/dev/null || true117118# 3. Copy the current template over the old UI implementation.119cp -R "<SKILL_DIR>/template/." "$PWD/"120cp app-store-screenshots.json "$BACKUP_DIR/template-app-store-screenshots.json" 2>/dev/null || true121122# 4. Restore preserved user state/assets over template samples.123cp "$PRESERVE_DIR/app-store-screenshots.json" app-store-screenshots.json 2>/dev/null || true124mkdir -p public125if [ -d "$PRESERVE_DIR/screenshots" ]; then126 mkdir -p "$BACKUP_DIR/template-samples/public"127 mv public/screenshots "$BACKUP_DIR/template-samples/public/screenshots" 2>/dev/null || true128 cp -R "$PRESERVE_DIR/screenshots" public/screenshots129else130 mkdir -p public/screenshots131fi132cp "$PRESERVE_DIR/app-icon.png" public/app-icon.png 2>/dev/null || true133```134135After copying, upgrade or create `app-store-screenshots.json`. If an existing project file exists, coerce it in place. If no project file exists but old slide data is embedded in `src/lib/defaults.ts` or `src/app/page.tsx`, extract it best-effort into the template's project JSON before falling back to starter slides. Prefer old arrays or objects named `slides`, `screens`, `features`, `defaultSlides`, `appName`, `tagline`, `theme`, and screenshot paths. If the old implementation only has image files, sort `public/screenshots/**` by path and seed slides from those files.136137Use a small JSON script like this for the final project-state coercion:138139```bash140BACKUP_DIR="$BACKUP_DIR" node <<'NODE'141const fs = require("fs");142const path = require("path");143144const PROJECT_FILE = "app-store-screenshots.json";145const DEFAULT_LOCALE = "en";146const DEVICE_KEYS = ["iphone", "ipad", "duo-outer", "duo-outer-landscape", "duo-inner", "duo-inner-landscape", "tvos", "watchos", "carplay", "mac", "android", "android-7", "android-10", "feature-graphic", "creative-universal", "creative-header", "creative-search"];147const LAYOUTS = ["hero", "device-bottom", "device-top", "two-devices", "no-device", "split-landscape", "feature-graphic"];148149function readJson(file) {150 try {151 return JSON.parse(fs.readFileSync(file, "utf8"));152 } catch {153 return null;154 }155}156157const templateState =158 readJson(path.join(process.env.BACKUP_DIR || "", "template-app-store-screenshots.json")) ||159 readJson(PROJECT_FILE) ||160 {};161const existingState = readJson(PROJECT_FILE) || {};162const hasExplicitConnectedCanvas = typeof existingState.connectedCanvas === "boolean";163const existingDecks =164 existingState.slidesByDevice && typeof existingState.slidesByDevice === "object" && !Array.isArray(existingState.slidesByDevice)165 ? existingState.slidesByDevice166 : {};167const hasExistingDecks = Object.keys(existingDecks).length > 0;168const state = {169 ...templateState,170 ...existingState,171 slidesByDevice: hasExistingDecks ? existingDecks : templateState.slidesByDevice || {},172};173174const legacySlides =175 Array.isArray(existingState.slides) ? existingState.slides :176 Array.isArray(existingState.screens) ? existingState.screens :177 Array.isArray(existingState.features) ? existingState.features :178 null;179180if (legacySlides && !hasExistingDecks) {181 state.slidesByDevice = {182 iphone: legacySlides,183 };184}185186function localized(value) {187 if (typeof value === "string") return { [DEFAULT_LOCALE]: value };188 if (value && typeof value === "object" && !Array.isArray(value)) {189 return Object.fromEntries(Object.entries(value).filter(([, text]) => typeof text === "string"));190 }191 return {};192}193194function cleanTransform(value) {195 if (!value || typeof value !== "object") return undefined;196 const { x, y, width, height, rotation, zIndex } = value;197 if (![x, y, width, height].every((n) => typeof n === "number" && Number.isFinite(n))) return undefined;198 return {199 x,200 y,201 width: Math.max(1, width),202 height: Math.max(1, height),203 ...(typeof rotation === "number" && Number.isFinite(rotation) ? { rotation } : {}),204 ...(typeof zIndex === "number" && Number.isFinite(zIndex) ? { zIndex } : {}),205 };206}207208function firstString(...values) {209 return values.find((value) => typeof value === "string") || "";210}211212// The editor refuses to load a deck with empty or repeated screen ids.213function uniqueId(value, used) {214 let id = typeof value === "string" && value.trim() ? value : "";215 while (!id || used.has(id)) id = `migrated-${Math.random().toString(36).slice(2, 10)}`;216 used.add(id);217 return id;218}219220function migrateSlide(slide, used) {221 if (!slide || typeof slide !== "object" || Array.isArray(slide)) return null;222 const transforms = {};223 const rawTransforms = slide.transforms && typeof slide.transforms === "object" ? slide.transforms : {};224 for (const [id, transform] of Object.entries(rawTransforms)) {225 const cleaned = cleanTransform(transform);226 if (["caption", "device", "deviceSecondary", "callout"].includes(id) && cleaned) transforms[id] = cleaned;227 }228 const textIds = new Set();229 const textElements = Array.isArray(slide.textElements)230 ? slide.textElements231 .map((element) => {232 if (!element || typeof element !== "object" || Array.isArray(element)) return null;233 const transform = cleanTransform(element.transform);234 if (!transform) return null;235 return {236 ...element,237 id: uniqueId(element.id, textIds),238 text: localized(element.text),239 transform,240 fontSize: Number.isFinite(element.fontSize) && element.fontSize > 0 ? element.fontSize : undefined,241 fontWeight: Number.isFinite(element.fontWeight) && element.fontWeight > 0 ? element.fontWeight : undefined,242 };243 })244 .filter(Boolean)245 : undefined;246247 const imageIds = new Set();248 const imageElements = Array.isArray(slide.imageElements)249 ? slide.imageElements.map((element) => {250 if (!element || typeof element !== "object" || Array.isArray(element) || typeof element.src !== "string") return null;251 const transform = cleanTransform(element.transform);252 return transform ? { ...element, id: uniqueId(element.id, imageIds), transform } : null;253 }).filter(Boolean)254 : undefined;255256 return {257 ...slide,258 id: uniqueId(slide.id, used),259 layout: LAYOUTS.includes(slide.layout) ? slide.layout : "device-bottom",260 label: localized(slide.label),261 headline: localized(slide.headline || slide.title || slide.caption || slide.copy),262 screenshot: firstString(slide.screenshot, slide.image, slide.src, slide.path),263 screenshotSecondary: typeof slide.screenshotSecondary === "string" ? slide.screenshotSecondary : undefined,264 inverted: typeof slide.inverted === "boolean" ? slide.inverted : undefined,265 ...(Object.keys(transforms).length ? { transforms } : { transforms: undefined }),266 ...(textElements && textElements.length ? { textElements } : { textElements: undefined }),267 ...(imageElements && imageElements.length ? { imageElements } : { imageElements: undefined }),268 // The editor clamps magnifier values on load; only a non-object would be rejected.269 callout: slide.callout && typeof slide.callout === "object" && !Array.isArray(slide.callout) ? slide.callout : undefined,270 };271}272273state.schemaVersion = 2;274state.connectedCanvas = hasExplicitConnectedCanvas ? existingState.connectedCanvas : false;275// Unique codes like "en", "pt-BR", "zh_Hans"; anything else makes the editor refuse the file.276const LOCALE_CODE = /^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/;277state.locales = Array.isArray(state.locales)278 ? [...new Set(state.locales.filter((locale) => typeof locale === "string" && LOCALE_CODE.test(locale)))]279 : [];280if (!state.locales.length) state.locales = [DEFAULT_LOCALE];281state.locale = state.locales.includes(state.locale) ? state.locale : state.locales[0];282state.device = DEVICE_KEYS.includes(state.device) ? state.device : "iphone";283if (state.orientation !== "portrait" && state.orientation !== "landscape") delete state.orientation;284for (const key of ["appName", "themeId", "appIcon"]) {285 if (state[key] !== undefined && typeof state[key] !== "string") delete state[key];286}287// The feature graphic only shows an icon that `appIcon` points at.288if (!state.appIcon && fs.existsSync(path.join("public", "app-icon.png"))) state.appIcon = "/app-icon.png";289290if (state.slidesByDevice && typeof state.slidesByDevice === "object") {291 for (const [device, slides] of Object.entries(state.slidesByDevice)) {292 // The editor only accepts known device decks; the backup keeps the original.293 if (!DEVICE_KEYS.includes(device)) {294 delete state.slidesByDevice[device];295 continue;296 }297 const used = new Set();298 state.slidesByDevice[device] = Array.isArray(slides) ? slides.map((slide) => migrateSlide(slide, used)).filter(Boolean) : [];299 }300}301302if (!state.slidesByDevice[state.device]) {303 const firstDeviceWithSlides = DEVICE_KEYS.find((device) => state.slidesByDevice[device]?.length);304 if (firstDeviceWithSlides) state.device = firstDeviceWithSlides;305}306307fs.writeFileSync(PROJECT_FILE, JSON.stringify(state, null, 2) + "\n");308NODE309```310311If `package.json` existed before the template copy, merge it after the project-state coercion instead of leaving a blind overwrite. Keep the template's `dev`, `build`, and `start` scripts and all editor dependencies, then add any old non-conflicting scripts and dependencies from the backed-up `package.json`.312313```bash314BACKUP_DIR="$BACKUP_DIR" node <<'NODE'315const fs = require("fs");316const path = require("path");317318function readJson(file) {319 try {320 return JSON.parse(fs.readFileSync(file, "utf8"));321 } catch {322 return null;323 }324}325326const oldPkg = readJson(path.join(process.env.BACKUP_DIR || "", "package.json"));327const templatePkg = readJson("package.json");328329if (oldPkg && templatePkg) {330 const merged = {331 ...oldPkg,332 ...templatePkg,333 scripts: {334 ...(oldPkg.scripts || {}),335 ...(templatePkg.scripts || {}),336 },337 dependencies: {338 ...(oldPkg.dependencies || {}),339 ...(templatePkg.dependencies || {}),340 },341 devDependencies: {342 ...(oldPkg.devDependencies || {}),343 ...(templatePkg.devDependencies || {}),344 },345 };346347 fs.writeFileSync("package.json", JSON.stringify(merged, null, 2) + "\n");348}349NODE350```351352Then install/update dependencies and verify:353354```bash355bun install # or pnpm install / yarn / npm install356set -o pipefail357bun run build 2>&1 | tee "$BACKUP_DIR/build.log" # or the detected package-manager equivalent358```359360Start the dev server and verify in the browser:361362- The toolbar shows **Isolated** for migrated pre-v2 decks, unless the project file already explicitly had `"connectedCanvas": true`.363- Existing screens, copy, screenshot paths, and app icon are present.364- Referenced screenshot files exist for every configured locale, or the final report lists the missing paths.365- Device decks retained from the old project do not silently become template placeholders. If a retained deck has empty screenshots or lacks active-locale copy, report it as a follow-up instead of removing it.366- A bundle export succeeds for the active device.367- `app-store-screenshots.json` contains `"schemaVersion": 2` and a boolean `"connectedCanvas"` value.368369## Step 1: Gather Input (Before Scaffolding)370371Ask the user these. Do not proceed until you have answers:372373### Required3743751. **App screenshots** — "Do you already have screenshots of the devices?"376 - If **yes**: ask "Where are your app screenshots? (PNG files of actual device captures)" and proceed.377 - If **no** and the app is **iOS + Swift**: offer the companion capture skill — "Want to capture them automatically with the `ios-marketing-capture` skill (https://github.com/ParthJadhav/ios-marketing-capture)?" If they say yes, install it with:378 ```bash379 npx skills add ParthJadhav/ios-marketing-capture380 ```381 Then have them run that skill first to generate the screenshots before continuing here.382 - If **no** and the app is **not iOS + Swift** (e.g. Android, React Native, Flutter, web): the capture skill won't work — the user needs to capture screenshots manually (simulator/device screenshots) before continuing.3832. **App icon** — "Where is your app icon PNG?"3843. **App name** — "What's the app called?"3854. **Feature list** — "List your app's features in priority order. What's the #1 thing your app does?"3865. **Style direction** — "What style do you want? You can either (a) pick one of the named deep-spec styles, or (b) describe your own vibe in your own words (warm/organic, dark/moody, clean/minimal, bold/colorful, plus any reference apps you like) and I'll build a custom palette. The template also ships with palette presets in the toolbar theme picker: the generic `clean-light`, `dark-bold`, `warm-editorial`, `ocean-fresh`, and `bloom-roast`, plus one preset per named style (same id as the style slug). The named deep specs live in `style-prompts/` — see `style-prompts.md` for the full index. Currently available: Retro Rubberhose Mascot, Moody Curated Dating, Paper Sticker Skeuomorphic, Dreamy Pastel Couples, Hand-Drawn Editorial Tasks, Glossy 3D K-Beauty Creator, Liquid Glass Aurora, Swiss Grid Bold, Neon Athletic Night, Magazine Cover Editorial, Candy Pop Social, Soft Clay Wellness, Midnight Glow Pro, Risograph Zine, Bento Keynote Grid, Toybox Primary, Quiet Japandi, Vintage Travel Poster. If the user names one of these — or describes something that clearly matches one — read `style-prompts/_QUALITY_BAR.md` first, then the matching deep spec file, and apply its entire spec (palette, gradients, shadows, rotations, per-slide breakdown). If the user describes a fully custom style, fall back to the General Visual Design Principles below and pick the closest deep spec as a starting reference."387388### Optional3893906. **Target stores** — Apple App Store, Mac App Store, Google Play, or a mix? Determines which platform decks to seed.3917. **iPad / Mac / Android tablet screenshots** — If yes, what sizes and orientations?3928. **Apple TV / Apple Watch / CarPlay** — Does the app have a tvOS or watchOS app, or CarPlay support? Each gets its own deck.3939. **iPhone Duo** — Does the app support iPhone Duo? If so, read `iphone-duo.md` before planning. Ask: what does the app do differently on the inner display (two panes, sidebar, wider grid)? Which displays and orientations have their own layout? Do they have simulator captures from each display at the exact sizes? Apple's bezels for iPhone, iPad, Watch, TV, Mac and iPhone Duo ship with the template (see Step 5); ask only whether they want a different finish.39410. **App Store creative assets** — Do they want a product page header / search results asset? Default to one universal 5244×2950 asset; offer separate header and search assets only if they need different compositions. Ask for a short brand line for each; don't reuse a screenshot headline.39511. **Feature Graphic** — Want a 1024×500 Play Store banner too?39612. **Localized screenshots** — Languages? (e.g. en, de, es, pt, ja, ar, he)39713. **Number of slides** — Apple allows up to 10, Google Play up to 8.39814. **Brand colors / font** — If they want a custom theme beyond the built-in presets.39915. **Additional instructions** — Anything specific.400401**IMPORTANT:** If the user gives instructions at any point, follow them. They override skill defaults.402403## Step 2: Scaffold the Template404405### Detect Package Manager406407Priority: **bun > pnpm > yarn > npm**.408409```bash410which bun && echo bun || which pnpm && echo pnpm || which yarn && echo yarn || echo npm411```412413### Copy the Template414415The template lives at `<this skill dir>/template/` — when the skill is installed, the whole folder is already on disk. Copy its contents (NOT the folder itself) into the user's working directory. The trailing `/.` copies dotfiles like `.gitignore` too.416417```bash418# Replace <SKILL_DIR> with the absolute path to this skill (the directory containing SKILL.md).419cp -R "<SKILL_DIR>/template/." "$PWD/"420```421422If the target directory already has a `package.json`, ask the user before overwriting during a new scaffold. If Step 0 detected an old implementation and the user chose **Yes**, do not ask this again; follow the migration path, preserve recoverability with the backup directory, and merge package metadata after the template copy.423424### Install Dependencies425426```bash427bun install # or pnpm install / yarn / npm install428```429430### Drop the User's Assets431432Move the user's screenshots into the layout the template expects:433434```435public/436├── app-icon.png # ← user's app icon437├── mockup.png # ← already copied by the template (iPhone bezel)438├── frames/ # ← Apple's bezels, bundled; swap a file to change finish (Step 5)439└── screenshots/440 ├── apple/441 │ ├── iphone/{locale}/01.png … N.png442 │ ├── ipad/{locale}/01.png … N.png443 │ ├── duo-outer/{portrait|landscape}/{locale}/01.png … N.png # iPhone Duo outer display444 │ ├── duo-inner/{portrait|landscape}/{locale}/01.png … N.png # iPhone Duo inner display445 │ ├── tvos/{locale}/01.png … N.png # Apple TV, 16:9446 │ ├── watchos/{locale}/01.png … N.png # Apple Watch447 │ ├── carplay/{locale}/01.png … N.png # CarPlay head-unit captures448 │ └── mac/{locale}/01.png … N.png # Mac, 16:10449 └── android/450 ├── phone/{locale}/01.png … N.png451 ├── tablet-7/{portrait|landscape}/{locale}/...452 └── tablet-10/{portrait|landscape}/{locale}/...453```454455The starter project state lives in `app-store-screenshots.json`, not `src/lib/defaults.ts`. If the user names their screenshots differently, either rename them or update the relevant slide `screenshot` fields in `app-store-screenshots.json` so the initial deck points at the right files. The user can also drag-drop files directly into the editor at runtime — those uploads are written to `public/screenshots/uploaded/<hash>.png` when the dev server is running.456457### (Optional) Seed Initial Copy458459If the user provided headlines, edit `app-store-screenshots.json` to set:460- `appName`461- `themeId` (one of `"clean-light" | "dark-bold" | "warm-editorial" | "ocean-fresh" | "bloom-roast"`, a named style slug such as `"swiss-grid-bold"` when the user picked that style, or add a matching entry to `THEMES` in `src/lib/constants.ts`). Themes may set `accentAlt` for the label color on inverted slides.462- `appIcon` — public path of the app icon (e.g. `"/app-icon.png"` after copying it to `public/app-icon.png`). The Play Store feature graphic shows it; blank uses the app's initial. The icon can also be picked in the feature-graphic inspector.463- `connectedCanvas` (`true` for new connected decks; migrated legacy decks should stay `false` until the user opts in)464- Starter slides per device with the user's `label` + `headline` + screenshot paths465- Optional `scene` for the whole project: `{ backdrop: "gradient"|"solid"|"aurora"|"spotlight"|"grid"|"dots"|"lines", span: boolean, decoration: "blobs"|"rings"|"sparkles"|"none", shadow: 0–100, glow: 0–100, tilt: -30–30, headlineWeight: 300–900, headlineCase: "as-typed"|"upper", captionAlign: "auto"|"left"|"center" }`. Omit it for the classic gradient + blobs look. Pick values that match the chosen style (e.g. `spotlight` + glow for dark pro styles, `lines` + left-aligned serif for editorial); the user can refine it in the editor's **Scene** popover or compare whole looks in **Style Lab**.466- Optional per-slide `callout: { focusX: 0–1, focusY: 0–1, zoom: 1.5–5, shape: "circle"|"rounded" }` to magnify one detail of the primary screenshot. It sits over the device's upper right unless `transforms.callout` places it.467- Optional per-slide `typography: { labelScale, headlineScale, appNameScale }` (0.5–2, default 1) when one headline is much longer or shorter than the rest of the deck. `appNameScale` only applies to the feature graphic, where `headlineScale` sizes the tagline.468469Otherwise, leave the defaults — the user can rewrite copy in the editor.470471### Start the Dev Server472473```bash474bun dev # → http://localhost:3000475```476477Tell the user to open the URL and start editing. The editor auto-saves to **`app-store-screenshots.json`** at the project root (plus a `localStorage` mirror for instant paint). Uploaded screenshots land in `public/screenshots/uploaded/<hash>.png`. Both are git-trackable — committing them means another machine can `git clone` and resume the exact deck.478479## Step 3: Coach the User on Copy480481Inside the editor the user will write headlines themselves, but they often need guidance. Apply these rules when reviewing their copy or generating suggestions.482483**Read [`copy-ideas.md`](./copy-ideas.md) before drafting headlines.** It has formulas per deck slot (hero, differentiator, feature, proof, closer), ready lines for 13 app categories, eyebrow labels, a weak-to-better table, four deck arcs, and localization notes. When you propose copy, give three options per slide (paint a moment / state an outcome / kill a pain), then rewrite the chosen one in the selected style's voice. The editor's inspector has a matching **Copy ideas** menu next to the headline field (`src/lib/copy-ideas.ts`) so users can drop in a formula and replace the bracketed words themselves.484485### The Iron Rules4864871. **One idea per headline.** Never join two things with "and."4882. **Short, common words.** 1-2 syllables. No jargon unless it's domain-specific.4893. **3-5 words per line.** Must be readable at thumbnail size in the App Store.4904. **Line breaks are intentional.** Newlines in the textarea map directly to visible breaks.491492### Three Approaches493494| Type | What it does | Example |495|------|-------------|---------|496| **Paint a moment** | You picture yourself doing it | "Check your coffee without opening the app." |497| **State an outcome** | What your life looks like after | "A home for every coffee you buy." |498| **Kill a pain** | Name a problem and destroy it | "Never waste a great bag of coffee." |499500### Bad-to-Better501502| Weak | Better | Why |503|------|--------|-----|504| Track habits and stay motivated | Keep your streak alive | one idea, faster to parse |505| Organize tasks with AI summaries | Turn notes into next steps | outcome-first, less jargon |506| Save recipes with tags and favorites | Find dinner fast | sells the benefit, not the UI |507508### Narrative Arc509510The user's slide deck should follow a rough arc (skip slots that don't fit):511512| Slot | Purpose |513|------|---------|514| #1 | **Hero / Main Benefit** — the ONLY slide most people see |515| #2 | **Differentiator** — what makes the app unique |516| #3 | **Ecosystem** — widgets, watch, extensions (skip if N/A) |517| #4+ | **Core Features** — one per slide, most important first |518| 2nd-to-last | **Trust Signal** — "made for people who [X]" |519| Last | **More Features** — pills listing extras (skip if few features) |520521### Layout Variation522523Vary the `layout` field across slides. The editor exposes:524- `hero` — centered headline + bottom-anchored device525- `device-bottom` — same composition, smaller headline526- `device-top` — flipped, device above caption (good contrast slide)527- `two-devices` — back + front phones layered528- `no-device` — big standalone headline (use sparingly)529- `split-landscape` — caption left + device right (tablet landscape and Mac)530- `feature-graphic` — Play Store banner (1024×500)531532Never repeat the same layout twice in a row. Use 1-2 `inverted` (dark) slides for visual rhythm.533534On iPhone Duo decks, `two-devices` is **Folded + open**: the second device is the same phone in its other state, drawn in that display's frame. Its second capture comes from the other display.535536### iPhone Duo Decks537538Read `iphone-duo.md` before writing copy or seeding any Duo deck. It holds the research behind these rules, with sources. In short:539540- **Plan the decks.** Lead with the inner display in landscape when the app has a real two-pane or wide layout. Add an outer portrait deck next. Make inner portrait and outer landscape decks only if the app has a real layout for them.541- **Show what only an open Duo can do.** At least one inner screenshot shows two panes, a sidebar or a wider grid. A screenshot that could have come from a normal iPhone wastes the Duo slot.542- **Inner landscape:** a one-line headline (about 30 characters or fewer) across the top, with the device large below. Pick captures where one pane's action and the other pane's result sit in the same frame.543- **Outer portrait:** a short, wide canvas. Use two short lines at most, large type, and one idea per screen. It can reuse the iPhone story.544- **Folded + open** once per deck, for the moment that carries across states ("Close it for today. Open it for the week.").545- **Write copy about what the user gets, not the hardware.** Localise every deck and check that each landscape line still fits on one line.546547### Cross-Screen / Cross-Canvas Composition548549Use the connected canvas as a design tool during Step 3, after the narrative arc and layout rhythm are chosen and before final export. For most decks with **5+ slides**, plan **one** tasteful cross-screen moment by default. For 8-10 slide decks, use at most **two**. For short, formal, or compliance-heavy decks, zero is fine. The goal is "these screenshots belong together," not "one giant poster chopped into pieces."550551Good cross-screen patterns:552- An oversized phone, tablet, or screenshot mosaic bridges two adjacent screens by 10-30% of its width, while each exported crop still reads as a complete ad.553- A background horizon, photo, gradient, doodle path, waveform, starfield, sticker trail, or map route continues across the seam.554- A mascot, 3D object, floating chip, or notification peeks from one screen into the next as a secondary visual, not the whole message.555- Related ideas form a pair: problem → solution, before → after, overview → detail, plan → result.556- The seam passes through negative space, a soft shadow, a simple object body, or a non-critical decorative area.557558Bad cross-screen patterns:559- Splitting headlines, app names, prices, legal text, ratings, CTAs, or critical UI across a seam.560- Centering one giant phone on the seam so each crop shows only a half-device and no clear benefit.561- Using cross-screen movement on every slide; it becomes a gimmick and makes the deck harder to scan.562- Cutting through faces, mascot eyes, key chart numbers, product claims, or app-store-required information.563- Requiring the viewer to understand the carousel as one uninterrupted poster. Every exported PNG must still pass the one-second standalone test.564- Letting shadows, stickers, or partial objects look accidentally clipped. If it crosses a boundary, make the bleed deliberate with scale, shadow, rotation, or continuation.565566Placement rules:567- Use adjacent screens only unless a deliberate 3-screen panorama is the entire concept.568- Keep all text fully inside a single exported screen with safe margins.569- Let 10-30% of a non-critical visual cross the seam; go beyond 40% only for backgrounds, paths, or abstract decoration.570- If adjacent screens have different background colors, bridge them with a shared object, matching shadow direction, or a designed transition band.571- Review both views: the zoomed-out connected canvas must look cohesive, and each individual export must still sell one idea.572573## Visual Design Principles574575These rules are derived from studying the best app store screenshots in the wild (Superlist, Headspace, CRED, (Not Boring) Camera, Arc Search, Linktree, Gentler Streak, etc.). They apply regardless of which style preset the user picks. Style-specific tokens (fonts, palette, accents) live in `style-prompts.md` — point the user there.576577### 1. The background is a designed surface — never white578579Plain white is the amateur tell. Every great deck uses a deliberate surface: a saturated color block, a warm cream/off-white (`#F4F1EC`-ish), a dark navy/near-black, or a gradient. The background can shift per slide (Headspace, Linktree do this), but it must read as intentional, not default.580581### 2. Headlines dominate582583The headline occupies roughly the **top 30–40%** of the canvas — much bigger than a typical web hero. If a person can't read it at thumbnail size with no zoom, redesign.584585### 3. Mixed emphasis inside the headline586587Almost every great headline has one word styled differently from the rest — a contrast color, an italic script, a heavier weight, or a hand-drawn underline. Examples:588- Superlist: "The one app that fits **your whole day**" (script + coral)589- Headspace: "Stress **less**" (`less` orange against black)590- Arc Search: "**Fastest** way to search. **Cleanest** way to browse." (purple / navy)591592Flat single-color headlines look weaker. Pick one emphasis word per slide.593594### 4. Decorative accents are the rule, not the exception595596Top decks layer at least one of these on most slides:597- Hand-drawn squiggles, arrows, scribbles (Superlist)598- Sparkles / glow (Gentler Streak, Arc)599- Label badges on the visual ("SUPER RAW", "Cinematic", "LUT")600- Floating widget chips with real stats ("$3,630 earned", "11,175 steps") — these tell the story without copy601- Award lockups on the hero only (Apple Design Award, Webby, star count)602603A bare phone on a bare bg with a bare headline is the default-skill output. Add one accent.604605### 5. Phone framing is a deliberate choice — vary it across the deck606607Three common framings, each carries a different feeling:608- **Bezelless / minimal frame** — maximizes UI legibility, modern (Arc, Linktree, Gentler)609- **Tilted floating phone with soft shadow** — product / advertorial feel (Superlist, CRED hero)610- **Full device with visible bezel, dead-center** — editorial, premium (CRED, NB Camera)611612Mix at least two framings across the deck.613614### 6. Proof anchors the hero, nothing else615616Award badges, press quotes, star counts, install counts — concentrate them on **slide 1 only**. Spreading them dilutes both the proof and the rest of the slides. NB Camera does this perfectly: Verge quote + Apple Design Award + 15,000+ stars all on the cover, none after.617618### 7. Density inside the phone, sparsity outside619620The screenshot inside the phone can (and should) be a real, dense product capture — actual lists, dashboards, charts, conversations. The space *outside* the phone is the opposite: one headline, one visual, one optional sub-line, one optional badge. Don't add bullet lists, multi-line paragraphs, or competing logos around the device.621622### 8. Break the phone parade623624Every 2–3 slides, drop the phone and use a different hero element to keep visual rhythm:625- 3D rendered product object (NB Camera's stylized camera)626- Photographic still (NB Camera slide 2)627- Real human / lifestyle photo (Linktree)628- Mascot illustration (Headspace's mascot, Gentler Streak's character)629- Typographic feature wall (Superlist's last slide)630- Phone grid mosaic (Linktree's "Trusted by 70M+" final slide)631632### 9. Last slide pattern633634The closer is almost always one of two things:635- **Feature wall** — a vertical list of one-word f636637…(truncated)