Feature Demo Skill
Entry point: the /feature-demo slash command → commands/feature-demo.md (copied into ~/.claude/commands/ at install time).
Install and run: see README.md.
Brand presets
Brand is read through brand.ts (the selector). The default preset is neutral (brand-agnostic, no logo). Switch presets with the FEATURE_DEMO_BRAND env var:
FEATURE_DEMO_BRAND=pagefly npx tsx scripts/run-render.tsx png ...
To add your own: create presets/<name>.ts exporting BrandTokens, register it in brand.ts, drop the logo PNGs into assets/logos/, then run with FEATURE_DEMO_BRAND=<name>.
Files
types.ts - DevInput / AssetSpec / BrandTokens schemas (Zod)
brand.ts - Brand preset selector (reads the FEATURE_DEMO_BRAND env var)
presets/ - Brand token presets (neutral is the default, plus pagefly)
templates/ - Template recipes (4 templates: hero-split, hero-stack, feature-callout, product-card)
renderers/ - Figma + Playwright + Paper stub
prompts/ - System prompts for pick-template / write-copy / classify-feedback
scripts/agent-entry.tsx - Orchestrator helpers (pure Node, Zod-validated)
scripts/run-render.tsx - CLI render tool (PNG/Figma plan)
assets/fonts/ - Poppins woff2, local (SIL OFL, redistributable)
outputs/ - Render artifacts (gitignored)
Intent-first architecture
Two render entry points in renderers/figma-renderer.tsx:
buildFigmaRenderPlan(spec) - template-routed. Caller picks templateId + variation, registry resolves intent.
buildFigmaRenderPlanFromIntent({ intent, size, fileName, frameName? }) - intent-first. Caller supplies LayoutIntent directly. Use for slot files and hand-authored recipes.
Slot pattern (slots/slot-N-*.ts):
- Owns its own
spec + config (fileName, size).
- Exports
slotNIntent: LayoutIntent + slotNConfig.
- Slot file calls
templates[id].buildIntent(spec, brand) by default. To deviate from the template, edit the slot file: replace the template call with hand-authored regions.
- Driver script imports slot, calls
buildFigmaRenderPlanFromIntent, writes outputs. Driver stays thin.
Region schema (templates/shared/layout-intent.ts) reserves z?: number + anchor?: { ref, edge, offset } for overlay anchoring. The renderer ignores them today (bounds are absolute).
Prompts
All three prompt files below are used when the main agent calls a sub-LLM inside the /feature-demo loop:
prompts/pick-template.md - Pick the template + variation from the feature spec.
prompts/write-copy.md - Generate the heading + bullets from the feature spec (Zod-bounded).
prompts/classify-feedback.md - Classify user feedback into one of 8 intents (DONE, CHANGE_TEMPLATE, CHANGE_VARIATION, CHANGE_COPY, CHANGE_MODE, CHANGE_SIZE, REJECT_BRAND, UNCLEAR).
Orchestrator entry
scripts/agent-entry.tsx exposes pure-Node helpers for the slash command:
readFeatureSpec(path, repoRoot) - read the spec, validate the path is inside the repo.
resolveScreenshots(paths, repoRoot) - check they exist, max 3.
parseSize(str) - parse "WIDTHxHEIGHT" → Size.
buildAssetSpec(inputs) - build and Zod-validate an AssetSpec.
renderByMode(spec, outDir) - dispatch to the PNG/Figma/Paper renderer.
PROMPT_PATHS - constants pointing at the three prompt files above.
DEFAULT_OUTPUT_DIR - defaults to .claude/skills/feature-demo/outputs/.
This file does not call an LLM or MCP. Every LLM call and all MCP orchestration happens in the main agent, in the slash command context.
Hard rules
- A real UI screenshot must be an
<img> pass-through: the output embeds the original pixels. The agent MAY view the image itself to decide layout and framing, but must NEVER feed the image through a model to recreate or redraw it.
- One landscape screenshot (single landscape): by default show it IN FULL, with padding against the canvas edge - never flush to the border. All four corners rounded. (The engine centers and pads automatically when
hero-split has only one screenshot.)
- Needs to zoom into detail -> let the image bleed off an edge: if a landscape image must be enlarged past one edge for the detail to be readable, the corners TOUCHING that edge are NOT rounded (squared off) so the cut reads naturally; the other corners stay rounded. Take care not to cut away important content. This is the existing device-duo behaviour (the bleeding part is squared off-canvas).
- Device duo (desktop + mobile): the landscape image MAY be enlarged past the canvas edge (positioned only, never redrawn - the pixels stay original); the portrait image MUST be shown in full, with no important content cropped. The default for a landscape + portrait pair is
hero-stack --variation=top (top and bottom); hero-split is the fallback when the user wants left and right.
- Square 1:1 canvas (e.g. 1080x1080): ALWAYS use
hero-stack --variation=top, the only template whose image frame sizes to the image's own aspect ratio, so neither a portrait nor a landscape gets cropped. Copy must be SHORT: heading ≤28 characters + EXACTLY 1 bullet, at a font size small enough to leave ~70% of the canvas for the image. Do NOT use hero-split / product-card (they crop at square width) or hero-stack --variation=bottom (a tall portrait pushes the copy off-canvas). The engine forces hero-stack/top and trims the bullets to one at square canvases, as a safety net if the agent picks wrong.
- Brand tokens are immutable from agent code. Only a human edits
brand.ts.
- Schema-validated I/O at every step.
- The agent asks for the output mode before rendering. Never default it.
- After rendering: wait for user feedback, never auto-retry.
- Render exactly ONE option by default. The agent commits to the best layout and does NOT fan out several options for the user to choose from. Render more than once only when the user explicitly asks to compare.
- Fit-check before rendering (the user is often a dev who cannot picture the layout): compare the real inputs (copy length + the orientation and count of the screenshots just viewed) against the target dimensions. On a clear mismatch, do NOT render a cramped result and stop there - advise the user with one specific fix first. The typical cases:
- Square 1:1 but long copy (heading >28 characters or >1 bullet) -> advise shortening the copy.
- Square 1:1 but the screenshot is a tall portrait -> the image eats the full height and crams the copy; advise a landscape/contained shot, or switching to a portrait canvas.
- 16:9 hero but only one tall portrait shot -> the wide canvas is left mostly empty; advise adding a desktop shot, or switching to a square/portrait canvas.
- Heading too long at any size -> advise trimming (Zod rejects anything over 60 characters).
State 2-3 advisory lines plus the specific fix, then let the user choose (fix the input, or render with the safe fallback). Never render through an obvious mismatch without telling the user first.
Output modes
| Mode |
Use case |
Status |
figma |
Fine-tune in Figma |
Ready |
png |
A dev needs an image quickly |
Ready |
paper |
Future |
Stub - throws "not configured" |
Workflow
- The user supplies a feature spec + a screenshot path.
- The agent asks for the output mode (figma / png).
- The agent views the screenshot itself (Read tool) to learn its orientation and where the feature sits in the frame.
- The agent commits to one template + variation and writes the copy on-brand, based on what it saw in step 3.
- Render exactly one option -> an output path.
- (png) The agent opens the output itself and checks the feature is clearly visible and not cropped; it adjusts at most once if needed, and only then shows the user.
- Show one single result plus one line on why that layout was chosen. Wait for feedback. The user drives the iteration.
Figma orchestration
The Figma renderer is a pure function: buildFigmaRenderPlan(spec) returns { fileName, screenshotsToUpload, pluginCode, intent, canvas } (pluginCode is minified to cut the token cost of passing it into use_figma). MCP calls must run in the main agent context; a subagent has no MCP.
Session cache (cuts token cost)
The agent reads .claude/skills/feature-demo/outputs/.figma-session.json through readFigmaSession() from agent-entry.tsx:
fileKey + fileUrl - the Figma file used last time
planKey - the team plan (does not change between renders)
Ask the user: reuse the previous file, or create a new one? If the user supplies a Figma URL in natural language ("generate into file ABC https://figma.com/file/XYZ"), parse it with parseFigmaFileKey() and save it to the session.
After each successful orchestration: call writeFigmaSession({ fileKey, fileUrl, planKey }).
MCP sequence
mcp__figma__whoami → get planKey. SKIP if the session already has a planKey.
mcp__figma__create_new_file({ fileName, planKey, editorType: 'design' }) → { file_key, file_url }. SKIP if the user is using an existing file (session.fileKey, or a URL from the user).
mcp__figma__upload_assets({ fileKey, count: N }) → returns N submitUrls. For each path in screenshotsToUpload, POST the file to its matching submitUrl (multipart file field preferred); the response contains an imageHash.
- Build
IMAGE_HASHES = { [absPath]: hash, ... } and prepend it to pluginCode as const IMAGE_HASHES = {...};. Optionally clean up the placeholder nodes upload_assets auto-creates (node ids 1:2, 2:2, 3:2...) with figma.getNodeByIdAsync(id).remove().
mcp__figma__use_figma({ fileKey, code: prepended, description }) → executes the plugin. 50K character limit.
- DO NOT call the verify steps by default:
mcp__figma__get_metadata and mcp__figma__get_screenshot return very heavy responses (5-20K tokens). Call them only when the user explicitly asks to verify, or when the render fails. The user sees the result directly in the Figma file.
- Return the
file_url to the user and call writeFigmaSession().
Natural language input
The user does not have to remember flags. The agent parses natural language to extract
(the Vietnamese keywords below are deliberate - users type in either language):
- Output mode: "png" / "ảnh" / "figma" / "vào figma"
- Figma file URL: any
figma.com/file/... or figma.com/design/... link in the message
- Template: "hero stack", "hero split", "feature callout", "product card"
- Variation: "top/bottom/left/right/upper/lower"
- Heading + bullets: if the user supplies clear text
When the user supplies template + variation + heading + bullets in full, SKIP the pick-template and write-copy LLM calls and build the AssetSpec directly.
1---2name: feature-demo3description: Feature Demo Skill4---56# Feature Demo Skill78Entry point: the `/feature-demo` slash command → `commands/feature-demo.md` (copied into `~/.claude/commands/` at install time).910Install and run: see `README.md`.1112## Brand presets1314Brand is read through `brand.ts` (the selector). The default preset is `neutral` (brand-agnostic, no logo). Switch presets with the `FEATURE_DEMO_BRAND` env var:1516```17FEATURE_DEMO_BRAND=pagefly npx tsx scripts/run-render.tsx png ...18```1920To add your own: create `presets/<name>.ts` exporting `BrandTokens`, register it in `brand.ts`, drop the logo PNGs into `assets/logos/`, then run with `FEATURE_DEMO_BRAND=<name>`.2122## Files2324- `types.ts` - DevInput / AssetSpec / BrandTokens schemas (Zod)25- `brand.ts` - Brand preset selector (reads the `FEATURE_DEMO_BRAND` env var)26- `presets/` - Brand token presets (`neutral` is the default, plus `pagefly`)27- `templates/` - Template recipes (4 templates: hero-split, hero-stack, feature-callout, product-card)28- `renderers/` - Figma + Playwright + Paper stub29- `prompts/` - System prompts for pick-template / write-copy / classify-feedback30- `scripts/agent-entry.tsx` - Orchestrator helpers (pure Node, Zod-validated)31- `scripts/run-render.tsx` - CLI render tool (PNG/Figma plan)32- `assets/fonts/` - Poppins woff2, local (SIL OFL, redistributable)33- `outputs/` - Render artifacts (gitignored)3435## Intent-first architecture3637Two render entry points in `renderers/figma-renderer.tsx`:3839- `buildFigmaRenderPlan(spec)` - template-routed. Caller picks `templateId` + `variation`, registry resolves intent.40- `buildFigmaRenderPlanFromIntent({ intent, size, fileName, frameName? })` - intent-first. Caller supplies `LayoutIntent` directly. Use for slot files and hand-authored recipes.4142Slot pattern (`slots/slot-N-*.ts`):43- Owns its own `spec` + `config` (fileName, size).44- Exports `slotNIntent: LayoutIntent` + `slotNConfig`.45- Slot file calls `templates[id].buildIntent(spec, brand)` by default. To deviate from the template, edit the slot file: replace the template call with hand-authored regions.46- Driver script imports slot, calls `buildFigmaRenderPlanFromIntent`, writes outputs. Driver stays thin.4748Region schema (`templates/shared/layout-intent.ts`) reserves `z?: number` + `anchor?: { ref, edge, offset }` for overlay anchoring. The renderer ignores them today (bounds are absolute).4950## Prompts5152All three prompt files below are used when the main agent calls a sub-LLM inside the `/feature-demo` loop:5354- `prompts/pick-template.md` - Pick the template + variation from the feature spec.55- `prompts/write-copy.md` - Generate the heading + bullets from the feature spec (Zod-bounded).56- `prompts/classify-feedback.md` - Classify user feedback into one of 8 intents (`DONE`, `CHANGE_TEMPLATE`, `CHANGE_VARIATION`, `CHANGE_COPY`, `CHANGE_MODE`, `CHANGE_SIZE`, `REJECT_BRAND`, `UNCLEAR`).5758## Orchestrator entry5960`scripts/agent-entry.tsx` exposes pure-Node helpers for the slash command:6162- `readFeatureSpec(path, repoRoot)` - read the spec, validate the path is inside the repo.63- `resolveScreenshots(paths, repoRoot)` - check they exist, max 3.64- `parseSize(str)` - parse "WIDTHxHEIGHT" → `Size`.65- `buildAssetSpec(inputs)` - build and Zod-validate an `AssetSpec`.66- `renderByMode(spec, outDir)` - dispatch to the PNG/Figma/Paper renderer.67- `PROMPT_PATHS` - constants pointing at the three prompt files above.68- `DEFAULT_OUTPUT_DIR` - defaults to `.claude/skills/feature-demo/outputs/`.6970This file does **not** call an LLM or MCP. Every LLM call and all MCP orchestration happens in the main agent, in the slash command context.7172## Hard rules73741. A real UI screenshot must be an `<img>` pass-through: the output embeds the original pixels. The agent MAY view the image itself to decide layout and framing, but must NEVER feed the image through a model to recreate or redraw it.75 - **One landscape screenshot (single landscape)**: by default show it IN FULL, with padding against the canvas edge - never flush to the border. All four corners rounded. (The engine centers and pads automatically when `hero-split` has only one screenshot.)76 - **Needs to zoom into detail -> let the image bleed off an edge**: if a landscape image must be enlarged past one edge for the detail to be readable, the corners TOUCHING that edge are NOT rounded (squared off) so the cut reads naturally; the other corners stay rounded. Take care not to cut away important content. This is the existing device-duo behaviour (the bleeding part is squared off-canvas).77 - Device duo (desktop + mobile): the landscape image MAY be enlarged past the canvas edge (positioned only, never redrawn - the pixels stay original); the portrait image MUST be shown in full, with no important content cropped. The default for a landscape + portrait pair is `hero-stack --variation=top` (top and bottom); `hero-split` is the fallback when the user wants left and right.78 - **Square 1:1 canvas (e.g. 1080x1080)**: ALWAYS use `hero-stack --variation=top`, the only template whose image frame sizes to the image's own aspect ratio, so neither a portrait nor a landscape gets cropped. Copy must be SHORT: heading ≤28 characters + EXACTLY 1 bullet, at a font size small enough to leave ~70% of the canvas for the image. Do NOT use `hero-split` / `product-card` (they crop at square width) or `hero-stack --variation=bottom` (a tall portrait pushes the copy off-canvas). The engine forces `hero-stack/top` and trims the bullets to one at square canvases, as a safety net if the agent picks wrong.792. Brand tokens are immutable from agent code. Only a human edits `brand.ts`.803. Schema-validated I/O at every step.814. The agent asks for the output mode before rendering. Never default it.825. After rendering: wait for user feedback, never auto-retry.836. Render exactly ONE option by default. The agent commits to the best layout and does NOT fan out several options for the user to choose from. Render more than once only when the user explicitly asks to compare.847. **Fit-check before rendering (the user is often a dev who cannot picture the layout)**: compare the real inputs (copy length + the orientation and count of the screenshots just viewed) against the target dimensions. On a clear mismatch, do NOT render a cramped result and stop there - advise the user with one specific fix first. The typical cases:85 - Square 1:1 but long copy (heading >28 characters or >1 bullet) -> advise shortening the copy.86 - Square 1:1 but the screenshot is a tall portrait -> the image eats the full height and crams the copy; advise a landscape/contained shot, or switching to a portrait canvas.87 - 16:9 hero but only one tall portrait shot -> the wide canvas is left mostly empty; advise adding a desktop shot, or switching to a square/portrait canvas.88 - Heading too long at any size -> advise trimming (Zod rejects anything over 60 characters).89 State 2-3 advisory lines plus the specific fix, then let the user choose (fix the input, or render with the safe fallback). Never render through an obvious mismatch without telling the user first.9091## Output modes9293| Mode | Use case | Status |94|---|---|---|95| `figma` | Fine-tune in Figma | Ready |96| `png` | A dev needs an image quickly | Ready |97| `paper` | Future | Stub - throws "not configured" |9899## Workflow1001011. The user supplies a feature spec + a screenshot path.1022. The agent asks for the output mode (figma / png).1033. The agent **views the screenshot itself** (Read tool) to learn its orientation and where the feature sits in the frame.1044. The agent commits to one template + variation and writes the copy on-brand, based on what it saw in step 3.1055. Render exactly one option -> an output path.1066. (png) The agent opens the output itself and checks the feature is clearly visible and not cropped; it adjusts at most once if needed, and only then shows the user.1077. Show one single result plus one line on why that layout was chosen. Wait for feedback. The user drives the iteration.108109## Figma orchestration110111The Figma renderer is a pure function: `buildFigmaRenderPlan(spec)` returns `{ fileName, screenshotsToUpload, pluginCode, intent, canvas }` (`pluginCode` is minified to cut the token cost of passing it into `use_figma`). MCP calls must run in the main agent context; a subagent has no MCP.112113### Session cache (cuts token cost)114115The agent reads `.claude/skills/feature-demo/outputs/.figma-session.json` through `readFigmaSession()` from `agent-entry.tsx`:116117- `fileKey` + `fileUrl` - the Figma file used last time118- `planKey` - the team plan (does not change between renders)119120Ask the user: reuse the previous file, or create a new one? If the user supplies a Figma URL in natural language ("generate into file ABC https://figma.com/file/XYZ"), parse it with `parseFigmaFileKey()` and save it to the session.121122After each successful orchestration: call `writeFigmaSession({ fileKey, fileUrl, planKey })`.123124### MCP sequence1251261. `mcp__figma__whoami` → get `planKey`. **SKIP** if the session already has a `planKey`.1272. `mcp__figma__create_new_file({ fileName, planKey, editorType: 'design' })` → `{ file_key, file_url }`. **SKIP** if the user is using an existing file (`session.fileKey`, or a URL from the user).1283. `mcp__figma__upload_assets({ fileKey, count: N })` → returns N `submitUrl`s. For each path in `screenshotsToUpload`, POST the file to its matching submitUrl (multipart `file` field preferred); the response contains an `imageHash`.1294. Build `IMAGE_HASHES = { [absPath]: hash, ... }` and prepend it to `pluginCode` as `const IMAGE_HASHES = {...};`. Optionally clean up the placeholder nodes `upload_assets` auto-creates (node ids `1:2`, `2:2`, `3:2`...) with `figma.getNodeByIdAsync(id).remove()`.1305. `mcp__figma__use_figma({ fileKey, code: prepended, description })` → executes the plugin. 50K character limit.1316. **DO NOT call the verify steps by default**: `mcp__figma__get_metadata` and `mcp__figma__get_screenshot` return very heavy responses (5-20K tokens). Call them only when the user explicitly asks to verify, or when the render fails. The user sees the result directly in the Figma file.1327. Return the `file_url` to the user and call `writeFigmaSession()`.133134### Natural language input135136The user does not have to remember flags. The agent parses natural language to extract137(the Vietnamese keywords below are deliberate - users type in either language):138139- Output mode: "png" / "ảnh" / "figma" / "vào figma"140- Figma file URL: any `figma.com/file/...` or `figma.com/design/...` link in the message141- Template: "hero stack", "hero split", "feature callout", "product card"142- Variation: "top/bottom/left/right/upper/lower"143- Heading + bullets: if the user supplies clear text144145When the user supplies template + variation + heading + bullets in full, **SKIP** the `pick-template` and `write-copy` LLM calls and build the AssetSpec directly.