Token导航 LogoToken导航TokenDH.com
前端设计需要联网github未标认证来源可访问许可证需确认审计通过

app-store-screenshots应用商店截图

Agent Skill

app-store-screenshots 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

196

周安装

8

GitHub Stars

133

下载量

63
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:app-store-screenshots(应用商店截图)
来源仓库:https://github.com/keremerkan/ascelerate
仓库路径:skills/app-store-screenshots
安装命令:
npx skills add https://github.com/keremerkan/ascelerate --skill app-store-screenshots
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/keremerkan/ascelerate --skill app-store-screenshots

简介

app-store-screenshots 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息。

  • 适合围绕仓库状态、代码变更或协作事项进行整理和分析。
  • 通过 npx skills add 命令从指定 GitHub 仓库安装,需结合原始 README 确认具体用法。
  • 安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写操作。
  • 适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

SKILL.md

App Store Screenshots Generator

Overview

Build a Next.js page that renders App Store screenshots for iPhone and iPad as advertisements (not UI showcases) and exports them via html-to-image at Apple's required resolutions. Uses pre-framed device screenshots from ascelerate screenshot frame (Apple device bezels with pixel-perfect accuracy, including Dynamic Island and rounded corners). Screenshots are the single most important conversion asset on the App Store.

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*.

Step 1: Ask the User These Questions

Before writing ANY code, gather all required information. Ask questions one at a time — wait for the user's answer before asking the next question. Dumping all questions at once is overwhelming. If the user volunteers information about upcoming questions, acknowledge it and skip those when you get to them.

Required

  1. Framed screenshots — "Where are your framed device screenshots? (from ascelerate screenshot frame — typically screenshots/framed/)"
  2. App icon — "Where is your app icon PNG?"
  3. Brand colors — "What are your brand colors? (accent color, text color, background preference)"
  4. Font — "What font does your app use? (or what font do you want for the screenshots?)"
  5. Feature list — "List your app's features in priority order. What's the #1 thing your app does?"
  6. Number of slides — "How many screenshots do you want? (Apple allows up to 10)"
  7. Style direction — "What style do you want? Examples: warm/organic, dark/moody, clean/minimal, bold/colorful, gradient-heavy, flat. Share App Store screenshot references if you have any."

Optional (ask after required questions are answered)

  1. Localization — "Do you want screenshots in multiple languages? If so, which languages? (e.g. English, German, French). You'll get a language switcher in the preview to toggle between them."
  2. Component assets — "Do you have any UI element PNGs (cards, widgets, etc.) you want as floating decorations? If not, that's fine — we'll skip them."
  3. Additional instructions — "Any specific requirements, constraints, or preferences?"

Derived from answers (do NOT ask — decide yourself)

Based on the user's style direction, brand colors, and app aesthetic, decide:

  • Background style: gradient direction, colors, whether light or dark base
  • Decorative elements: blobs, glows, geometric shapes, or none — match the style
  • Dark vs light slides: how many of each, which features suit dark treatment
  • Typography treatment: weight, tracking, line height — match the brand personality
  • Color palette: derive text colors, secondary colors, shadow tints from the brand colors

IMPORTANT: If the user gives additional instructions at any point during the process, follow them. User instructions always override skill defaults.

Step 2: Set Up the Project

Requirements

  • Node.js 18+
  • Framed screenshots — generated by ascelerate screenshot frame (see ascelerate). The user should have framed device screenshots ready before starting.

Project Location

Default location is app-store-screenshots/ at the root of the user's working directory. Ask the user: "I'll create the project in app-store-screenshots/. Want me to use a different folder or a temporary location instead?" If the directory already exists with a Next.js project, reuse it.

Git tracking: After creating the project, check if the working directory is a git repo (git rev-parse --is-inside-work-tree). If so, suggest adding the project folder (or at minimum its node_modules/) to .gitignore.

Detect Package Manager

Check what's available, use this priority: bun > pnpm > yarn > npm

# Check in order
which bun && echo "use bun" || which pnpm && echo "use pnpm" || which yarn && echo "use yarn" || echo "use npm"

Scaffold (if no existing Next.js project)

Create the project inside app-store-screenshots/:

mkdir -p app-store-screenshots && cd app-store-screenshots

# With bun:
bunx create-next-app@latest . --typescript --app --src-dir --no-eslint --no-tailwind --import-alias "@/*"
bun add html-to-image jszip

# With pnpm:
pnpx create-next-app@latest . --typescript --app --src-dir --no-eslint --no-tailwind --import-alias "@/*"
pnpm add html-to-image jszip

# With yarn:
yarn create next-app . --typescript --app --src-dir --no-eslint --no-tailwind --import-alias "@/*"
yarn add html-to-image jszip

# With npm:
npx create-next-app@latest . --typescript --app --src-dir --no-eslint --no-tailwind --import-alias "@/*"
npm install html-to-image jszip

File Structure

app-store-screenshots/
├── public/
│   ├── app-icon.png            # User's app icon
│   └── framed/                 # Framed screenshots from ascelerate
│       ├── en-US/              # Per-locale directories (matches ascelerate output)
│       │   ├── iPhone 17 Pro Max-01-home.png
│       │   ├── iPhone 17 Pro Max-02-settings.png
│       │   └── iPad Pro 13-inch (M5)-01-home.png
│       └── de-DE/
│           └── ...
├── src/app/
│   ├── copy.json               # Localized copy (editable without touching code)
│   ├── layout.tsx              # Font setup
│   └── page.tsx                # The screenshot generator (single file)
└── package.json

Copy or symlink the framed screenshots from ascelerate's output (screenshots/framed/) into public/framed/. The directory structure matches ascelerate's output: {locale}/{device}-{name}.png.

If the app UI is the same across languages (only marketing text changes), copy only one locale — the Device component's onError fallback handles missing locales.

The generator is page.tsx + copy.json. No routing, no extra layouts, no API routes. All text lives in copy.json so the user can update copy without editing components.

Font Setup

// src/app/layout.tsx
import { YourFont } from "next/font/google"; // Use whatever font the user specified
const font = YourFont({ subsets: ["latin"] });

export default function Layout({ children }: { children: React.ReactNode }) {
  return <html><body className={font.className}>{children}</body></html>;
}

Step 2.5: Set Up Framed Screenshots

Before building the page, copy the framed device screenshots into the project. These are pre-generated by ascelerate screenshot frame, which composites raw screenshots into Apple device bezels with pixel-perfect accuracy (Dynamic Island, rounded corners, bezel fully intact).

Copy Framed Screenshots

Based on the user's answer to question #8, copy or symlink the framed output:

# Copy framed screenshots from ascelerate's output into the Next.js public directory
cp -r /path/to/screenshots/framed/* public/framed/

The framed directory structure from ascelerate looks like:

screenshots/framed/
├── en-US/
│   ├── iPhone 17 Pro Max-01-home.png
│   ├── iPhone 17 Pro Max-02-settings.png
│   └── iPad Pro 13-inch (M5)-01-home.png
└── de-DE/
    └── ...

If the app UI doesn't change across languages (only marketing text differs), copy only one locale — the Device component's onError fallback handles missing locales.

Important Notes

  • Framing is configured per-device in ascelerate/screenshot.yml via frameDevice and deviceBezel — see ascelerate docs
  • The framed images already contain the device bezel, Dynamic Island, and rounded corners — the Next.js page just renders them as <img> tags
  • If the user hasn't framed their screenshots yet, they should run ascelerate screenshot frame first

Step 3: Plan the Slides

Screenshot Framework (Narrative Arc)

Adapt this framework to the user's requested slide count. Not all slots are required — pick what fits:

SlotPurposeNotes
#1Hero / Main BenefitApp icon + tagline + home screen. This is the ONLY one most people see.
#2DifferentiatorWhat makes this app unique vs competitors
#3EcosystemWidgets, extensions, watch — beyond the main app. Skip if N/A.
#4+Core FeaturesOne feature per slide, most important first
2nd to lastTrust SignalIdentity/craft — "made for people who [X]"
LastMore FeaturesPills listing extras + coming soon. Skip if few features.

Rules:

  • Each slide sells ONE idea. Never two features on one slide.
  • Vary layouts across slides — never repeat the same template structure.
  • Include 1-2 contrast slides (inverted bg) for visual rhythm.

Step 4: Write Copy FIRST

Get all headlines approved before building layouts. Bad copy ruins good design.

The Iron Rules

  1. One idea per headline. Never join two things with "and."
  2. Short, common words. 1-2 syllables. No jargon unless it's domain-specific.
  3. 3-5 words per line. Must be readable at thumbnail size in the App Store.
  4. Line breaks are intentional. Control where lines break with <br />.

Three Approaches (pick one per slide)

TypeWhat it doesExample
Paint a momentYou picture yourself doing it"Check your coffee without opening the app."
State an outcomeWhat your life looks like after"A home for every coffee you buy."
Kill a painName a problem and destroy it"Never waste a great bag of coffee."

What NEVER Works

  • Feature lists as headlines: "Log every item with tags, categories, and notes"
  • Two ideas joined by "and": "Track X and never miss Y"
  • Compound clauses: "Save and customize X for every Y you own"
  • Vague aspirational: "Every item, tracked"
  • Marketing buzzwords: "AI-powered tips" (unless it's actually AI)

Copy Process

  1. Write 3 options per slide using the three approaches
  2. Read each at arm's length — if you can't parse it in 1 second, it's too complex
  3. Check: does each line have 3-5 words? If not, adjust line breaks
  4. Present options to the user with reasoning for each
  5. If localized: translate approved headlines into all requested languages. Respect the tone/formality of each language. Line breaks may need adjusting per language — German and French text is often longer than English.

Reference Apps for Copy Style

  • Raycast — specific, descriptive, one concrete value per slide
  • Turf — ultra-simple action verbs, conversational
  • Mela / Notion — warm, minimal, elegant

Step 5: Build the Page

Architecture

page.tsx
├── Constants (IPHONE_W, IPHONE_H, IPAD_W, IPAD_H, design tokens)
├── LOCALES (code, label, asc locale code for App Store Connect)
├── ASC_DEVICE (maps "iphone" → "APP_IPHONE_67", "ipad" → "APP_IPAD_PRO_3GEN_129")
├── COPY (imported from copy.json — localized strings per language, keyed by slide name)
├── Device component (renders a pre-framed device image — just an <img>)
├── Caption component (label + headline)
├── Decorative components (blobs, glows, shapes — based on style direction)
├── Screenshot1..N components (one per slide, with iPhone and/or iPad variants)
├── SCREENSHOTS array (registry, with device type tag)
├── ScreenshotPreview (ResizeObserver scaling + hover export)
└── ScreenshotsPage (grid + toolbar with language switcher + device filter + Export/Export All Languages)

Export Sizes (Largest Only — Apple auto-scales to smaller displays)

App Store Connect accepts the largest screenshot size per device and automatically scales it for smaller displays. Only generate:

// iPhone 6.9" — covers all iPhone sizes
const IPHONE_W = 1320;
const IPHONE_H = 2868;

// iPad 13" — covers all iPad sizes
const IPAD_W = 2064;
const IPAD_H = 2752;

Rendering Strategy

Each screenshot is designed at full export resolution. Two copies exist:

  1. Preview: CSS transform: scale() via ResizeObserver to fit a grid card
  2. Export: Offscreen at position: absolute; left: -9999px at true resolution

If both iPhone and iPad are requested:

  • Separate sections: Render iPhone and iPad in distinct sections with device headers (<h2>iPhone</h2>, <h2>iPad</h2>), each with its own grid. Do NOT mix them in a single grid.
  • Device filter: The toolbar includes an All / iPhone / iPad toggle that shows/hides sections.
  • Each section filters SCREENSHOTS.filter(s => s.device === "iphone") etc.

Localization

If the user wants multiple languages, store all copy in a separate copy.json file so the user can edit text without touching code:

src/app/copy.json
{
  "en": {
    "iphone-hero": { "headline": "Spam texts,<br/>silenced.", "pills": ["Smart", "Private"] },
    "iphone-privacy": { "label": "On-device AI", "headline": "Completely<br/>private.", "sub": "..." }
  },
  "de": {
    "iphone-hero": { "headline": "Spam-SMS,<br/>verstummt.", "pills": ["Intelligent", "Datenschutz"] },
    "iphone-privacy": { "label": "KI auf dem Gerät", "headline": "Vollständig<br/>privat.", "sub": "..." }
  }
}

Import and type-cast it in page.tsx:

import COPY_JSON from "./copy.json";
const COPY = COPY_JSON as Record<Locale, Record<string, SlideCopy>>;

Each slide component receives the current locale and looks up its copy from COPY[locale][slideName].

Localized device images: If app screenshots differ per language (localized UI), ascelerate's framed output is already organized by locale: public/framed/en-US/, public/framed/de-DE/, etc. Slide components use the current locale in the path: src={/framed/${ascLocale}/${deviceName}-${screenshotName}.png}. One locale is the required default — all others are optional and fall back via onError on the Device component.

ScreenshotPreview Component

Each slide is previewed using CSS scale-down via ResizeObserver. Clicking a preview exports that single slide:

function ScreenshotPreview({ def, index, locale, w, h }: {
  def: ScreenshotDef; index: number; locale: Locale; w: number; h: number;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [scale, setScale] = useState(0.2);
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      for (const entry of entries) setScale(entry.contentRect.width / w);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [w]);
  const Comp = def.component;
  return (
    <div ref={containerRef}
      style={{ position: "relative", overflow: "hidden", borderRadius: 12,
        border: "1px solid #e5e7eb", aspectRatio: `${w}/${h}`,
        cursor: "pointer", background: "#fafafa" }}>
      <div style={{ transformOrigin: "top left", transform: `scale(${scale})`, width: w, height: h }}>
        <Comp w={w} h={h} locale={locale} />
      </div>
      <div style={{ position: "absolute", bottom: 8, left: 8, background: "rgba(0,0,0,0.6)",
        color: "#fff", fontSize: 11, fontWeight: 600, padding: "3px 8px", borderRadius: 6 }}>
        #{index + 1} — {def.name}
      </div>
    </div>
  );
}

Toolbar & Page Layout

The main page has a toolbar with a language switcher, device filter (if both iPhone and iPad), and export buttons. The grid shows preview cards. Offscreen containers hold full-resolution slides for export:

export default function ScreenshotsPage() {
  const exportRefs = useRef<Map<number, HTMLDivElement>>(new Map());
  const [exporting, setExporting] = useState(false);
  const [locale, setLocale] = useState<Locale>("en");
  // If both iPhone and iPad: const [deviceFilter, setDeviceFilter] = useState<"all"|"iphone"|"ipad">("all");

  return (
    <div style={{ padding: 32, maxWidth: 1400, margin: "0 auto" }}>
      {/* Toolbar */}
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 32, flexWrap: "wrap" }}>
        <h1 style={{ fontSize: 24, fontWeight: 700, color: "#111", margin: 0 }}>App Screenshots</h1>
        <div style={{ flex: 1 }} />

        {/* Language Switcher */}
        <select value={locale} onChange={(e) => setLocale(e.target.value as Locale)}
          style={{ padding: "8px 12px", borderRadius: 8, border: "1px solid #e5e7eb",
            background: "#fff", color: "#333", fontWeight: 600, fontSize: 14, cursor: "pointer" }}>
          {LOCALES.map((l) => (
            <option key={l.code} value={l.code}>{l.label} ({l.code})</option>
          ))}
        </select>

        {/* Device filter (only if both iPhone and iPad) */}
        {/* <select value={deviceFilter} onChange={...}> All / iPhone / iPad </select> */}

        <button onClick={handleExportAll} disabled={exporting}
          style={{ padding: "8px 20px", borderRadius: 8, border: "none",
            background: exporting ? "#999" : BRAND_COLOR, color: "#fff",
            fontWeight: 600, fontSize: 14, cursor: exporting ? "not-allowed" : "pointer" }}>
          {exporting ? "Exporting..." : `Export (${locale.toUpperCase()})`}
        </button>
        <button onClick={handleExportAllLanguages} disabled={exporting}
          style={{ padding: "8px 20px", borderRadius: 8, border: "none",
            background: exporting ? "#999" : BRAND_DARK, color: "#fff",
            fontWeight: 600, fontSize: 14, cursor: exporting ? "not-allowed" : "pointer" }}>
          {exporting ? "Exporting..." : "Export All Languages"}
        </button>
      </div>

      {/* Screenshots Grid */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 20 }}>
        {SCREENSHOTS.map((def, i) => (
          <div key={def.name} onClick={() => handleExportSingle(i)}>
            <ScreenshotPreview def={def} index={i} locale={locale} w={IPHONE_W} h={IPHONE_H} />
          </div>
        ))}
      </div>

      {/* Offscreen Export Containers */}
      {SCREENSHOTS.map((def, i) => {
        const Comp = def.component;
        return (
          <div key={`export-${i}`}
            ref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}
            style={{ position: "absolute", left: -9999, fontFamily: '"SF Pro Display", "SF Pro", system-ui, sans-serif' }}>
            <Comp w={IPHONE_W} h={IPHONE_H} locale={locale} />
          </div>
        );
      })}
    </div>
  );
}

Export Handlers

Three export functions: single slide, current language, and all languages:

async function captureScreenshot(el: HTMLElement, w: number, h: number): Promise<string> {
  const wrapper = el.parentElement!;
  wrapper.style.left = "0px";
  wrapper.style.opacity = "1";
  wrapper.style.zIndex = "-1";
  const opts = { width: w, height: h, pixelRatio: 1, cacheBust: true };
  await toPng(el, opts); // warm-up
  const dataUrl = await toPng(el, opts); // actual capture
  wrapper.style.left = "-9999px";
  wrapper.style.opacity = "";
  wrapper.style.zIndex = "";
  return dataUrl;
}

// Export current language as zip
const handleExportAll = useCallback(async () => {
  setExporting(true);
  const zip = new JSZip();
  const ascLocale = LOCALES.find((l) => l.code === locale)!.asc;
  for (let i = 0; i < SCREENSHOTS.length; i++) {
    const def = SCREENSHOTS[i];
    const el = exportRefs.current.get(i);
    if (!el) continue;
    const prefix = String(i + 1).padStart(2, "0");
    const dataUrl = await captureScreenshot(el, IPHONE_W, IPHONE_H);
    zip.file(`${ascLocale}/${ASC_DEVICE}/${prefix}_${def.name}.png`, dataUrlToBlob(dataUrl));
    await new Promise((r) => setTimeout(r, 300));
  }
  const blob = await zip.generateAsync({ type: "blob" });
  const url = URL.createObjectURL(blob);
  downloadDataUrl(url, `screenshots-${ascLocale}.zip`);
  URL.revokeObjectURL(url);
  setExporting(false);
}, [locale]);

// Export ALL languages as single zip
const handleExportAllLanguages = useCallback(async () => {
  setExporting(true);
  const zip = new JSZip();
  const originalLocale = locale;
  for (const lang of LOCALES) {
    setLocale(lang.code);
    await new Promise((r) => setTimeout(r, 500)); // wait for re-render
    for (let i = 0; i < SCREENSHOTS.length; i++) {
      const def = SCREENSHOTS[i];
      const el = exportRefs.current.get(i);
      if (!el) continue;
      const prefix = String(i + 1).padStart(2, "0");
      const dataUrl = await captureScreenshot(el, IPHONE_W, IPHONE_H);
      zip.file(`${lang.asc}/${ASC_DEVICE}/${prefix}_${def.name}.png`, dataUrlToBlob(dataUrl));
      await new Promise((r) => setTimeout(r, 300));
    }
  }
  setLocale(originalLocale);
  const blob = await zip.generateAsync({ type: "blob" });
  const url = URL.createObjectURL(blob);
  downloadDataUrl(url, "appname-screenshots.zip");
  URL.revokeObjectURL(url);
  setExporting(false);
}, [locale]);

// Export single slide as PNG
const handleExportSingle = useCallback(async (idx: number) => {
  const def = SCREENSHOTS[idx];
  const el = exportRefs.current.get(idx);
  if (!el || !def) return;
  setExporting(true);
  const prefix = String(idx + 1).padStart(2, "0");
  const filename = `${prefix}-iphone-${def.name}-${locale}-${IPHONE_W}x${IPHONE_H}.png`;
  const dataUrl = await captureScreenshot(el, IPHONE_W, IPHONE_H);
  downloadDataUrl(dataUrl, filename);
  setExporting(false);
}, [locale]);

Helper functions:

function downloadDataUrl(dataUrl: string, filename: string) {
  const link = document.createElement("a");
  link.download = filename;
  link.href = dataUrl;
  link.click();
}

function dataUrlToBlob(dataUrl: string): Uint8Array {
  const base64 = dataUrl.split(",")[1];
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes;
}

The "Export All Languages" zip structure is compatible with ascelerate apps media upload <app> --folder screenshots.zip --replace for direct upload to App Store Connect.

Device Component

Since ascelerate screenshot frame produces complete device-framed images (bezel + screenshot composited together), the device component is just an <img> tag — no CSS clipping, no screen offset calculations, no border-radius hacks:

function Device({ src, alt, style }: {
  src: string; alt: string; style?: React.CSSProperties;
}) {
  return (
    <img
      src={src}
      alt={alt}
      style={style}
      draggable={false}
      onError={(e) => {
        // Fall back to English if localized image is missing
        const img = e.currentTarget;
        const enSrc = img.src.replace(/\/(?!en\/)[a-z]{2}\//, "/en/");
        if (img.src !== enSrc) img.src = enSrc;
      }}
    />
  );
}

The framed images already contain the device bezel with the Dynamic Island, rounded corners, and screen content composited pixel-perfectly. The transparent areas around the device allow it to sit naturally on any background. The onError fallback ensures missing locale-specific images gracefully degrade to English.

Typography (Resolution-Independent)

All sizing relative to canvas width W (use IPHONE_W or IPAD_W depending on device):

ElementSizeWeightLine Height
Category labelW * 0.028600 (semibold)default
HeadlineW * 0.09 to W * 0.1700 (bold)1.0
Hero headlineW * 0.1700 (bold)0.92

Device Placement Patterns

Vary across slides — NEVER use the same layout twice in a row:

Centered device (hero, single-feature):

bottom: 0, width: "82-86%", translateX(-50%) translateY(12-14%)

Two devices layered (comparison):

Back: left: "-8%", width: "65%", rotate(-4deg), opacity: 0.55
Front: right: "-4%", width: "82%", translateY(10%)

Device + floating elements (only if user provided component PNGs):

Cards should NOT block the device's main content.
Position at edges, slight rotation (2-5deg), drop shadows.
If distracting, push partially off-screen or make smaller.

iPad-Specific Layout Notes

iPad screenshots have a landscape-ish aspect ratio (2064×2752, closer to 3:4 vs iPhone's ~1:2.2). This affects layouts:

  • The device image is wider relative to the canvas — use narrower widths (70-78% instead of 82-86%) to avoid crowding
  • Captions may need to sit above the device rather than beside it, since the wider device leaves less horizontal space
  • Two-device layouts work best with smaller scaling and more overlap
  • iPad's larger visible screen area means the app UI is more readable — lean into showing the content

"More Features" Slide (Optional)

Dark/contrast background with app icon, headline ("And so much more."), and feature pills. Can include a "Coming Soon" section with dimmer pills.

Step 6: Export

Why html-to-image, NOT html2canvas

html2canvas breaks on CSS filters, gradients, drop-shadow, backdrop-filter, and complex clipping. html-to-image uses native browser SVG serialization — handles most CSS faithfully.

IMPORTANT: Use filter: drop-shadow() instead of box-shadow. While html-to-image handles most CSS well, box-shadow renders with visible boxy artifacts in exports. Always use CSS filter: drop-shadow() for shadows on devices, floating elements, and cards. Note that drop-shadow blur radius is roughly half of box-shadow blur for a similar visual effect:

/* BAD — renders boxy/corrupted in exports */
box-shadow: 0 16px 50px rgba(0,0,0,0.35);

/* GOOD — renders correctly in exports */
filter: drop-shadow(0 16px 25px rgba(0,0,0,0.35));

Export Implementation

import { toPng } from "html-to-image";

// CRITICAL: el must be the slide component's root div (position: relative),
// NOT the offscreen wrapper. Use ref on wrapper, then ref.firstElementChild.
// toPng must capture the element that owns the positioned children.
const wrapper = el.parentElement!;

// Before capture: move wrapper on-screen
wrapper.style.left = "0px";
wrapper.style.opacity = "1";
wrapper.style.zIndex = "-1";

const opts = { width: W, height: H, pixelRatio: 1, cacheBust: true };

// CRITICAL: Double-call trick — first warms up fonts/images, second produces clean output
await toPng(el, opts);
const dataUrl = await toPng(el, opts);

// After capture: move wrapper back off-screen
wrapper.style.left = "-9999px";
wrapper.style.opacity = "";
wrapper.style.zIndex = "";

Use W = IPHONE_W, H = IPHONE_H for iPhone slides and W = IPAD_W, H = IPAD_H for iPad slides.

Offscreen container setup: The wrapper div is position: absolute; left: -9999px with fontFamily set. The ref should resolve to the slide's root div inside it (via firstElementChild), not the wrapper itself:

ref={(el) => { if (el?.firstElementChild) exportRefs.current.set(i, el.firstElementChild as HTMLDivElement); }}

Key Rules

  • Double-call trick: First toPng() loads fonts/images lazily. Second produces clean output. Without this, exports are blank.
  • On-screen for capture: Temporarily move to left: 0 before calling toPng.
  • Offscreen wrapper: Use position: absolute; left: -9999px (not fixed). Do NOT set width/height on the wrapper — let the slide component define its own dimensions.
  • No resizing needed: Each slide is already at the exact export resolution. No canvas scaling step.
  • Zip export (ascelerate compatible): Two export buttons. "Export ({LOCALE})" exports the current language. "Export All Languages" loops through every locale (sets state, waits for re-render, captures). Both create a zip compatible with ascelerate. Structure: en-US/APP_IPHONE_67/01_hero.png en-US/APP_IPAD_PRO_3GEN_129/01_hero.png de-DE/APP_IPHONE_67/01_hero.png Device type mapping: iphone → APP_IPHONE_67, ipad → APP_IPAD_PRO_3GEN_129. Each locale entry needs an asc field for the App Store Connect locale code (e.g. {code: "en", label: "English", asc: "en-US"}). Files are ordered alphabetically so use zero-padded prefixes: 01_name.png.
  • Single-click export: Downloads individual PNGs with device in filename: 01-iphone-hero-en-1320x2868.png.
  • 300ms delay between sequential exports.
  • Set fontFamily on the offscreen container.
  • Use String(index + 1).padStart(2, "0") for numbering.

Common Mistakes

MistakeFix
All slides look the sameVary device position (center, left, right, two-device, no-device)
Decorative elements invisibleIncrease size and opacity — better too visible than invisible
Copy is too complex"One second at arm's length" test
Floating elements block the deviceMove off-screen edges or above the device
Plain white/black backgroundUse gradients — even subtle ones add depth
Too clutteredRemove floating elements, simplify to device + caption
Too simple/emptyAdd larger decorative elements, floating items at edges
Device frame touches canvas edgeAlways keep padding between the device and the canvas sides — touching the edge looks cheap and cramped
Headlines use "and"Split into two slides or pick one idea
No visual contrast across slidesMix light and dark backgrounds
Export is blankUse double-call trick; move element on-screen before capture
Export has empty top, content at bottomtoPng must target the slide's root div (has position: relative), not the offscreen wrapper
Screenshots not framedRun ascelerate screenshot frame first, then copy screenshots/framed/ to public/framed/
Shadows look boxy/corrupted in exportUse filter: drop-shadow() instead of box-shadowhtml-to-image doesn't render box-shadow correctly
Framed image looks wrongCheck bezel configuration in ascelerate/screenshot.yml — bezel must match the capture device

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

36.99%
按下载量换算23

Claude

31.36%
按下载量换算20

Cursor

18.66%
按下载量换算12

Gemini CLI

9.14%
按下载量换算6

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills