---
name: HyperShots
slug: hypershots
category: Automation
description: Generate spec-reliable App Store screenshots from HTML/CSS panels, with optional AI-generated assets. Ideal for store listings, marketing panels with device frames, and screenshot localization/translation.
github: "https://github.com/hypersocialinc/hypershots/tree/main/docs/superpowers/plans/2026-07-13-hypershots-skill.md"
language: Shell
stars: 16
forks: 0
install: "git clone https://github.com/hypersocialinc/hypershots"
added: 2026-07-18T04:41:54.705Z
last_synced: 2026-07-18T04:41:54.705Z
canonical_url: "https://dirskills.com/skills/hypershots"
---

# HyperShots

Generate spec-reliable App Store screenshots from HTML/CSS panels, with optional AI-generated assets. Ideal for store listings, marketing panels with device frames, and screenshot localization/translation.

**Install:** `git clone https://github.com/hypersocialinc/hypershots`

## README

# HyperShots Skill Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the `hypershots` agent skill — spec-reliable, multi-language App Store screenshots from deterministic HTML/CSS with optional AI-generated assets — as a standalone open-source repo installable via `npx skills add hypersocialinc/hypershots`.

**Architecture:** A skill repo (`skills/hypershots/`) shipping an immutable geometric frame kit (`frame.css` + `fit.js` + vendored fonts + `profiles.json`), operational scripts (preflight/scaffold/render/validate/edit-pass), reference docs for four gears (create/revise/translate/style-edit), and an annotated Spotless example set. Agents author bespoke panel HTML per app into a scaffolded `.shots/` workspace in the *user's* repo; headless Chrome renders exact store canvases; a validator enforces Apple's specs; genmedia (fal's public CLI) is a consent-gated soft dependency for generative assets only.

**Tech Stack:** Bash + Node (no npm deps at runtime), headless Chrome/Chromium, sips + ImageMagick, genmedia CLI (optional), gray-matter (dev-only, for the skill validator), GitHub Actions.

**Spec:** `docs/superpowers/specs/2026-07-13-hypershots-skill-design.md` (rev 2). Working dir for all tasks: the repo root (this repository).

---

## File structure (locked by this plan)

```
hypershots/
  .gitignore  LICENSE  package.json  README.md
  .github/workflows/validate.yml
  scripts/validate-skills.mjs            # repo CI (copied from agent-skills)
  tests/
    fixture/panel-fixture.html  fixture/theme.css
    run-tests.sh
  skills/hypershots/
    SKILL.md
    agents/openai.yaml
    profiles.json
    assets/frame.css  assets/fit.js  assets/fonts.css
    assets/fonts/*.woff2 + OFL-InterTight.txt + OFL-IBMPlexMono.txt
    references/store-specs.md  create.md  revise.md  translate.md
               i18n.md  edit-filter.md  asset-recipes.md  gotchas.md
    scripts/preflight.sh  scaffold.sh  render.sh  validate.sh
            translate-inject.mjs  make-mask.mjs  edit-pass.sh
    examples/spotless/README.md  brief.md  theme.css  panels/panel-*.html
              contact-sheet.png
```

Every skill script takes the workspace dir as its first argument and is
path-independent (resolves its own location via `$(dirname "$0")`).

---

### Task 1: Repo base (gitignore, LICENSE, package.json)

**Files:**
- Create: `.gitignore`, `LICENSE`, `package.json`

- [ ] **Step 1: Write `.gitignore`**

```gitignore
node_modules/
.DS_Store
*.log
# rendered output inside example/test workspaces (contact sheets are committed explicitly)
tests/fixture/out/
```

- [ ] **Step 2: Write `LICENSE` (MIT)**

```text
MIT License

Copyright (c) 2026 HyperSocial Incorporated

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

---
Vendored fonts (skills/hypershots/assets/fonts/) are licensed separately under
the SIL Open Font License 1.1 — see OFL-InterTight.txt and OFL-IBMPlexMono.txt
in that directory.
```

- [ ] **Step 3: Write `package.json`** (dev-only dep for the skill validator)

```json
{
  "name": "hypershots-repo",
  "private": true,
  "type": "module",
  "scripts": {
    "validate": "node scripts/validate-skills.mjs",
    "test": "bash tests/run-tests.sh"
  },
  "devDependencies": {
    "gray-matter": "^4.0.3"
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add .gitignore LICENSE package.json
git commit -m "chore: repo base (MIT license, package.json, gitignore)"
```

---

### Task 2: Skill skeleton + frontmatter (installer contract)

**Files:**
- Create: `skills/hypershots/SKILL.md` (skeleton — full content in Task 12)
- Create: `skills/hypershots/agents/openai.yaml`

- [ ] **Step 1: Write skeleton `SKILL.md`** — frontmatter is the installer contract; `name` MUST equal the directory name or `npx skills` silently drops the skill.

```markdown
---
name: hypershots
description: "Generate spec-reliable App Store screenshots from deterministic HTML/CSS panels, with optional AI-generated sticker assets and style grading. Use when the user wants App Store / store listing screenshots, marketing panels with device frames, screenshot localization or translation ('translate my screenshots'), or asks about Apple screenshot sizes and specs. Not for general image generation, social graphics, or capturing in-app screenshots from a simulator."
---

# HyperShots

(Full authoring guide lands in a later task. Layout under construction.)
```

- [ ] **Step 2: Write `agents/openai.yaml`** (catalog convention for Codex)

```yaml
interface:
  display_name: HyperShots
  short_description: Spec-reliable App Store screenshots from deterministic HTML + optional AI assets.
  default_prompt: Create App Store screenshots for my app using the hypershots skill. Ask me the brief questionnaire first.
```

- [ ] **Step 3: Commit**

```bash
git add skills/
git commit -m "feat: skill skeleton with installer frontmatter + openai.yaml"
```

---

### Task 3: Repo CI — skill validator

**Files:**
- Create: `scripts/validate-skills.mjs` (copy from the catalog repo)
- Create: `.github/workflows/validate.yml`

- [ ] **Step 1: Copy the validator from the catalog repo**

Run: `cp <catalog-repo>/scripts/validate-skills.mjs scripts/validate-skills.mjs`

Read the copied file; if it hardcodes the catalog repo's paths, adjust only the skills-dir constant so it scans `skills/` relative to the repo root (keep the frontmatter rules identical: `name` matches dir, non-empty `description`).

- [ ] **Step 2: Install dev dep and run the validator**

Run: `npm install && npm run validate`
Expected: PASS — 1 skill (`hypershots`) valid.

- [ ] **Step 3: Write `.github/workflows/validate.yml`**

```yaml
name: validate
on: [push, pull_request]
jobs:
  skills:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install
      - run: npm run validate
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: browser-actions/setup-chrome@v1
      - run: sudo apt-get update && sudo apt-get install -y imagemagick
      - run: CHROME="$(which chrome)" bash tests/run-tests.sh
```

(The `render` job will fail until Task 8 lands `tests/run-tests.sh`; that's fine — this repo isn't pushed until the end.)

- [ ] **Step 4: Commit**

```bash
git add scripts/ .github/ package-lock.json
git commit -m "ci: skill frontmatter validator + render test workflow"
```

---

### Task 4: profiles.json + vendored fonts

**Files:**
- Create: `skills/hypershots/profiles.json`
- Create: `skills/hypershots/assets/fonts/` (woff2 + OFL notices), `skills/hypershots/assets/fonts.css`

- [ ] **Step 1: Write `profiles.json`** — verified arithmetic: css × scale = exact store canvas.

```json
{
  "iphone-6.9": { "css": [430, 932], "scale": 3, "out": [1290, 2796], "class": "iphone", "note": "default; the only REQUIRED iPhone size" },
  "iphone-6.9-alt": { "css": [440, 956], "scale": 3, "out": [1320, 2868], "class": "iphone" },
  "iphone-6.5": { "css": [428, 926], "scale": 3, "out": [1284, 2778], "class": "iphone", "note": "legacy slot, still accepted" },
  "ipad-13": { "css": [1032, 1376], "scale": 2, "out": [2064, 2752], "class": "ipad", "note": "separate authoring pass — do not re-render iphone panels" }
}
```

- [ ] **Step 2: Download variable-font woff2 files** (Google Fonts serves woff2 to a Chrome UA; variable fonts cover all weights in one file per style)

```bash
cd skills/hypershots/assets/fonts
UA="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
curl -s -A "$UA" "https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,400..900;1,400..900&display=swap" -o it.css
curl -s -A "$UA" "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" -o pm.css
grep -oE 'https://[^)]+\.woff2' it.css pm.css | sed 's/^[^:]*://' | sort -u > urls.txt
# download each; name by hash-suffix is fine, we re-declare @font-face ourselves
i=0; while read -r u; do i=$((i+1)); curl -s "$u" -o "font-$i.woff2"; done < urls.txt
ls -la *.woff2
```

Then inspect `it.css`/`pm.css` to see which URL maps to which family/style, and rename to: `InterTight-var.woff2`, `InterTight-italic-var.woff2`, `IBMPlexMono-400.woff2`, `IBMPlexMono-500.woff2`, `IBMPlexMono-600.woff2`. Note: Google may serve per-script subsets (multiple URLs per family) — keep only the `latin` subset files (the css2 response labels each block with `/* latin */`). Delete `it.css pm.css urls.txt` and any leftover `font-*.woff2`.

- [ ] **Step 3: Fetch OFL notices**

```bash
curl -s https://raw.githubusercontent.com/google/fonts/main/ofl/intertight/OFL.txt -o OFL-InterTight.txt
curl -s https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexmono/OFL.txt -o OFL-IBMPlexMono.txt
head -3 OFL-InterTight.txt OFL-IBMPlexMono.txt   # sanity: real OFL text, not a 404 page
```

- [ ] **Step 4: Write `skills/hypershots/assets/fonts.css`**

```css
/* Vendored fonts (SIL OFL 1.1 — see fonts/OFL-*.txt). Local @font-face so
   renders are offline-deterministic: no Google Fonts network fetch, no silent
   fallback under --virtual-time-budget, no upstream metric drift. */
@font-face{font-family:'Inter Tight';src:url('fonts/InterTight-var.woff2') format('woff2');font-weight:400 900;font-style:normal;font-display:block}
@font-face{font-family:'Inter Tight';src:url('fonts/InterTight-italic-var.woff2') format('woff2');font-weight:400 900;font-style:italic;font-display:block}
@font-face{font-family:'IBM Plex Mono';src:url('fonts/IBMPlexMono-400.woff2') format('woff2');font-weight:400;font-display:block}
@font-face{font-family:'IBM Plex Mono';src:url('fonts/IBMPlexMono-500.woff2') format('woff2');font-weight:500;font-display:block}
@font-face{font-family:'IBM Plex Mono';src:url('fonts/IBMPlexMono-600.woff2') format('woff2');font-weight:600;font-display:block}
```

- [ ] **Step 5: Commit**

```bash
cd <hypershots-repo>
git add skills/hypershots/profiles.json skills/hypershots/assets/
git commit -m "feat: device profiles + vendored OFL fonts (offline-deterministic renders)"
```

---

### Task 5: frame.css (geometric contract) — the heart of the kit

**Files:**
- Create: `skills/hypershots/assets/frame.css`

Design rules (from spec): geometry only — **no brand tokens**; sized by per-profile
CSS variables so one panel re-renders across near-aspect iPhone profiles without
gutters; device anchors relative to panel dims; theme tokens are *consumed* here
but *defined* in the workspace `theme.css`.

- [ ] **Step 1: Write `frame.css`**

```css
/* HyperShots frame kit — GEOMETRIC CONTRACT. Do not put brand styling here.
   Theme tokens (--paper, --ink, --accent, fonts...) are defined per-app in the
   workspace theme.css. Panel dims come from the per-profile profile.css that
   render.sh generates: --panel-w / --panel-h.

   IMMUTABLE (agents must not restyle): .panel dims, .device geometry, .screen
   aspect, .di, .statusbar positions. VARIABLE: everything visual via theme
   tokens, plus sticker placement via inline left/top/width/transform. */

:root{
  --panel-w:428px; --panel-h:926px;      /* overridden by profile.css */
  --device-w-ratio:0.80;                 /* device width  / panel width  */
  --device-top-ratio:0.330;              /* device top    / panel height */
  --device-aspect:2.0906;                /* device height / width (715/342) */
  --device-top:calc(var(--panel-h) * var(--device-top-ratio));
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased;text-rendering:geometricPrecision}

.panel{
  position:relative;overflow:hidden;
  width:var(--panel-w);height:var(--panel-h);
  background:var(--paper,#f5f5f2);
  font-family:var(--font-sans,system-ui,-apple-system,sans-serif);
}

/* ---- copy block above the device ---- */
.wrap{position:relative;z-index:2;padding:42px 34px 0}
.eyebrow{display:flex;align-items:center;gap:14px}
.eyebrow b{font-family:var(--font-mono,ui-monospace,Menlo,monospace);font-weight:500;
  font-size:12px;letter-spacing:2.4px;text-transform:uppercase;color:var(--accent,#444);white-space:nowrap}
.eyebrow i{flex:1;height:1px;background:var(--rule,rgba(0,0,0,.16))}
.headline{font-weight:800;font-size:45px;line-height:1.02;letter-spacing:-1.3px;color:var(--ink,#111);margin-top:16px}
.sub{font-weight:500;font-size:19.5px;line-height:1.25;color:var(--mid,#555);margin-top:12px}

/* ---- device: FIXED geometry, identical on every panel of a set ----
   Screen aspect stays ~0.460 (matches a real 1206x2622 capture) at any
   profile because height derives from width via --device-aspect. */
.stage{position:absolute;inset:0;z-index:1}
.device{position:absolute;left:50%;top:var(--device-top);transform:translateX(-50%);
  width:calc(var(--panel-w) * var(--device-w-ratio));
  height:calc(var(--panel-w) * var(--device-w-ratio) * var(--device-aspect));
  background:#0a0a0c;border-radius:60px;padding:12px;
  box-shadow:0 34px 70px -22px rgba(0,0,0,.5), 0 0 0 2px #000 inset, 0 1px 0 rgba(255,255,255,.08) inset}
.screen{position:relative;width:100%;height:100%;border-radius:47px;overflow:hidden;background:var(--paper,#f5f5f2)}
.shot{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top center;display:block}

/* Dynamic Island + status bar — ONLY for hand-built screens. Real simulator
   captures already contain the device's own status bar + island: never stack
   these over a real capture (classic double-DI bug). */
.di{position:absolute;top:11px;left:50%;transform:translateX(-50%);width:100px;height:30px;
  background:#050506;border-radius:16px;z-index:8}
.statusbar{position:absolute;top:0;left:0;right:0;height:54px;z-index:7;color:var(--ink,#111);
  display:flex;align-items:center;justify-content:space-between;padding:16px 30px 0}
.statusbar .time{font-weight:600;font-size:16px;font-family:system-ui,-apple-system}
.statusbar .icons{display:flex;gap:6px;align-items:center}
.statusbar.light{color:#fff}

/* ---- breakout sticker primitives (position/rotate via inline style) ---- */
.sticker{position:absolute;z-index:20}
.emoji{position:absolute;z-index:20;line-height:1;filter:drop-shadow(0 10px 12px rgba(0,0,0,.28))}
.cutout{position:absolute;z-index:20;filter:drop-shadow(0 12px 16px rgba(0,0,0,.30))}
.chip{position:absolute;z-index:20;background:var(--paper-hi,#fff);border-radius:20px;
  padding:10px 16px;font-weight:800;box-shadow:0 12px 22px -8px rgba(0,0,0,.4);border:2px solid #fff}
.chip small{font-weight:600;color:var(--mid,#555);font-size:.6em}
.pin{position:absolute;z-index:20;color:#fff;font-weight:800;font-size:26px;
  padding:8px 14px;border-radius:14px;box-shadow:0 12px 20px -6px rgba(0,0,0,.45);border:2px solid #fff}
.pin::after{content:"";position:absolute;left:22px;bottom:-8px;width:16px;height:16px;
  background:inherit;border-radius:3px;transform:rotate(45deg)}
.gradeBadge{position:absolute;z-index:20;width:88px;height:88px;border-radius:22px;
  background:var(--badge-bg,#e7efe8);border:3px solid #fff;box-shadow:0 14px 26px -8px rgba(0,0,0,.42);
  display:flex;align-items:center;justify-content:center;color:var(--badge-ink,#2d5a3d);font-weight:800;font-size:52px}
```

- [ ] **Step 2: Eyeball-verify the variable math** (no render yet — that needs Task 6)

Check by hand: at 428 panel → device w = 342.4, h = 715.8, top = 305.6 (matches shipped Spotless values 342/715/306 within a pixel). At 430 → w = 344, h = 719.2, screen aspect (344−24)/(719.2−24) = 0.4603 ≈ 318/691. Write these numbers in a comment at the top of frame.css if any constant changes.

- [ ] **Step 3: Commit**

```bash
git add skills/hypershots/assets/frame.css
git commit -m "feat: geometric frame contract (profile-variable panel, fixed device aspect)"
```

---

### Task 6: fit.js + render.sh (render pipeline)

**Files:**
- Create: `skills/hypershots/assets/fit.js`
- Create: `skills/hypershots/scripts/render.sh`

- [ ] **Step 1: Write `fit.js`** — runs in-page before screenshot: waits for fonts, auto-fits `[data-fit]` text against the device-top budget (failure mode is *overlap with the device*, not overflow), dumps `[data-protect]` boxes + fit status into a DOM node that render.sh extracts via `--dump-dom`.

```js
/* HyperShots fit + boxes dump. Include LAST in every panel:
   <script src="fit.js"></script>
   Contract: [data-fit] on shrinkable copy blocks (optional data-fit-floor, px);
   [data-protect="name"] on regions the style-edit must preserve. */
(async () => {
  await document.fonts.ready;
  const root = document.documentElement;
  const cs = getComputedStyle(root);
  const deviceTop = parseFloat(cs.getPropertyValue('--panel-h')) *
                    parseFloat(cs.getPropertyValue('--device-top-ratio'));
  const failures = [];
  for (const el of document.querySelectorAll('[data-fit]')) {
    const maxBottom = el.dataset.fitMax ? parseFloat(el.dataset.fitMax) : deviceTop - 14;
    const floor = el.dataset.fitFloor ? parseFloat(el.dataset.fitFloor) : 26;
    let size = parseFloat(getComputedStyle(el).fontSize);
    while (el.getBoundingClientRect().bottom > maxBottom && size > floor) {
      size -= 1;
      el.style.fontSize = size + 'px';
    }
    if (el.getBoundingClientRect().bottom > maxBottom) {
      failures.push(el.dataset.i18n || el.className || 'unnamed');
    }
  }
  const boxes = [...document.querySelectorAll('[data-protect]')].map(el => {
    const r = el.getBoundingClientRect();
    return { name: el.dataset.protect || el.className,
             x: r.x, y: r.y, w: r.width, h: r.height };
  });
  const dump = document.createElement('script');
  dump.type = 'application/json';
  dump.id = 'hypershots-boxes';
  dump.textContent = JSON.stringify({ fitFailures: failures, boxes });
  document.body.appendChild(dump);
})();
```

- [ ] **Step 2: Write `render.sh`**

Interface: `render.sh <workspace> [profile] [locale]` (defaults: `iphone-6.9`, `en`).
Renders `<workspace>/panels/panel-*.html` (or `panels-<locale>/` for non-en) →
`<workspace>/out/<profile>/<locale>/panel-N.png` + `panel-N.boxes.json`.

```bash
#!/usr/bin/env bash
# HyperShots renderer: headless Chrome at an exact per-profile canvas.
set -euo pipefail

WS="${1:?usage: render.sh <workspace> [profile] [locale]}"
PROFILE="${2:-iphone-6.9}"
LOCALE="${3:-en}"
KIT="$(cd "$(dirname "$0")/.." && pwd)"     # skills/hypershots

find_chrome() {
  if [ -n "${CHROME:-}" ]; then echo "$CHROME"; return; fi
  local mac="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
  [ -x "$mac" ] && { echo "$mac"; return; }
  for c in google-chrome chromium chromium-browser; do
    command -v "$c" >/dev/null 2>&1 && { command -v "$c"; return; }
  done
  echo "ERROR: no Chrome/Chromium found. Set \$CHROME." >&2; exit 1
}
CHROME_BIN="$(find_chrome)"

# profile -> css dims + scale (node parses profiles.json; no jq depen
