Use when building App Store or Google Play screenshot pages, generating exportable marketing screenshots for iOS and/or Android apps, or scaffolding a screensho
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-app-store-screenshots-bba6e99ff571 ,按照其中的说明把「app-store-screenshots」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
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:
public/screenshots/uploaded/<hash>.png)app-store-screenshots.json at the project root (git-trackable) + localStorage mirrorhtml-to-image⌘Z / ⇧⌘Z) over the last 50 edits of the sessionSupported devices out of the box:
2880×1800, 2560×1600, 1440×900, 1280×800); see "Mac" under Step 5Screenshots 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.
template/ (co-located with this SKILL.md) into the user's working directory.public/screenshots/... and their app icon into public/.app-store-screenshots.json with the user's app name, starting copy, screenshots, and connected-canvas preference so the first preview is meaningful.You should NOT write page.tsx, device frames, or export logic by hand. They live in the template.
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).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.
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:
public/screenshots/, public/app-icon.png, uploaded screenshots, and any existing app-store-screenshots.json./tmp/app-store-screenshots-migration-<timestamp>/) and mention the path in the final response.app-store-screenshots.json with JSON tooling. Do not regex-edit JSON.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.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.dependencies, devDependencies, and useful scripts unless they directly conflict.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", "tvos", "watchos", "carplay", "mac", "android", "android-7", "android-10", "feature-graphic"];
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"
? 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") return value;
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") || "";
}
function migrateSlide(slide) {
if (!slide || typeof slide !== "object") 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 (cleaned) transforms[id] = cleaned;
}
const textElements = Array.isArray(slide.textElements)
? slide.textElements
.map((element) => {
const transform = cleanTransform(element.transform);
if (!element || typeof element.id !== "string" || !transform) return null;
return {
...element,
text: localized(element.text),
transform,
};
})
.filter(Boolean)
: undefined;
return {
...slide,
id: typeof slide.id === "string" ? slide.id : `migrated-${Math.random().toString(36).slice(2, 10)}`,
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),
...(Object.keys(transforms).length ? { transforms } : { transforms: undefined }),
...(textElements && textElements.length ? { textElements } : { textElements: undefined }),
};
}
state.schemaVersion = 2;
state.connectedCanvas = hasExplicitConnectedCanvas ? existingState.connectedCanvas : false;
state.locales = Array.isArray(state.locales) && 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.slidesByDevice && typeof state.slidesByDevice === "object") {
for (const [device, slides] of Object.entries(state.slidesByDevice)) {
if (!DEVICE_KEYS.includes(device)) continue;
state.slidesByDevice[device] = Array.isArray(slides) ? slides.map(migrateSlide).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:
"connectedCanvas": true.app-store-screenshots.json contains "schemaVersion": 2 and a boolean "connectedCanvas" value.Ask the user these. Do not proceed until you have answers:
ios-marketing-capture skill (https://github.com/ParthJadhav/ios-marketing-capture)?" If they say yes, install it with:
npx skills add ParthJadhav/ios-marketing-capture
Then have them run that skill first to generate the screenshots before continuing here.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."IMPORTANT: If the user gives instructions at any point, follow them. They override skill defaults.
Priority: bun > pnpm > yarn > npm.
which bun && echo bun || which pnpm && echo pnpm || which yarn && echo yarn || echo npm
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.
bun install # or pnpm install / yarn / npm install
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)
└── screenshots/
├── apple/
│ ├── iphone/{locale}/01.png … N.png
│ ├── ipad/{locale}/01.png … N.png
│ ├── 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.
If the user provided headlines, edit app-store-screenshots.json to set:
appNamethemeId (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.connectedCanvas (true for new connected decks; migrated legacy decks should stay false until the user opts in)label + headline + screenshot pathstypography: { 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.
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.
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.
| 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." |
| 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 |
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) |
Vary the layout field across slides. The editor exposes:
hero — centered headline + bottom-anchored devicedevice-bottom — same composition, smaller headlinedevice-top — flipped, device above caption (good contrast slide)two-devices — back + front phones layeredno-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.
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:
Bad cross-screen patterns:
Placement rules:
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.
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.
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.
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:
less orange against black)Flat single-color headlines look weaker. Pick one emphasis word per slide.
Top decks layer at least one of these on most slides:
A bare phone on a bare bg with a bare headline is the default-skill output. Add one accent.
Three common framings, each carries a different feeling:
Mix at least two framings across the deck.
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.
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.
Every 2–3 slides, drop the phone and use a different hero element to keep visual rhythm:
The closer is almost always one of two things:
Pick one. Don't make the last slide another single-feature hero — it wastes the spot.
Shrink the slide to ~160px wide (App Store search-result size). Squint. Can you read the headline? Can you tell what the app does in under a second? If not, the headline is too long, the type is too thin, or there's no contrast between text and background. Fix before exporting.
Always confirm the language list with the user before scaffolding — even if they didn't volunteer it. Ask: "Should screenshots be localized? If yes, which locales? (e.g. en, de, es, pt, ja)." Default to English-only if they say no or skip.
The project state file (app-store-screenshots.json) carries a locales: string[] field — the list of locale codes the project targets. The editor reads this to decide:
locales.length <= 1.After scaffolding, edit app-store-screenshots.json to set locales to the user's chosen list, e.g. "locales": ["en", "de", "ja"]. Also set "locale": "en" (or whichever is the source-of-truth language) so the editor opens on it.
The editor stores headlines and labels per-locale on each slide — switch to a locale and type to fill it in; unfilled locales fall back to en at preview time. Screenshots are a single string per slide; put {locale} anywhere in the path and the editor substitutes the active locale at render and export (e.g. /screenshots/apple/iphone/{locale}/01.png).
ar, he, fa, ur), the template handles direction inversion through CSS — let the user verify each slide looks intentional, not just flipped.Inside the editor, the user picks a device, then hits Export bundle. A single zip downloads with every required size × every project locale for that device, organized as <platform>/<device>/<WxH>/<locale>/NN-<layout>.png (e.g. ios/iphone/1320x2868/en/01-hero.png, macos/mac/2880x1800/en/01-hero.png). Repeat per device.
When connectedCanvas is enabled, exports are crops of the connected canvas, not isolated screen renders. If a mockup sits halfway across screen 2 and screen 3, screen 2's PNG contains its left crop and screen 3's PNG contains its right crop exactly as placed. Legacy decks should start with connectedCanvas: false, including Step 0 migrations, so old offscreen/clipped elements export as they did before. The user can turn on Connected after intentionally composing cross-screen elements.
Before export, zoom out to inspect the connected canvas as a strip, then inspect the individual cropped screens. Cross-screen elements should feel intentional in the strip and harmless in isolation.
Project locales come from app-store-screenshots.json locales field — set during scaffolding (Step 4). Single-locale projects produce a flat per-size structure with just the one locale folder.
Each slide is rendered once per locale at canvas resolution and scaled to every export size. The exporter waits until every visible screenshot has actually painted before it saves a PNG (Safari/WebKit decodes images inside the render asynchronously, which used to produce blank device screens), and shows a warning toast naming the screen if one never appears.
If exports come out blank or with black screen rectangles:
objectFit: cover, but truly transparent sources can still produce black regions.public/; export retries paths that were previously missing before it starts rendering.Every Apple TV and Apple Watch size below was read from App Store Connect's own metadata (asc screenshots sizes --all). Re-derive it the same way if Apple changes the slots.
| Device | Display type | Accepted sizes | Canvas |
|---|---|---|---|
| Apple TV | APP_APPLE_TV | 3840×2160, 1920×1080 (landscape only) | 3840×2160 |
| Apple Watch | APP_WATCH_ULTRA | 422×514, 410×502 | 422×514 |
| Apple Watch | APP_WATCH_SERIES_10 | 416×496 | ↑ |
| Apple Watch | APP_WATCH_SERIES_7 | 396×484 | ↑ |
| Apple Watch | APP_WATCH_SERIES_4 | 368×448 | ↑ |
| Apple Watch | APP_WATCH_SERIES_3 | 312×390 | ↑ |
| CarPlay | iPhone slots (landscape) | 2868×1320, 2778×1284, 2622×1206, 2436×1125 | 2868×1320 |
carplay device is a head-unit frame on a landscape 6.9" iPhone canvas for exactly that. Head units vary by vehicle; the frame uses Apple's CarPlay Simulator "Standard" 800×480 preset (5:3). Change CARPLAY_RATIO in src/lib/constants.ts for another preset (Minimum 748×456, Widescreen 1920×720, Portrait 900×1200, Video Playback 1920×1080).split-landscape (caption left, device right) is the strongest layout for the wide TV and CarPlay canvases. On the watch, keep headlines to two or three short words per line — the canvas is only 422 px wide.| Device | Display type | Accepted sizes | Canvas |
|---|---|---|---|
| Mac | APP_DESKTOP | 2880×1800, 2560×1600, 1440×900, 1280×800 (16:10 landscape only) | 2880×1800 |
macos/mac/<WxH>/<locale>/ rather than inside ios/. Every Mac size is an exact 16:10 downscale of the canvas; nothing is trimmed.hero → split-landscape → device-top (inverted) → two-devices → no-device. Because the window is contained, hero and device-bottom look almost the same; prefer split-landscape or two-devices (two overlapping windows) for variety.split-landscape — never two devices side-by-sidesplit-landscape or hero; Watch headlines fit on the 422 px canvas without wrapping mid-phraseinverted: true) slide when the deck is long enough| Mistake | Fix |
|---|---|
Edited page.tsx instead of using the editor | Roll back the edit; let users iterate in the browser |
| Tried to rebuild device frames from scratch | They're in src/components/editor/device-frames.tsx — modify there |
| Pasted screenshots into git directly | public/screenshots/... is fine to commit. Drop-target uploads are now also written to public/screenshots/uploaded/<hash>.png — commit both that folder and app-store-screenshots.json so collaborators reproduce your deck after git clone. |
| Wrong directory layout for tablet screenshots | See Step 2 — android/tablet-7/portrait/{locale}/... etc. |
| Reset wiped the deck | Reset clears in-memory state and re-saves defaults to app-store-screenshots.json. Recover by git checkout app-store-screenshots.json if it was committed, or export first before resetting. |
| Export is blank | Check the export toast for a "may be missing" warning and re-export; otherwise the source PNG probably has alpha — flatten to RGB |
| Looked for a CarPlay slot in App Store Connect | There isn't one — upload CarPlay shots into the iPhone slot |
| Mac window shows two title bars | The source is a single-window capture with its own title bar — crop it off or use a full-screen 16:10 capture |
bun dev port collision | Template defaults to next dev; let Next pick the next free port (3001+) |
The current template writes schemaVersion: 2. Existing projects made by earlier versions of this skill usually have no schemaVersion and may still store string label / headline values. Do not hand-edit those projects unless the JSON is invalid. On load, src/lib/storage.ts:
{ "en": "..." } objects.connectedCanvas: false, so already-clipped phones or captions do not suddenly appear in neighboring exports.app-store-screenshots.json and localStorage only after the file endpoint has loaded successfully, so stale browser cache cannot overwrite the canonical project file during dev-server restarts.There are two migration modes:
connectedCanvas: false for pre-v2 JSON so old exports remain visually stable.schemaVersion: 2. Preserve an existing explicit connectedCanvas boolean; otherwise write connectedCanvas: false without asking more product/design questions.For explicit in-place upgrades, copy the current template's src/components/editor/, src/lib/, app routes, config, and package files into the project while preserving user assets and project JSON. If the old project had custom themes, merge those THEMES entries into src/lib/constants.ts; otherwise the editor falls back to clean-light and warns in the browser. Then run the app once and confirm schemaVersion: 2 and a boolean connectedCanvas are present.
The template structure (after copy):
project/
├── package.json
├── tsconfig.json
├── next.config.mjs
├── tailwind.config.ts
├── postcss.config.mjs
├── components.json # ShadCN config (for future `shadcn add`)
├── public/
│ ├── mockup.png # iPhone bezel (do NOT replace without re-measuring PHONE_SCREEN)
│ ├── app-icon.png # → user supplies
│ ├── fonts/imported/ # Fonts imported from the toolbar (gitignored, like screenshots/uploaded/)
│ └── screenshots/...
└── src/
├── app/
│ ├── layout.tsx # Font + root layout
│ ├── page.tsx # Renders <ScreenshotEditor />
│ └── globals.css # Tailwind + ShadCN tokens
├── components/
│ ├── editor/
│ │ ├── screenshot-editor.tsx # Top-level editor (state, autosave, export)
│ │ ├── toolbar.tsx # Platform tabs, device select, theme, font, locale, undo/redo, export
│ │ ├── sidebar.tsx # Screen list with @dnd-kit reordering
│ │ ├── slide-thumb.tsx # Draggable screen card
│ │ ├── preview-stage.tsx # ResizeObserver-scaled connected canvas
│ │ ├── inspector.tsx # Right-pane controls for active slide
│ │ ├── screenshot-picker.tsx # File drop + picker
│ │ ├── background-controls.tsx # Per-slide theme / alternate / custom background
│ │ ├── font-importer.tsx # Hidden input behind the toolbar's "Import font…"
│ │ ├── image-element-canvas.tsx # Image overlay content (+ create-image-mask.ts edge fade)
│ │ ├── slide-canvas.tsx # Data-driven screen/deck renderer (all layouts)
│ │ └── device-frames.tsx # Phone, IPad, AppleTV, AppleWatch, CarPlayScreen, MacWindow, Android
│ └── ui/ # Minimal ShadCN primitives (button, select, etc.)
└── lib/
├── constants.ts # Canvas sizes, export sizes, themes, screenshot fonts, frame ratios
├── defaults.ts # Initial slide decks per device
├── types.ts # Slide / ProjectState / Theme types
├── storage.ts # useProject() — autosave + undo/redo history
├── image-cache.ts # preloadImages + img() helper
├── contrast.ts # Readable caption colours on custom backgrounds
├── export-render.ts # Slide → PNG; waits for every screenshot to paint
└── utils.ts # cn() helper
When you finish scaffolding, start the dev server (bun dev / pnpm dev / yarn dev / npm run dev) and then tell the user the following, in this order:
http://localhost:3000 (or whichever port Next picked — read it from the dev server output and quote the actual URL). Tell them to open it in the browser.bun install # only needed the first time, or after pulling new deps
bun dev # → http://localhost:3000
Substitute pnpm / yarn / npm run as appropriate for what was detected in Step 2.Check out apps generated by this skill here: https://www.parthjadhav.com/products/app-store-screenshots — and tag @parthjadhav8 on Twitter if you want your app to be added to the showcase.