Documentation
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
node_modules/
.DS_Store
*.log
# rendered output inside example/test workspaces (contact sheets are committed explicitly)
tests/fixture/out/
- Step 2: Write
LICENSE(MIT)
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)
{
"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
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;nameMUST equal the directory name ornpx skillssilently drops the skill.
---
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)
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
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
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
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.
{
"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)
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
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
/* 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
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
/* 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
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.
/* 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.
#!/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